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 36ed2c3 Ignite ducktape control sh (#8127) 36ed2c3 is described below commit 36ed2c381811cc05d568cbfe49baf33087568539 Author: Ivan Daschinskiy <ivanda...@gmail.com> AuthorDate: Mon Aug 10 16:38:56 2020 +0300 Ignite ducktape control sh (#8127) --- .gitignore | 1 + .travis.yml | 8 + bin/control.sh | 13 +- bin/include/build-classpath.sh | 7 +- modules/ducktests/tests/check_style.sh | 26 --- .../ignitetest/services/utils/config/ignite.xml.j2 | 4 +- .../ignitetest/services/utils/control_utility.py | 167 +++++++++++++++ .../ignitetest/services/utils/ignite_aware.py | 21 +- .../tests/ignitetest/tests/control_utility_test.py | 235 +++++++++++++++++++++ .../tests/ignitetest/tests/pme_free_switch_test.py | 4 +- modules/ducktests/tests/{.pylintrc => tox.ini} | 23 ++ 11 files changed, 461 insertions(+), 48 deletions(-) diff --git a/.gitignore b/.gitignore index 1a8b46a..151ad01 100644 --- a/.gitignore +++ b/.gitignore @@ -87,3 +87,4 @@ packages *.pyc /tests/venv modules/ducktests/tests/docker/build/** +modules/ducktests/tests/.tox diff --git a/.travis.yml b/.travis.yml index 73117e8..afb930f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -50,3 +50,11 @@ matrix: dotnet: 3.1.101 script: - dotnet build modules/platforms/dotnet/Apache.Ignite.DotNetCore.sln + + - language: python + python: + - "3.8" + install: pip install tox-travis + before_script: cd modules/ducktests/tests + script: + - tox diff --git a/bin/control.sh b/bin/control.sh index 5d615d4..4493c6c 100755 --- a/bin/control.sh +++ b/bin/control.sh @@ -1,12 +1,9 @@ #!/usr/bin/env bash -if [ ! -z "${IGNITE_SCRIPT_STRICT_MODE:-}" ] -then - set -o nounset - set -o errexit - set -o pipefail - set -o errtrace - set -o functrace -fi +set -o nounset +set -o errexit +set -o pipefail +set -o errtrace +set -o functrace # # Licensed to the Apache Software Foundation (ASF) under one or more diff --git a/bin/include/build-classpath.sh b/bin/include/build-classpath.sh index 9a43683..0625f41 100644 --- a/bin/include/build-classpath.sh +++ b/bin/include/build-classpath.sh @@ -47,14 +47,13 @@ includeToClassPath() { for file in $1/* do - if [[ -z "${EXCLUDE_MODULES}" ]] || [[ ${EXCLUDE_MODULES} != *"`basename $file`"* ]]; then - echo "$file included" + if [[ -z "${EXCLUDE_MODULES:-}" ]] || [[ ${EXCLUDE_MODULES:-} != *"`basename $file`"* ]]; then if [ -d ${file} ] && [ -d "${file}/target" ]; then if [ -d "${file}/target/classes" ]; then IGNITE_LIBS=${IGNITE_LIBS}${SEP}${file}/target/classes fi - if [[ -z "${EXCLUDE_TEST_CLASSES}" ]]; then + if [[ -z "${EXCLUDE_TEST_CLASSES:-}" ]]; then if [ -d "${file}/target/test-classes" ]; then IGNITE_LIBS=${IGNITE_LIBS}${SEP}${file}/target/test-classes fi @@ -68,7 +67,7 @@ includeToClassPath() { echo "$file excluded by EXCLUDE_MODULES settings" fi done - + IFS=$SAVEIFS } diff --git a/modules/ducktests/tests/check_style.sh b/modules/ducktests/tests/check_style.sh deleted file mode 100755 index 39405a7..0000000 --- a/modules/ducktests/tests/check_style.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash - -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -if ! command -v pylint &> /dev/null -then - echo "Please, install pylint first" - exit 1 -fi - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -pylint --rcfile="$SCRIPT_DIR"/.pylintrc "$SCRIPT_DIR"/ignitetest diff --git a/modules/ducktests/tests/ignitetest/services/utils/config/ignite.xml.j2 b/modules/ducktests/tests/ignitetest/services/utils/config/ignite.xml.j2 index 5ceb5b6..50d2c5c 100644 --- a/modules/ducktests/tests/ignitetest/services/utils/config/ignite.xml.j2 +++ b/modules/ducktests/tests/ignitetest/services/utils/config/ignite.xml.j2 @@ -30,7 +30,9 @@ </property> <property name="clientMode" value="{{ client_mode or False | lower }}"/> - + {% if consistent_id %} + <property name="consistentId" value="{{ consistent_id }}"/> + {% endif %} {{ properties }} </bean> </beans> diff --git a/modules/ducktests/tests/ignitetest/services/utils/control_utility.py b/modules/ducktests/tests/ignitetest/services/utils/control_utility.py new file mode 100644 index 0000000..36ff5bb --- /dev/null +++ b/modules/ducktests/tests/ignitetest/services/utils/control_utility.py @@ -0,0 +1,167 @@ +# 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 control utility wrapper. +""" +import random +import re +from collections import namedtuple + +from ducktape.cluster.remoteaccount import RemoteCommandError + + +class ControlUtility: + """ + Control utility (control.sh) wrapper. + """ + BASE_COMMAND = "control.sh" + + def __init__(self, cluster, text_context): + self._cluster = cluster + self.logger = text_context.logger + + def baseline(self): + """ + :return Baseline nodes. + """ + return self.cluster_state().baseline + + def cluster_state(self): + """ + :return: Cluster state. + """ + output = self.__run("--baseline") + + return self.__parse_cluster_state(output) + + def set_baseline(self, baseline): + """ + :param baseline: Baseline nodes or topology version to set as baseline. + """ + if isinstance(baseline, int): + result = self.__run("--baseline version %d --yes" % baseline) + else: + result = self.__run("--baseline set %s --yes" % + ",".join([node.account.externally_routable_ip for node in baseline])) + + return self.__parse_cluster_state(result) + + def add_to_baseline(self, nodes): + """ + :param nodes: Nodes that should be added to baseline. + """ + result = self.__run("--baseline add %s --yes" % + ",".join([node.account.externally_routable_ip for node in nodes])) + + return self.__parse_cluster_state(result) + + def remove_from_baseline(self, nodes): + """ + :param nodes: Nodes that should be removed to baseline. + """ + result = self.__run("--baseline remove %s --yes" % + ",".join([node.account.externally_routable_ip for node in nodes])) + + return self.__parse_cluster_state(result) + + def disable_baseline_auto_adjust(self): + """ + Disable baseline auto adjust. + """ + return self.__run("--baseline auto_adjust disable --yes") + + def enable_baseline_auto_adjust(self, timeout=None): + """ + Enable baseline auto adjust. + :param timeout: Auto adjust timeout in millis. + """ + timeout_str = "timeout %d" % timeout if timeout else "" + return self.__run("--baseline auto_adjust enable %s --yes" % timeout_str) + + def activate(self): + """ + Activate cluster. + """ + return self.__run("--activate --yes") + + def deactivate(self): + """ + Deactivate cluster. + """ + return self.__run("--deactivate --yes") + + @staticmethod + def __parse_cluster_state(output): + state_pattern = re.compile("Cluster state: ([^\\s]+)") + topology_pattern = re.compile("Current topology version: (\\d+)") + baseline_pattern = re.compile("Consistent(Id|ID)=([^\\s]+),\\sS(tate|TATE)=([^\\s]+),?(\\sOrder=(\\d+))?") + + match = state_pattern.search(output) + state = match.group(1) if match else None + + match = topology_pattern.search(output) + topology = int(match.group(1)) if match else None + + baseline = [BaselineNode(consistent_id=m[1], state=m[3], order=int(m[5]) if m[5] else None) + for m in baseline_pattern.findall(output)] + + return ClusterState(state=state, topology_version=topology, baseline=baseline) + + def __run(self, cmd): + node = random.choice(self.__alives()) + + self.logger.debug("Run command %s on node %s", cmd, node.name) + + raw_output = node.account.ssh_capture(self.__form_cmd(node, cmd), allow_fail=True) + code, output = self.__parse_output(raw_output) + + self.logger.debug("Output of command %s on node %s, exited with code %d, is %s", cmd, node.name, code, output) + + if code != 0: + raise ControlUtilityError(node.account, cmd, code, output) + + return output + + def __form_cmd(self, node, cmd): + return self._cluster.path.script("%s --host %s %s" % (self.BASE_COMMAND, node.account.externally_routable_ip, + cmd)) + + @staticmethod + def __parse_output(raw_output): + exit_code = raw_output.channel_file.channel.recv_exit_status() + output = "".join(raw_output) + + pattern = re.compile("Command \\[[^\\s]*\\] finished with code: (\\d+)") + match = pattern.search(output) + + if match: + return int(match.group(1)), output + return exit_code, output + + def __alives(self): + return [node for node in self._cluster.nodes if self._cluster.alive(node)] + + +BaselineNode = namedtuple("BaselineNode", ["consistent_id", "state", "order"]) +ClusterState = namedtuple("ClusterState", ["state", "topology_version", "baseline"]) + + +class ControlUtilityError(RemoteCommandError): + """ + Error is raised when control utility failed. + """ + def __init__(self, account, cmd, exit_status, output): + super(ControlUtilityError, self).__init__(account, cmd, exit_status, "".join(output)) diff --git a/modules/ducktests/tests/ignitetest/services/utils/ignite_aware.py b/modules/ducktests/tests/ignitetest/services/utils/ignite_aware.py index 7fe97a9..2e292a3 100644 --- a/modules/ducktests/tests/ignitetest/services/utils/ignite_aware.py +++ b/modules/ducktests/tests/ignitetest/services/utils/ignite_aware.py @@ -18,10 +18,11 @@ This module contains the base class to build services aware of Ignite. """ import os -from abc import abstractmethod +from abc import abstractmethod, ABCMeta from ducktape.services.background_thread import BackgroundThreadService from ducktape.utils.util import wait_until +from six import add_metaclass from ignitetest.services.utils.ignite_config import IgniteLoggerConfig, IgniteServerConfig, IgniteClientConfig from ignitetest.services.utils.ignite_path import IgnitePath @@ -29,6 +30,7 @@ from ignitetest.services.utils.jmx_utils import ignite_jmx_mixin from ignitetest.tests.utils.version import IgniteVersion +@add_metaclass(ABCMeta) class IgniteAwareService(BackgroundThreadService): """ The base class to build services aware of Ignite. @@ -85,8 +87,14 @@ class IgniteAwareService(BackgroundThreadService): logger_config = IgniteLoggerConfig().render(work_dir=self.WORK_DIR) node.account.mkdirs(self.PERSISTENT_ROOT) - node.account.create_file(self.CONFIG_FILE, self.config().render( - config_dir=self.PERSISTENT_ROOT, work_dir=self.WORK_DIR, properties=self.properties)) + + node_config = self.config().render(config_dir=self.PERSISTENT_ROOT, + work_dir=self.WORK_DIR, + properties=self.properties, + consistent_id=node.account.externally_routable_ip) + + setattr(node, "consistent_id", node.account.externally_routable_ip) + node.account.create_file(self.CONFIG_FILE, node_config) node.account.create_file(self.LOG4J_CONFIG_FILE, logger_config) @abstractmethod @@ -156,10 +164,9 @@ class IgniteAwareService(BackgroundThreadService): :param backoff_sec: Number of seconds to back off between each failure to meet the condition before checking again. """ - assert len(self.nodes) == 1 - - self.await_event_on_node(evt_message, self.nodes[0], timeout_sec, from_the_beginning=from_the_beginning, - backoff_sec=backoff_sec) + for node in self.nodes: + self.await_event_on_node(evt_message, node, timeout_sec, from_the_beginning=from_the_beginning, + backoff_sec=backoff_sec) def execute(self, command): """ diff --git a/modules/ducktests/tests/ignitetest/tests/control_utility_test.py b/modules/ducktests/tests/ignitetest/tests/control_utility_test.py new file mode 100644 index 0000000..027cc3c --- /dev/null +++ b/modules/ducktests/tests/ignitetest/tests/control_utility_test.py @@ -0,0 +1,235 @@ +# 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 control.sh utility tests. +""" +from ducktape.mark import parametrize +from ducktape.mark.resource import cluster +from ducktape.utils.util import wait_until +from jinja2 import Template + +from ignitetest.services.ignite import IgniteService +from ignitetest.services.utils.control_utility import ControlUtility, ControlUtilityError +from ignitetest.tests.utils.ignite_test import IgniteTest +from ignitetest.tests.utils.version import DEV_BRANCH, LATEST_2_8, IgniteVersion, LATEST_2_7, V_2_8_0 + + +# pylint: disable=W0223 +class BaselineTests(IgniteTest): + """ + Tests baseline command + """ + NUM_NODES = 3 + + CONFIG_TEMPLATE = """ + {% if version > "2.9.0" %} + <property name="clusterStateOnStart" value="INACTIVE"/> + {% else %} + <property name="activeOnStart" value="false"/> + {% endif %} + <property name="dataStorageConfiguration"> + <bean class="org.apache.ignite.configuration.DataStorageConfiguration"> + <property name="defaultDataRegionConfiguration"> + <bean class="org.apache.ignite.configuration.DataRegionConfiguration"> + <property name="persistenceEnabled" value="true"/> + <property name="maxSize" value="#{100L * 1024 * 1024}"/> + </bean> + </property> + </bean> + </property> + """ + + @staticmethod + def properties(version): + """ + Render properties for ignite node configuration. + """ + return Template(BaselineTests.CONFIG_TEMPLATE) \ + .render(version=version) + + def __init__(self, test_context): + super(BaselineTests, self).__init__(test_context) + self.servers = None + + @cluster(num_nodes=NUM_NODES) + @parametrize(version=str(DEV_BRANCH)) + @parametrize(version=str(LATEST_2_8)) + @parametrize(version=str(LATEST_2_7)) + def test_baseline_set(self, version): + """ + Test baseline set. + """ + blt_size = self.NUM_NODES - 2 + self.servers = self.__start_ignite_nodes(version, blt_size) + + control_utility = ControlUtility(self.servers, self.test_context) + control_utility.activate() + + # Check baseline of activated cluster. + baseline = control_utility.baseline() + self.__check_baseline_size(baseline, blt_size) + self.__check_nodes_in_baseline(self.servers.nodes, baseline) + + # Set baseline using list of conststent ids. + new_node = self.__start_ignite_nodes(version, 1) + control_utility.set_baseline(self.servers.nodes + new_node.nodes) + blt_size += 1 + + baseline = control_utility.baseline() + self.__check_baseline_size(baseline, blt_size) + self.__check_nodes_in_baseline(new_node.nodes, baseline) + + # Set baseline using topology version. + new_node = self.__start_ignite_nodes(version, 1) + _, version, _ = control_utility.cluster_state() + control_utility.set_baseline(version) + blt_size += 1 + + baseline = control_utility.baseline() + self.__check_baseline_size(baseline, blt_size) + self.__check_nodes_in_baseline(new_node.nodes, baseline) + + @cluster(num_nodes=NUM_NODES) + @parametrize(version=str(DEV_BRANCH)) + @parametrize(version=str(LATEST_2_8)) + @parametrize(version=str(LATEST_2_7)) + def test_baseline_add_remove(self, version): + """ + Test add and remove nodes from baseline. + """ + blt_size = self.NUM_NODES - 1 + self.servers = self.__start_ignite_nodes(version, blt_size) + + control_utility = ControlUtility(self.servers, self.test_context) + + control_utility.activate() + + # Add node to baseline. + new_node = self.__start_ignite_nodes(version, 1) + control_utility.add_to_baseline(new_node.nodes) + blt_size += 1 + + baseline = control_utility.baseline() + self.__check_baseline_size(baseline, blt_size) + self.__check_nodes_in_baseline(new_node.nodes, baseline) + + # Expected failure (remove of online node is not allowed). + try: + control_utility.remove_from_baseline(new_node.nodes) + + assert False, "Remove of online node from baseline should fail!" + except ControlUtilityError: + pass + + # Remove of offline node from baseline. + new_node.stop() + + self.servers.await_event("Node left topology", timeout_sec=30, from_the_beginning=True) + + control_utility.remove_from_baseline(new_node.nodes) + blt_size -= 1 + + baseline = control_utility.baseline() + self.__check_baseline_size(baseline, blt_size) + self.__check_nodes_not_in_baseline(new_node.nodes, baseline) + + @cluster(num_nodes=NUM_NODES) + @parametrize(version=str(DEV_BRANCH)) + @parametrize(version=str(LATEST_2_8)) + @parametrize(version=str(LATEST_2_7)) + def test_activate_deactivate(self, version): + """ + Test activate and deactivate cluster. + """ + self.servers = self.__start_ignite_nodes(version, self.NUM_NODES) + + control_utility = ControlUtility(self.servers, self.test_context) + + control_utility.activate() + + state, _, _ = control_utility.cluster_state() + + assert state.lower() == 'active', 'Unexpected state %s' % state + + control_utility.deactivate() + + state, _, _ = control_utility.cluster_state() + + assert state.lower() == 'inactive', 'Unexpected state %s' % state + + @cluster(num_nodes=NUM_NODES) + @parametrize(version=str(DEV_BRANCH)) + @parametrize(version=str(LATEST_2_8)) + def test_baseline_autoadjust(self, version): + """ + Test activate and deactivate cluster. + """ + if version < V_2_8_0: + self.logger.info("Skipping test because this feature is not supported for version %s" % version) + return + + blt_size = self.NUM_NODES - 2 + self.servers = self.__start_ignite_nodes(version, blt_size) + + control_utility = ControlUtility(self.servers, self.test_context) + control_utility.activate() + + # Add node. + control_utility.enable_baseline_auto_adjust(2000) + new_node = self.__start_ignite_nodes(version, 1) + blt_size += 1 + + wait_until(lambda: len(control_utility.baseline()) == blt_size, timeout_sec=5) + + baseline = control_utility.baseline() + self.__check_nodes_in_baseline(new_node.nodes, baseline) + + # Add node when auto adjust disabled. + control_utility.disable_baseline_auto_adjust() + old_topology = control_utility.cluster_state().topology_version + new_node = self.__start_ignite_nodes(version, 1) + + wait_until(lambda: control_utility.cluster_state().topology_version != old_topology, timeout_sec=5) + baseline = control_utility.baseline() + self.__check_nodes_not_in_baseline(new_node.nodes, baseline) + + @staticmethod + def __check_nodes_in_baseline(nodes, baseline): + blset = set(node.consistent_id for node in baseline) + + for node in nodes: + assert node.consistent_id in blset + + @staticmethod + def __check_nodes_not_in_baseline(nodes, baseline): + blset = set(node.consistent_id for node in baseline) + + for node in nodes: + assert node.consistent_id not in blset + + @staticmethod + def __check_baseline_size(baseline, size): + assert len(baseline) == size, 'Unexpected size of baseline %d, %d expected' % (len(baseline), size) + + def __start_ignite_nodes(self, version, num_nodes, timeout_sec=180): + ignite_version = IgniteVersion(version) + + servers = IgniteService(self.test_context, num_nodes=num_nodes, version=ignite_version, + properties=self.properties(ignite_version)) + + servers.start(timeout_sec=timeout_sec) + + return servers 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 cd9feec..108fa9d 100644 --- a/modules/ducktests/tests/ignitetest/tests/pme_free_switch_test.py +++ b/modules/ducktests/tests/ignitetest/tests/pme_free_switch_test.py @@ -24,6 +24,7 @@ from ducktape.mark.resource import cluster from ignitetest.services.ignite import IgniteService from ignitetest.services.ignite_app import IgniteApplicationService +from ignitetest.services.utils.control_utility import ControlUtility from ignitetest.tests.utils.ignite_test import IgniteTest from ignitetest.tests.utils.version import DEV_BRANCH, LATEST_2_7, V_2_8_0, IgniteVersion @@ -106,8 +107,7 @@ class PmeFreeSwitchTest(IgniteTest): single_key_tx_streamer.start() if ignite_version >= V_2_8_0: - long_tx_streamer.execute( - "control.sh --host %s --baseline auto_adjust disable --yes" % ignites.nodes[0].account.hostname) + ControlUtility(ignites, self.test_context).disable_baseline_auto_adjust() self.stage("Stopping server node") diff --git a/modules/ducktests/tests/.pylintrc b/modules/ducktests/tests/tox.ini similarity index 73% rename from modules/ducktests/tests/.pylintrc rename to modules/ducktests/tests/tox.ini index 73536f3..8a7235b 100644 --- a/modules/ducktests/tests/.pylintrc +++ b/modules/ducktests/tests/tox.ini @@ -12,6 +12,29 @@ # 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. +[tox] +envlist = linter +skipsdist = True + +[travis] +python = + 3.8: linter + +[testenv] +envdir = {homedir}/.virtualenvs/ignite-ducktests-{envname} +deps = + ducktape==0.7.8 + requests==2.20.0 + monotonic + mock + pytest + pylint + +[testenv:linter] +basepython = python3.8 +commands = + pylint --rcfile=tox.ini ./ignitetest + [BASIC] min-public-methods=0 # TODO: Remove after migrating to python 3