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/12/25 08:27:50 UTC

[ignite] branch ignite-ducktape updated: IGNITE-13882 Add parametrization to ducktape install_root and persistence_root (#8600)

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 cbf8bdf  IGNITE-13882 Add parametrization to ducktape install_root and persistence_root (#8600)
cbf8bdf is described below

commit cbf8bdfd05c316495d8e437e80d8fa92fd1088b3
Author: Ivan Daschinskiy <iv...@gmail.com>
AuthorDate: Fri Dec 25 11:27:21 2020 +0300

    IGNITE-13882 Add parametrization to ducktape install_root and persistence_root (#8600)
---
 .../ducktests/tests/ignitetest/services/ignite.py  |   4 +-
 .../tests/ignitetest/services/ignite_app.py        |   2 +-
 .../ducktests/tests/ignitetest/services/spark.py   |  68 ++++----
 .../ignitetest/services/utils/config_template.py   |   6 +-
 .../ignitetest/services/utils/control_utility.py   |   2 +-
 .../ignitetest/services/utils/ignite_aware.py      |  54 +++++--
 .../tests/ignitetest/services/utils/ignite_path.py |  56 -------
 .../services/utils/ignite_persistence.py           |  74 ---------
 .../tests/ignitetest/services/utils/ignite_spec.py |  82 ++++++----
 .../tests/ignitetest/services/utils/jmx_utils.py   |  15 +-
 .../tests/ignitetest/services/utils/path.py        | 173 +++++++++++++++++++++
 .../services/zk/templates/log4j.properties.j2      |   4 +-
 .../services/zk/templates/zookeeper.properties.j2  |   2 +-
 .../tests/ignitetest/services/zk/zookeeper.py      |  78 ++++++----
 .../ducktests/tests/ignitetest/tests/self_test.py  |  39 +++++
 modules/ducktests/tests/tox.ini                    |   1 -
 16 files changed, 410 insertions(+), 250 deletions(-)

diff --git a/modules/ducktests/tests/ignitetest/services/ignite.py b/modules/ducktests/tests/ignitetest/services/ignite.py
index 7923dc7..af866b2 100644
--- a/modules/ducktests/tests/ignitetest/services/ignite.py
+++ b/modules/ducktests/tests/ignitetest/services/ignite.py
@@ -17,7 +17,6 @@
 This module contains class to start ignite cluster node.
 """
 
-import os
 import re
 import signal
 from datetime import datetime
@@ -32,7 +31,6 @@ class IgniteService(IgniteAwareService):
     Ignite node service.
     """
     APP_SERVICE_CLASS = "org.apache.ignite.startup.cmdline.CommandLineStartup"
-    HEAP_DUMP_FILE = os.path.join(IgniteAwareService.PERSISTENT_ROOT, "ignite-heap.bin")
 
     # pylint: disable=R0913
     def __init__(self, context, config, num_nodes, jvm_opts=None, startup_timeout_sec=60, shutdown_timeout_sec=10,
@@ -42,7 +40,7 @@ class IgniteService(IgniteAwareService):
 
     def clean_node(self, node):
         node.account.kill_java_processes(self.APP_SERVICE_CLASS, clean_shutdown=False, allow_fail=True)
-        node.account.ssh("sudo rm -rf -- %s" % self.PERSISTENT_ROOT, allow_fail=False)
+        node.account.ssh("rm -rf -- %s" % self.persistent_root, allow_fail=False)
 
     def thread_dump(self, node):
         """
diff --git a/modules/ducktests/tests/ignitetest/services/ignite_app.py b/modules/ducktests/tests/ignitetest/services/ignite_app.py
index 8833582..502eb56 100644
--- a/modules/ducktests/tests/ignitetest/services/ignite_app.py
+++ b/modules/ducktests/tests/ignitetest/services/ignite_app.py
@@ -76,7 +76,7 @@ class IgniteApplicationService(IgniteAwareService):
 
         node.account.kill_java_processes(self.servicejava_class_name, clean_shutdown=False, allow_fail=True)
 
-        node.account.ssh("rm -rf %s" % self.PERSISTENT_ROOT, allow_fail=False)
+        node.account.ssh("rm -rf -- %s" % self.persistent_root, allow_fail=False)
 
     def pids(self, node):
         return node.account.java_pids(self.servicejava_class_name)
diff --git a/modules/ducktests/tests/ignitetest/services/spark.py b/modules/ducktests/tests/ignitetest/services/spark.py
index 09e99ac..39ec199 100644
--- a/modules/ducktests/tests/ignitetest/services/spark.py
+++ b/modules/ducktests/tests/ignitetest/services/spark.py
@@ -18,25 +18,22 @@ This module contains spark service class.
 """
 
 import os.path
+from distutils.version import LooseVersion
 
 from ducktape.cluster.remoteaccount import RemoteCommandError
 from ducktape.services.background_thread import BackgroundThreadService
 
-from ignitetest.services.utils.ignite_persistence import PersistenceAware
+from ignitetest.services.utils.path import PathAware
 from ignitetest.services.utils.log_utils import monitor_log
 
 
-class SparkService(BackgroundThreadService, PersistenceAware):
+# pylint: disable=abstract-method
+class SparkService(BackgroundThreadService, PathAware):
     """
     Start a spark node.
     """
-    INSTALL_DIR = "/opt/spark-{version}".format(version="2.3.4")
-    SPARK_PERSISTENT_ROOT = "/mnt/spark"
-
-    logs = {}
-
     # pylint: disable=R0913
-    def __init__(self, context, num_nodes=3):
+    def __init__(self, context, num_nodes=3, version=LooseVersion("2.3.4")):
         """
         :param context: test context
         :param num_nodes: number of Ignite nodes.
@@ -44,16 +41,20 @@ class SparkService(BackgroundThreadService, PersistenceAware):
         super().__init__(context, num_nodes)
 
         self.log_level = "DEBUG"
+        self._version = version
+        self.init_logs_attribute()
 
-        for node in self.nodes:
-            self.logs["master_logs" + node.account.hostname] = {
-                "path": self.master_log_path(node),
-                "collect_default": True
-            }
-            self.logs["worker_logs" + node.account.hostname] = {
-                "path": self.slave_log_path(node),
-                "collect_default": True
-            }
+    @property
+    def project(self):
+        return "spark"
+
+    @property
+    def version(self):
+        return self._version
+
+    @property
+    def globals(self):
+        return self.context.globals
 
     def start(self, clean=True):
         BackgroundThreadService.start(self, clean=clean)
@@ -69,14 +70,25 @@ class SparkService(BackgroundThreadService, PersistenceAware):
         else:
             script = "start-slave.sh spark://{spark_master}:7077".format(spark_master=self.nodes[0].account.hostname)
 
-        start_script = os.path.join(SparkService.INSTALL_DIR, "sbin", script)
+        start_script = os.path.join(self.home_dir, "sbin", script)
 
-        cmd = "export SPARK_LOG_DIR={spark_dir}; ".format(spark_dir=SparkService.SPARK_PERSISTENT_ROOT)
-        cmd += "export SPARK_WORKER_DIR={spark_dir}; ".format(spark_dir=SparkService.SPARK_PERSISTENT_ROOT)
+        cmd = "export SPARK_LOG_DIR={spark_dir}; ".format(spark_dir=self.persistent_root)
+        cmd += "export SPARK_WORKER_DIR={spark_dir}; ".format(spark_dir=self.persistent_root)
         cmd += "{start_script} &".format(start_script=start_script)
 
         return cmd
 
+    def init_logs_attribute(self):
+        for node in self.nodes:
+            self.logs["master_logs" + node.account.hostname] = {
+                "path": self.master_log_path(node),
+                "collect_default": True
+            }
+            self.logs["worker_logs" + node.account.hostname] = {
+                "path": self.slave_log_path(node),
+                "collect_default": True
+            }
+
     def start_node(self, node):
         self.init_persistent(node)
 
@@ -103,9 +115,9 @@ class SparkService(BackgroundThreadService, PersistenceAware):
 
     def stop_node(self, node):
         if node == self.nodes[0]:
-            node.account.ssh(os.path.join(SparkService.INSTALL_DIR, "sbin", "stop-master.sh"))
+            node.account.ssh(os.path.join(self.home_dir, "sbin", "stop-master.sh"))
         else:
-            node.account.ssh(os.path.join(SparkService.INSTALL_DIR, "sbin", "stop-slave.sh"))
+            node.account.ssh(os.path.join(self.home_dir, "sbin", "stop-slave.sh"))
 
     def clean_node(self, node):
         """
@@ -113,7 +125,7 @@ class SparkService(BackgroundThreadService, PersistenceAware):
         """
         node.account.kill_java_processes(self.java_class_name(node),
                                          clean_shutdown=False, allow_fail=True)
-        node.account.ssh("sudo rm -rf -- %s" % SparkService.SPARK_PERSISTENT_ROOT, allow_fail=False)
+        node.account.ssh("rm -rf -- %s" % self.persistent_root, allow_fail=False)
 
     def pids(self, node):
         """
@@ -135,26 +147,24 @@ class SparkService(BackgroundThreadService, PersistenceAware):
 
         return "org.apache.spark.deploy.worker.Worker"
 
-    @staticmethod
-    def master_log_path(node):
+    def master_log_path(self, node):
         """
         :param node: Spark master node.
         :return: Path to log file.
         """
         return "{SPARK_LOG_DIR}/spark-{userID}-org.apache.spark.deploy.master.Master-{instance}-{host}.out".format(
-            SPARK_LOG_DIR=SparkService.SPARK_PERSISTENT_ROOT,
+            SPARK_LOG_DIR=self.persistent_root,
             userID=node.account.user,
             instance=1,
             host=node.account.hostname)
 
-    @staticmethod
-    def slave_log_path(node):
+    def slave_log_path(self, node):
         """
         :param node: Spark slave node.
         :return: Path to log file.
         """
         return "{SPARK_LOG_DIR}/spark-{userID}-org.apache.spark.deploy.worker.Worker-{instance}-{host}.out".format(
-            SPARK_LOG_DIR=SparkService.SPARK_PERSISTENT_ROOT,
+            SPARK_LOG_DIR=self.persistent_root,
             userID=node.account.user,
             instance=1,
             host=node.account.hostname)
diff --git a/modules/ducktests/tests/ignitetest/services/utils/config_template.py b/modules/ducktests/tests/ignitetest/services/utils/config_template.py
index 875f12b..91c279f 100644
--- a/modules/ducktests/tests/ignitetest/services/utils/config_template.py
+++ b/modules/ducktests/tests/ignitetest/services/utils/config_template.py
@@ -20,8 +20,8 @@ import os
 
 from jinja2 import FileSystemLoader, Environment
 
-DEFAULT_CONFIG_PATH = os.path.dirname(os.path.abspath(__file__)) + "/templates"
-DEFAULT_IGNITE_CONF = DEFAULT_CONFIG_PATH + "/ignite.xml.j2"
+DEFAULT_CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates")
+DEFAULT_IGNITE_CONF = os.path.join(DEFAULT_CONFIG_PATH, "ignite.xml.j2")
 
 
 class ConfigTemplate:
@@ -69,4 +69,4 @@ class IgniteLoggerConfigTemplate(ConfigTemplate):
     Ignite logger configuration.
     """
     def __init__(self):
-        super().__init__(DEFAULT_CONFIG_PATH + "/log4j.xml.j2")
+        super().__init__(os.path.join(DEFAULT_CONFIG_PATH, "log4j.xml.j2"))
diff --git a/modules/ducktests/tests/ignitetest/services/utils/control_utility.py b/modules/ducktests/tests/ignitetest/services/utils/control_utility.py
index 90b6549..9b1a9ea 100644
--- a/modules/ducktests/tests/ignitetest/services/utils/control_utility.py
+++ b/modules/ducktests/tests/ignitetest/services/utils/control_utility.py
@@ -264,7 +264,7 @@ class ControlUtility:
         return output
 
     def __form_cmd(self, node, cmd):
-        return self._cluster.spec.path.script(f"{self.BASE_COMMAND} --host {node.account.externally_routable_ip} {cmd}")
+        return self._cluster.script(f"{self.BASE_COMMAND} --host {node.account.externally_routable_ip} {cmd}")
 
     @staticmethod
     def __parse_output(raw_output):
diff --git a/modules/ducktests/tests/ignitetest/services/utils/ignite_aware.py b/modules/ducktests/tests/ignitetest/services/utils/ignite_aware.py
index 06fbe32..cf0e9c7 100644
--- a/modules/ducktests/tests/ignitetest/services/utils/ignite_aware.py
+++ b/modules/ducktests/tests/ignitetest/services/utils/ignite_aware.py
@@ -29,19 +29,18 @@ from ducktape.services.background_thread import BackgroundThreadService
 from ducktape.utils.util import wait_until
 
 from ignitetest.services.utils.concurrent import CountDownLatch, AtomicValue
-from ignitetest.services.utils.ignite_persistence import IgnitePersistenceAware
+from ignitetest.services.utils.path import IgnitePathAware
 from ignitetest.services.utils.ignite_spec import resolve_spec
 from ignitetest.services.utils.jmx_utils import ignite_jmx_mixin
 from ignitetest.services.utils.log_utils import monitor_log
 
 
-class IgniteAwareService(BackgroundThreadService, IgnitePersistenceAware, metaclass=ABCMeta):
+# pylint: disable=too-many-public-methods
+class IgniteAwareService(BackgroundThreadService, IgnitePathAware, metaclass=ABCMeta):
     """
     The base class to build services aware of Ignite.
     """
 
-    NETFILTER_STORE_PATH = os.path.join(IgnitePersistenceAware.TEMP_DIR, "iptables.bak")
-
     # pylint: disable=R0913
     def __init__(self, context, config, num_nodes, startup_timeout_sec, shutdown_timeout_sec, **kwargs):
         """
@@ -58,10 +57,23 @@ class IgniteAwareService(BackgroundThreadService, IgnitePersistenceAware, metacl
         self.shutdown_timeout_sec = shutdown_timeout_sec
 
         self.spec = resolve_spec(self, context, config, **kwargs)
+        self.init_logs_attribute()
 
         self.disconnected_nodes = []
         self.killed = False
 
+    @property
+    def version(self):
+        return self.config.version
+
+    @property
+    def project(self):
+        return self.spec.project
+
+    @property
+    def globals(self):
+        return self.context.globals
+
     def start_async(self, clean=True):
         """
         Starts in async way.
@@ -89,7 +101,7 @@ class IgniteAwareService(BackgroundThreadService, IgnitePersistenceAware, metacl
 
         wait_until(lambda: self.alive(node), timeout_sec=10)
 
-        ignite_jmx_mixin(node, self.pids(node))
+        ignite_jmx_mixin(node, self.spec, self.pids(node))
 
     def stop_async(self):
         """
@@ -159,7 +171,7 @@ class IgniteAwareService(BackgroundThreadService, IgnitePersistenceAware, metacl
 
         node_config = self._prepare_config(node)
 
-        node.account.create_file(self.CONFIG_FILE, node_config)
+        node.account.create_file(self.config_file, node_config)
 
     def _prepare_config(self, node):
         if not self.config.consistent_id:
@@ -171,7 +183,7 @@ class IgniteAwareService(BackgroundThreadService, IgnitePersistenceAware, metacl
 
         config.discovery_spi.prepare_on_start(cluster=self)
 
-        node_config = self.spec.config_template.render(config_dir=self.PERSISTENT_ROOT, work_dir=self.WORK_DIR,
+        node_config = self.spec.config_template.render(config_dir=self.persistent_root, work_dir=self.work_dir,
                                                        config=config)
 
         setattr(node, "consistent_id", node.account.externally_routable_ip)
@@ -190,7 +202,7 @@ class IgniteAwareService(BackgroundThreadService, IgnitePersistenceAware, metacl
 
     # pylint: disable=W0613
     def _worker(self, idx, node):
-        cmd = self.spec.command(node.log_file)
+        cmd = self.spec.command(node)
 
         self.logger.debug("Attempting to start Application Service on %s with command: %s" % (str(node.account), cmd))
 
@@ -277,6 +289,13 @@ class IgniteAwareService(BackgroundThreadService, IgnitePersistenceAware, metacl
 
         task(node)
 
+    @property
+    def netfilter_store_path(self):
+        """
+        :return: path to store backup of iptables filter
+        """
+        return os.path.join(self.temp_dir, "iptables.bak")
+
     def drop_network(self, nodes=None):
         """
         Disconnects node from cluster.
@@ -311,12 +330,12 @@ class IgniteAwareService(BackgroundThreadService, IgnitePersistenceAware, metacl
     def __backup_iptables(self, nodes):
         # Store current network filter settings.
         for node in nodes:
-            cmd = "sudo iptables-save | tee " + IgniteAwareService.NETFILTER_STORE_PATH
+            cmd = f"sudo iptables-save | tee {self.netfilter_store_path}"
 
             exec_error = str(node.account.ssh_client.exec_command(cmd)[2].read(), sys.getdefaultencoding())
 
             if "Warning: iptables-legacy tables present" in exec_error:
-                cmd = "sudo iptables-legacy-save | tee " + IgniteAwareService.NETFILTER_STORE_PATH
+                cmd = f"sudo iptables-legacy-save | tee {self.netfilter_store_path}"
 
                 exec_error = str(node.account.ssh_client.exec_command(cmd)[2].read(), sys.getdefaultencoding())
 
@@ -330,7 +349,7 @@ class IgniteAwareService(BackgroundThreadService, IgnitePersistenceAware, metacl
 
     def __restore_iptables(self):
         # Restore previous network filter settings.
-        cmd = "sudo iptables-restore < " + IgniteAwareService.NETFILTER_STORE_PATH
+        cmd = f"sudo iptables-restore < {self.netfilter_store_path}"
 
         errors = []
 
@@ -359,8 +378,13 @@ class IgniteAwareService(BackgroundThreadService, IgnitePersistenceAware, metacl
         """
         Update the node log file.
         """
-        cnt = list(node.account.ssh_capture(f'ls {self.LOGS_DIR} | '
-                                            f'grep -E "^console_[0-9]*.log$" | '
-                                            f'wc -l', callback=int))[0]
+        if not hasattr(node, 'log_file'):
+            node.log_file = os.path.join(self.log_dir, "console.log")
 
-        node.log_file = self.STDOUT_STDERR_CAPTURE.replace('.log', f'_{cnt + 1}.log')
+        cnt = list(node.account.ssh_capture(f'ls {self.log_dir} | '
+                                            f'grep -E "^console.log(.[0-9]+)?$" | '
+                                            f'wc -l', callback=int))[0]
+        if cnt > 0:
+            rotated_log = os.path.join(self.log_dir, f"console.log.{cnt}")
+            self.logger.debug(f"rotating {node.log_file} to {rotated_log} on {node.name}")
+            node.account.ssh(f"mv {node.log_file} {rotated_log}")
diff --git a/modules/ducktests/tests/ignitetest/services/utils/ignite_path.py b/modules/ducktests/tests/ignitetest/services/utils/ignite_path.py
deleted file mode 100644
index 4089112..0000000
--- a/modules/ducktests/tests/ignitetest/services/utils/ignite_path.py
+++ /dev/null
@@ -1,56 +0,0 @@
-# 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 module contains ignite path resolve utilities.
-"""
-
-import os
-
-
-class IgnitePath:
-    """Path resolver for Ignite system tests which assumes the following layout:
-
-       /opt/ignite-dev          # Current version of Ignite under test
-       /opt/ignite-2.7.6        # Example of an older version of Ignite installed from tarball
-       /opt/ignite-<version>    # Other previous versions of Ignite
-       ...
-   """
-    SCRATCH_ROOT = "/mnt"
-    IGNITE_INSTALL_ROOT = "/opt"
-
-    def __init__(self, version, project="ignite"):
-        self.version = version
-        home_dir = "%s-%s" % (project, str(self.version))
-        self.home = os.path.join(IgnitePath.IGNITE_INSTALL_ROOT, home_dir)
-
-    def module(self, module_name):
-        """
-        :param module_name: name of Ignite optional lib
-        :return: absolute path to the specified module
-        """
-        if self.version.is_dev:
-            module_path = os.path.join("modules", module_name, "target")
-        else:
-            module_path = os.path.join("libs", "optional", "ignite-%s" % module_name)
-
-        return os.path.join(self.home, module_path)
-
-    def script(self, script_name):
-        """
-        :param script_name: name of Ignite script
-        :return: absolute path to the specified script
-        """
-        return os.path.join(self.home, "bin", script_name)
diff --git a/modules/ducktests/tests/ignitetest/services/utils/ignite_persistence.py b/modules/ducktests/tests/ignitetest/services/utils/ignite_persistence.py
deleted file mode 100644
index 4e018cc..0000000
--- a/modules/ducktests/tests/ignitetest/services/utils/ignite_persistence.py
+++ /dev/null
@@ -1,74 +0,0 @@
-# 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 module contains classes that represent persistent artifacts of tests
-"""
-
-import os
-
-from ignitetest.services.utils.config_template import IgniteLoggerConfigTemplate
-
-
-class PersistenceAware:
-    """
-    This class contains basic persistence artifacts
-    """
-    # Root directory for persistent output
-    PERSISTENT_ROOT = "/mnt/service"
-    TEMP_DIR = os.path.join(PERSISTENT_ROOT, "tmp")
-    LOGS_DIR = os.path.join(PERSISTENT_ROOT, "logs")
-    STDOUT_STDERR_CAPTURE = os.path.join(LOGS_DIR, "console.log")
-
-    logs = {
-        "console_log": {
-            "path": LOGS_DIR,
-            "collect_default": True
-        }
-    }
-
-    def init_persistent(self, node):
-        """
-        Init persistent directory.
-        :param node: Service node.
-        """
-        node.account.mkdirs(self.PERSISTENT_ROOT)
-        node.account.mkdirs(self.TEMP_DIR)
-        node.account.mkdirs(self.LOGS_DIR)
-
-
-class IgnitePersistenceAware(PersistenceAware):
-    """
-    This class contains Ignite persistence artifacts
-    """
-    WORK_DIR = os.path.join(PersistenceAware.PERSISTENT_ROOT, "work")
-    CONFIG_FILE = os.path.join(PersistenceAware.PERSISTENT_ROOT, "ignite-config.xml")
-    LOG4J_CONFIG_FILE = os.path.join(PersistenceAware.PERSISTENT_ROOT, "ignite-log4j.xml")
-
-    def __getattribute__(self, item):
-        if item == 'logs':
-            return PersistenceAware.logs
-
-        return super().__getattribute__(item)
-
-    def init_persistent(self, node):
-        """
-        Init persistent directory.
-        :param node: Ignite service node.
-        """
-        super().init_persistent(node)
-
-        logger_config = IgniteLoggerConfigTemplate().render(work_dir=self.WORK_DIR)
-        node.account.create_file(self.LOG4J_CONFIG_FILE, logger_config)
diff --git a/modules/ducktests/tests/ignitetest/services/utils/ignite_spec.py b/modules/ducktests/tests/ignitetest/services/utils/ignite_spec.py
index 6b9ba9f..489c887 100644
--- a/modules/ducktests/tests/ignitetest/services/utils/ignite_spec.py
+++ b/modules/ducktests/tests/ignitetest/services/utils/ignite_spec.py
@@ -20,11 +20,11 @@ This module contains Spec classes that describes config and command line to star
 import base64
 import importlib
 import json
+import os
 from abc import ABCMeta, abstractmethod
 
 from ignitetest.services.utils.config_template import IgniteClientConfigTemplate, IgniteServerConfigTemplate
-from ignitetest.services.utils.ignite_path import IgnitePath
-from ignitetest.services.utils.ignite_persistence import IgnitePersistenceAware
+from ignitetest.services.utils.path import get_home_dir, get_module_path
 from ignitetest.utils.version import DEV_BRANCH
 
 
@@ -46,10 +46,11 @@ def resolve_spec(service, context, config, **kwargs):
         return len(impl_filter) > 0
 
     if is_impl("IgniteService"):
-        return _resolve_spec("NodeSpec", ApacheIgniteNodeSpec)(config=config, **kwargs)
+        return _resolve_spec("NodeSpec", ApacheIgniteNodeSpec)(path_aware=service, config=config, **kwargs)
 
     if is_impl("IgniteApplicationService"):
-        return _resolve_spec("AppSpec", ApacheIgniteApplicationSpec)(context=context, config=config, **kwargs)
+        return _resolve_spec("AppSpec", ApacheIgniteApplicationSpec)(path_aware=service, context=context,
+                                                                     config=config, **kwargs)
 
     raise Exception("There is no specification for class %s" % type(service))
 
@@ -58,12 +59,13 @@ class IgniteSpec(metaclass=ABCMeta):
     """
     This class is a basic Spec
     """
-    def __init__(self, config, project, jvm_opts):
-        self.version = config.version
-        self.path = IgnitePath(self.version, project)
+    def __init__(self, path_aware, config, project, jvm_opts):
+        self.project = project
+        self.path_aware = path_aware
         self.envs = {}
         self.jvm_opts = jvm_opts or []
         self.config = config
+        self.version = config.version
 
     @property
     def config_template(self):
@@ -74,8 +76,22 @@ class IgniteSpec(metaclass=ABCMeta):
             return IgniteClientConfigTemplate()
         return IgniteServerConfigTemplate()
 
+    def __home(self, version=None):
+        """
+        Get home directory for current spec.
+        """
+        version = version if version else self.version
+        return get_home_dir(self.path_aware.install_root, self.project, version)
+
+    def _module(self, name):
+        """
+        Get module path for current spec.
+        """
+        version = DEV_BRANCH if name == "ducktests" else self.version
+        return get_module_path(self.__home(version), name, version)
+
     @abstractmethod
-    def command(self):
+    def command(self, node):
         """
         :return: string that represents command to run service on a node
         """
@@ -95,23 +111,22 @@ class IgniteSpec(metaclass=ABCMeta):
         return " ".join(opts)
 
 
-class IgniteNodeSpec(IgniteSpec, IgnitePersistenceAware):
+class IgniteNodeSpec(IgniteSpec):
     """
     Spec to run ignite node
     """
-    # pylint: disable=W0221
-    def command(self, stdout_stderr):
+    def command(self, node):
         cmd = "%s %s %s %s 2>&1 | tee -a %s &" % \
               (self._envs(),
-               self.path.script("ignite.sh"),
+               self.path_aware.script("ignite.sh"),
                self._jvm_opts(),
-               self.CONFIG_FILE,
-               stdout_stderr)
+               self.path_aware.config_file,
+               node.log_file)
 
         return cmd
 
 
-class IgniteApplicationSpec(IgniteSpec, IgnitePersistenceAware):
+class IgniteApplicationSpec(IgniteSpec):
     """
     Spec to run ignite application
     """
@@ -123,18 +138,18 @@ class IgniteApplicationSpec(IgniteSpec, IgnitePersistenceAware):
         return ",".join(self.args)
 
     # pylint: disable=W0221
-    def command(self, stdout_stderr):
+    def command(self, node):
         cmd = "%s %s %s %s 2>&1 | tee -a %s &" % \
               (self._envs(),
-               self.path.script("ignite.sh"),
+               self.path_aware.script("ignite.sh"),
                self._jvm_opts(),
                self._app_args(),
-               stdout_stderr)
+               node.log_file)
 
         return cmd
 
 
-class ApacheIgniteNodeSpec(IgniteNodeSpec, IgnitePersistenceAware):
+class ApacheIgniteNodeSpec(IgniteNodeSpec):
     """
     Implementation IgniteNodeSpec for Apache Ignite project
     """
@@ -143,24 +158,24 @@ class ApacheIgniteNodeSpec(IgniteNodeSpec, IgnitePersistenceAware):
 
         libs = (modules or [])
         libs.append("log4j")
-        libs = list(map(lambda m: self.path.module(m) + "/*", libs))
+        libs = list(map(lambda m: os.path.join(self._module(m), "*"), libs))
 
-        libs.append(IgnitePath(DEV_BRANCH).module("ducktests") + "/*")
+        libs.append(os.path.join(self._module("ducktests"), "*"))
 
         self.envs = {
             'EXCLUDE_TEST_CLASSES': 'true',
-            'IGNITE_LOG_DIR': self.PERSISTENT_ROOT,
+            'IGNITE_LOG_DIR': self.path_aware.persistent_root,
             'USER_LIBS': ":".join(libs)
         }
 
         self.jvm_opts.extend([
-            "-DIGNITE_SUCCESS_FILE=" + self.PERSISTENT_ROOT + "/success_file",
-            "-Dlog4j.configuration=file:" + self.LOG4J_CONFIG_FILE,
+            "-DIGNITE_SUCCESS_FILE=" + os.path.join(self.path_aware.persistent_root, "success_file"),
+            "-Dlog4j.configuration=file:" + self.path_aware.log_config_file,
             "-Dlog4j.configDebug=true"
         ])
 
 
-class ApacheIgniteApplicationSpec(IgniteApplicationSpec, IgnitePersistenceAware):
+class ApacheIgniteApplicationSpec(IgniteApplicationSpec):
     """
     Implementation IgniteApplicationSpec for Apache Ignite project
     """
@@ -172,20 +187,20 @@ class ApacheIgniteApplicationSpec(IgniteApplicationSpec, IgnitePersistenceAware)
         libs = modules or []
         libs.extend(["log4j"])
 
-        libs = [self.path.module(m) + "/*" for m in libs]
-        libs.append(IgnitePath(DEV_BRANCH).module("ducktests") + "/*")
+        libs = list(map(lambda m: os.path.join(self._module(m), "*"), libs))
+        libs.append(os.path.join(self._module("ducktests"), "*"))
         libs.extend(self.__jackson())
 
         self.envs = {
             "MAIN_CLASS": servicejava_class_name,
             "EXCLUDE_TEST_CLASSES": "true",
-            "IGNITE_LOG_DIR": self.PERSISTENT_ROOT,
+            "IGNITE_LOG_DIR": self.path_aware.persistent_root,
             "USER_LIBS": ":".join(libs)
         }
 
         self.jvm_opts.extend([
-            "-DIGNITE_SUCCESS_FILE=" + self.PERSISTENT_ROOT + "/success_file",
-            "-Dlog4j.configuration=file:" + self.LOG4J_CONFIG_FILE,
+            "-DIGNITE_SUCCESS_FILE=" + os.path.join(self.path_aware.persistent_root, "success_file"),
+            "-Dlog4j.configuration=file:" + self.path_aware.log_config_file,
             "-Dlog4j.configDebug=true",
             "-DIGNITE_NO_SHUTDOWN_HOOK=true",  # allows to perform operations on app termination.
             "-Xmx1G",
@@ -196,13 +211,14 @@ class ApacheIgniteApplicationSpec(IgniteApplicationSpec, IgnitePersistenceAware)
         self.args = [
             str(start_ignite),
             java_class_name,
-            self.CONFIG_FILE,
+            self.path_aware.config_file,
             str(base64.b64encode(json.dumps(params).encode('utf-8')), 'utf-8')
         ]
 
     def __jackson(self):
-        if not self.version.is_dev:
-            aws = self.path.module("aws")
+        version = self.version
+        if not version.is_dev:
+            aws = self._module("aws")
             return self.context.cluster.nodes[0].account.ssh_capture(
                 "ls -d %s/* | grep jackson | tr '\n' ':' | sed 's/.$//'" % aws)
 
diff --git a/modules/ducktests/tests/ignitetest/services/utils/jmx_utils.py b/modules/ducktests/tests/ignitetest/services/utils/jmx_utils.py
index 925034b..a14d96d 100644
--- a/modules/ducktests/tests/ignitetest/services/utils/jmx_utils.py
+++ b/modules/ducktests/tests/ignitetest/services/utils/jmx_utils.py
@@ -17,19 +17,20 @@
 This module contains JMX Console client and different utilities and mixins to retrieve ignite node parameters
 and attributes.
 """
-
+import os
 import re
 
 from ignitetest.services.utils.decorators import memoize
 
 
-def ignite_jmx_mixin(node, pids):
+def ignite_jmx_mixin(node, spec, pids):
     """
     Dynamically mixin JMX attributes to Ignite service node.
     :param node: Ignite service node.
     :param pids: Ignite service node pids.
     """
     setattr(node, 'pids', pids)
+    setattr(node, 'spec', spec)
     base_cls = node.__class__
     base_cls_name = node.__class__.__name__
     node.__class__ = type(base_cls_name, (base_cls, IgniteJmxMixin), {})
@@ -55,12 +56,18 @@ class JmxMBean:
 class JmxClient:
     """JMX client, invokes jmxterm on node locally.
     """
-    jmx_util_cmd = 'java -jar /opt/jmxterm.jar -v silent -n'
-
     def __init__(self, node):
         self.node = node
+        self.install_root = node.spec.path_aware.install_root
         self.pid = node.pids[0]
 
+    @property
+    def jmx_util_cmd(self):
+        """
+        :return: jmxterm prepared command line invocation.
+        """
+        return os.path.join(f"java -jar {self.install_root}/jmxterm.jar -v silent -n")
+
     @memoize
     def find_mbean(self, pattern, domain='org.apache'):
         """
diff --git a/modules/ducktests/tests/ignitetest/services/utils/path.py b/modules/ducktests/tests/ignitetest/services/utils/path.py
new file mode 100644
index 0000000..9cab69d
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/path.py
@@ -0,0 +1,173 @@
+# 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 module contains classes that represent persistent artifacts of tests
+"""
+
+import os
+from abc import abstractmethod, ABCMeta
+
+from ignitetest.services.utils.config_template import IgniteLoggerConfigTemplate
+
+
+def get_home_dir(install_root, project, version):
+    """
+    Get path to binary release (home) directory depending on version.
+    """
+    return os.path.join(install_root, f"{project}-{version}")
+
+
+def get_module_path(project_dir, module_name, version):
+    """
+    Get absolute path to the specified module.
+    """
+    if version.is_dev:
+        module_path = os.path.join("modules", module_name, "target")
+    else:
+        module_path = os.path.join("libs", "optional", "ignite-%s" % module_name)
+
+    return os.path.join(project_dir, module_path)
+
+
+class PathAware:
+    """
+    Basic class for path configs.
+    """
+    def init_persistent(self, node):
+        """
+        Init persistent directory.
+        :param node: Service node.
+        """
+        node.account.mkdirs(f"{self.persistent_root} {self.temp_dir} {self.work_dir} {self.log_dir}")
+
+    def init_logs_attribute(self):
+        """
+        Initialize logs attribute for collecting logs by ducktape.
+        After changing to property based logs, will be removed.
+        """
+        setattr(self, 'logs', {
+            "logs": {
+                "path": self.log_dir,
+                "collect_default": True
+            }
+        })
+
+    @property
+    @abstractmethod
+    def config_file(self):
+        """
+        :return: path to project configuration file
+        """
+
+    @property
+    @abstractmethod
+    def log_config_file(self):
+        """
+        :return: path to logger configuration file
+        """
+
+    @property
+    def work_dir(self):
+        """
+        :return: path to work directory
+        """
+        return os.path.join(self.persistent_root, "work")
+
+    @property
+    def log_dir(self):
+        """
+        :return: path to log directory
+        """
+        return os.path.join(self.persistent_root, "logs")
+
+    @property
+    @abstractmethod
+    def project(self):
+        """
+        :return: project name, for example 'zookeeper' for Apache Zookeeper.
+        """
+
+    @property
+    @abstractmethod
+    def version(self):
+        """
+        :return: version of project.
+        """
+
+    @property
+    @abstractmethod
+    def globals(self):
+        """
+        :return: dictionary of globals variable (usually from test context).
+        """
+
+    @property
+    def home_dir(self):
+        """
+        :return: path to binary release (home) directory
+        """
+        return get_home_dir(self.install_root, self.project, self.version)
+
+    @property
+    def temp_dir(self):
+        """
+        :return: path to temp directory
+        """
+        return os.path.join(self.persistent_root, "tmp")
+
+    @property
+    def persistent_root(self):
+        """
+        :return: path to persistent root
+        """
+        return self.globals.get("persistent_root", "/mnt/service")
+
+    @property
+    def install_root(self):
+        """
+        :return: path to distributive installation root
+        """
+        return self.globals.get("install_root", "/opt")
+
+
+class IgnitePathAware(PathAware, metaclass=ABCMeta):
+    """
+    This class contains Ignite path configs.
+    """
+    def init_persistent(self, node):
+        """
+        Init persistent directory.
+        :param node: Ignite service node.
+        """
+        super().init_persistent(node)
+
+        logger_config = IgniteLoggerConfigTemplate().render(work_dir=self.work_dir)
+        node.account.create_file(self.log_config_file, logger_config)
+
+    @property
+    def config_file(self):
+        return os.path.join(self.persistent_root, "ignite-config.xml")
+
+    @property
+    def log_config_file(self):
+        return os.path.join(self.persistent_root, "ignite-log4j.xml")
+
+    def script(self, script_name):
+        """
+        :param script_name: name of Ignite script
+        :return: absolute path to the specified script
+        """
+        return os.path.join(self.home_dir, "bin", script_name)
diff --git a/modules/ducktests/tests/ignitetest/services/zk/templates/log4j.properties.j2 b/modules/ducktests/tests/ignitetest/services/zk/templates/log4j.properties.j2
index 507ec98..1f51c94 100644
--- a/modules/ducktests/tests/ignitetest/services/zk/templates/log4j.properties.j2
+++ b/modules/ducktests/tests/ignitetest/services/zk/templates/log4j.properties.j2
@@ -18,8 +18,8 @@
 zookeeper.root.logger=INFO, FILE
 zookeeper.console.threshold=INFO
 
-zookeeper.log.dir={{ PERSISTENT_ROOT }}
-zookeeper.log.file=zookeeper.log
+zookeeper.log.dir={{ log_dir }}
+zookeeper.log.file={{ LOG_FILENAME }}
 zookeeper.log.threshold=INFO
 zookeeper.log.maxfilesize=256MB
 zookeeper.log.maxbackupindex=20
diff --git a/modules/ducktests/tests/ignitetest/services/zk/templates/zookeeper.properties.j2 b/modules/ducktests/tests/ignitetest/services/zk/templates/zookeeper.properties.j2
index 2ea8c3d..8a4e7b8 100644
--- a/modules/ducktests/tests/ignitetest/services/zk/templates/zookeeper.properties.j2
+++ b/modules/ducktests/tests/ignitetest/services/zk/templates/zookeeper.properties.j2
@@ -19,7 +19,7 @@ tickTime={{ settings.tick_time }}
 minSessionTimeout={{ settings.min_session_timeout }}
 initLimit={{ settings.init_limit }}
 syncLimit={{ settings.sync_limit }}
-dataDir={{ DATA_DIR }}
+dataDir={{ data_dir }}
 clientPort={{ settings.client_port }}
 {% for node in nodes %}
 server.{{ loop.index }}={{ node.account.hostname }}:2888:3888
diff --git a/modules/ducktests/tests/ignitetest/services/zk/zookeeper.py b/modules/ducktests/tests/ignitetest/services/zk/zookeeper.py
index baf8848..b76ea99 100644
--- a/modules/ducktests/tests/ignitetest/services/zk/zookeeper.py
+++ b/modules/ducktests/tests/ignitetest/services/zk/zookeeper.py
@@ -18,11 +18,13 @@ This module contains classes and utilities to start zookeeper cluster for testin
 """
 
 import os.path
+from distutils.version import LooseVersion
 
 from ducktape.services.service import Service
 from ducktape.utils.util import wait_until
 
 from ignitetest.services.utils.log_utils import monitor_log
+from ignitetest.services.utils.path import PathAware
 
 
 class ZookeeperSettings:
@@ -36,32 +38,48 @@ class ZookeeperSettings:
         self.sync_limit = kwargs.get('sync_limit', 5)
         self.client_port = kwargs.get('client_port', 2181)
 
+        version = kwargs.get("version")
+        if version:
+            if isinstance(version, str):
+                version = LooseVersion(version)
+            self.version = version
+        else:
+            self.version = LooseVersion("3.5.8")
+
         assert self.tick_time <= self.min_session_timeout // 2, "'tick_time' must be <= 'min_session_timeout' / 2"
 
 
-class ZookeeperService(Service):
+class ZookeeperService(Service, PathAware):
     """
     Zookeeper service.
     """
-    PERSISTENT_ROOT = "/mnt/zookeeper"
-    CONFIG_ROOT = os.path.join(PERSISTENT_ROOT, "conf")
-    LOG_FILE = os.path.join(PERSISTENT_ROOT, "zookeeper.log")
-    DATA_DIR = os.path.join(PERSISTENT_ROOT, "data")
-    CONFIG_FILE = os.path.join(CONFIG_ROOT, "zookeeper.properties")
-    LOG_CONFIG_FILE = os.path.join(CONFIG_ROOT, "log4j.properties")
-    ZK_LIB_DIR = "/opt/zookeeper-3.5.8/lib"
-
-    logs = {
-        "zk_log": {
-            "path": LOG_FILE,
-            "collect_default": True
-        }
-    }
+    LOG_FILENAME = "zookeeper.log"
 
     def __init__(self, context, num_nodes, settings=ZookeeperSettings(), start_timeout_sec=60):
         super().__init__(context, num_nodes)
         self.settings = settings
         self.start_timeout_sec = start_timeout_sec
+        self.init_logs_attribute()
+
+    @property
+    def version(self):
+        return self.settings.version
+
+    @property
+    def globals(self):
+        return self.context.globals
+
+    @property
+    def log_config_file(self):
+        return os.path.join(self.persistent_root, "log4j.properties")
+
+    @property
+    def config_file(self):
+        return os.path.join(self.persistent_root, "zookeeper.properties")
+
+    @property
+    def project(self):
+        return "zookeeper"
 
     def start(self, clean=True):
         super().start(clean=clean)
@@ -77,19 +95,18 @@ class ZookeeperService(Service):
 
         self.logger.info("Starting Zookeeper node %d on %s", idx, node.account.hostname)
 
-        node.account.ssh("mkdir -p %s" % self.DATA_DIR)
-        node.account.ssh("mkdir -p %s" % self.CONFIG_ROOT)
-        node.account.ssh("echo %d > %s/myid" % (idx, self.DATA_DIR))
+        self.init_persistent(node)
+        node.account.ssh(f"echo {idx} > {self.work_dir}/myid")
 
-        config_file = self.render('zookeeper.properties.j2', settings=self.settings)
-        node.account.create_file(self.CONFIG_FILE, config_file)
+        config_file = self.render('zookeeper.properties.j2', settings=self.settings, data_dir=self.work_dir)
+        node.account.create_file(self.config_file, config_file)
         self.logger.info("ZK config %s", config_file)
 
-        log_config_file = self.render('log4j.properties.j2')
-        node.account.create_file(self.LOG_CONFIG_FILE, log_config_file)
+        log_config_file = self.render('log4j.properties.j2', log_dir=self.log_dir)
+        node.account.create_file(self.log_config_file, log_config_file)
 
-        start_cmd = "nohup java -cp %s/*:%s org.apache.zookeeper.server.quorum.QuorumPeerMain %s >/dev/null 2>&1 &" % \
-                    (self.ZK_LIB_DIR, self.CONFIG_ROOT, self.CONFIG_FILE)
+        start_cmd = f"nohup java -cp {os.path.join(self.home_dir, 'lib')}/*:{self.persistent_root} " \
+                    f"org.apache.zookeeper.server.quorum.QuorumPeerMain {self.config_file} >/dev/null 2>&1 &"
 
         node.account.ssh(start_cmd)
 
@@ -104,13 +121,20 @@ class ZookeeperService(Service):
         :param node:  Zookeeper service node.
         :param timeout: Wait timeout.
         """
-        with monitor_log(node, self.LOG_FILE, from_the_beginning=True) as monitor:
+        with monitor_log(node, self.log_file, from_the_beginning=True) as monitor:
             monitor.wait_until(
                 "LEADER ELECTION TOOK",
                 timeout_sec=timeout,
-                err_msg="Zookeeper quorum was not formed on %s" % node.account.hostname
+                err_msg=f"Zookeeper quorum was not formed on {node.account.hostname}"
             )
 
+    @property
+    def log_file(self):
+        """
+        :return: current log file of node.
+        """
+        return os.path.join(self.log_dir, self.LOG_FILENAME)
+
     @staticmethod
     def java_class_name():
         """ The class name of the Zookeeper quorum peers. """
@@ -150,7 +174,7 @@ class ZookeeperService(Service):
             self.logger.warn("%s %s was still alive at cleanup time. Killing forcefully..." %
                              (self.__class__.__name__, node.account))
         node.account.kill_process("zookeeper", clean_shutdown=False, allow_fail=True)
-        node.account.ssh("rm -rf %s %s %s" % (self.CONFIG_ROOT, self.DATA_DIR, self.LOG_FILE), allow_fail=False)
+        node.account.ssh(f"rm -rf -- {self.persistent_root}", allow_fail=False)
 
     def kill(self):
         """
diff --git a/modules/ducktests/tests/ignitetest/tests/self_test.py b/modules/ducktests/tests/ignitetest/tests/self_test.py
index e5f91e9..1d6e6ea 100644
--- a/modules/ducktests/tests/ignitetest/tests/self_test.py
+++ b/modules/ducktests/tests/ignitetest/tests/self_test.py
@@ -16,6 +16,7 @@
 """
 This module contains smoke tests that checks that ducktape works as expected
 """
+import os
 
 from ignitetest.services.ignite import IgniteService
 from ignitetest.services.ignite_app import IgniteApplicationService
@@ -93,3 +94,41 @@ class SelfTest(IgniteTest):
         client.stop()
 
         ignites.stop()
+
+    @cluster(num_nodes=1)
+    @ignite_versions(str(DEV_BRANCH))
+    def test_logs_rotation(self, ignite_version):
+        """
+        Test logs rotation after ignite service restart.
+        """
+        def get_log_lines_count(service, filename):
+            node = service.nodes[0]
+            log_file = os.path.join(service.log_dir, filename)
+            log_cnt = list(node.account.ssh_capture(f'cat {log_file} | wc -l', callback=int))[0]
+            return log_cnt
+
+        def get_logs_count(service):
+            node = service.nodes[0]
+            return list(node.account.ssh_capture(f'ls {service.log_dir} | wc -l', callback=int))[0]
+
+        ignites = IgniteService(self.test_context, IgniteConfiguration(version=IgniteVersion(ignite_version)),
+                                num_nodes=1)
+
+        ignites.start()
+
+        num_restarts = 6
+        for i in range(num_restarts - 1):
+            ignites.stop()
+
+            old_cnt = get_log_lines_count(ignites, "console.log")
+            assert old_cnt > 0
+
+            ignites.start(clean=False)
+
+            new_cnt = get_log_lines_count(ignites, "console.log")
+            assert new_cnt > 0
+
+            # check that there is no new entry in rotated file
+            assert old_cnt == get_log_lines_count(ignites, f"console.log.{i + 1}")
+
+        assert get_logs_count(ignites) == num_restarts
diff --git a/modules/ducktests/tests/tox.ini b/modules/ducktests/tests/tox.ini
index f74d685..02b773b 100644
--- a/modules/ducktests/tests/tox.ini
+++ b/modules/ducktests/tests/tox.ini
@@ -26,7 +26,6 @@ python =
 envdir = {homedir}/.virtualenvs/ignite-ducktests-{envname}
 deps =
     -r ./docker/requirements-dev.txt
-recreate = True
 usedevelop = True
 commands =
     pytest {env:PYTESTARGS:} {posargs}