blob: ef16fd04ca84e784691d7492b48975634c325ffa [file]
#!/usr/bin/env python3
#
# 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.
#
"""Run Edge ops against disposable data and stubbed service/stop commands."""
import fnmatch
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
import xml.etree.ElementTree as ET
REPOSITORY = Path(__file__).resolve().parents[2]
WINDOWS = os.name == "nt"
DIRECTORY_KEYS = (
"cn_system_dir",
"cn_consensus_dir",
"dn_system_dir",
"dn_data_dirs",
"dn_consensus_dir",
"dn_wal_dirs",
"dn_tracing_dir",
"dn_sync_dir",
"pipe_receiver_file_dirs",
"iot_consensus_v2_receiver_file_dirs",
"sort_tmp_dir",
)
class EdgeOpsTest(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory(prefix="iotdb-edge-ops-")
self.addCleanup(self.temporary.cleanup)
self.root = Path(self.temporary.name).resolve()
self.installation = self.root / "edge installation"
self.config = self.installation / "conf"
self.config.mkdir(parents=True)
self.events = self.root / "events.txt"
self.bin = self.root / "bin"
self.bin.mkdir()
self.env = os.environ.copy()
for key in (
"IOTDB_HOME",
"IOTDB_CONF",
"IOTDB_DATA_HOME",
"IOTDB_LOG_DIR",
"JAVA_HOME",
):
self.env.pop(key, None)
self.env["PATH"] = str(self.bin) + os.pathsep + self.env["PATH"]
self.env["EDGE_TEST_EVENTS"] = str(self.events)
self.env["EDGE_TEST_STOP_STATUS"] = "0"
self.env["SYSTEMD_DIR"] = str(self.root / "systemd")
Path(self.env["SYSTEMD_DIR"]).mkdir()
self.marker = self.directory(self.installation / "data") / "marker"
self.marker.touch()
self.keep = self.directory(self.installation / "logs") / "keep.log"
self.keep.touch()
if WINDOWS:
self.destroy = self.copy("tools/windows/ops/destroy-edge.bat")
self.stop = self.installation / "sbin/windows/stop-edge.bat"
self.write(
self.stop,
"@echo off\n"
'>>"%EDGE_TEST_EVENTS%" echo stop %*\n'
'if not exist "%IOTDB_HOME%\\data\\marker" exit /b 97\n'
"exit /b %EDGE_TEST_STOP_STATUS%\n",
)
else:
self.destroy = self.copy("tools/ops/destroy-edge.sh")
self.daemon = self.copy("tools/ops/daemon-edge.sh")
self.stop = self.installation / "sbin/stop-edge.sh"
self.write(
self.stop,
"#!/bin/bash\n"
'printf "stop %s\\n" "$*" >> "$EDGE_TEST_EVENTS"\n'
"sleep 0.05\n"
'[ -f "$IOTDB_HOME/data/marker" ] || exit 97\n'
'exit "$EDGE_TEST_STOP_STATUS"\n',
)
self.write(
self.installation / "sbin/start-edge.sh", "#!/bin/bash\nexit 0\n"
)
self.write(self.bin / "java", "#!/bin/bash\nexit 0\n")
self.write(
self.bin / "systemctl",
"#!/bin/bash\n"
'printf "systemctl %s\\n" "$*" >> "$EDGE_TEST_EVENTS"\n'
'if [ "$1" = show ]; then printf "%s\\n" "$EDGE_TEST_SERVICE_PIDFILE"; fi\n'
'[ "$1" != "$EDGE_TEST_SYSTEMCTL_FAIL" ]\n',
)
@staticmethod
def write(path, content):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(content.encode("utf-8"))
path.chmod(0o755)
def copy(self, relative):
destination = self.installation / relative
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(REPOSITORY / "scripts" / relative, destination)
destination.chmod(0o755)
return destination
@staticmethod
def directory(path):
path.mkdir(parents=True, exist_ok=True)
(path / "data.txt").touch()
return path
def properties(self, lines, name="iotdb-system.properties"):
self.write(self.config / name, "\r\n".join(lines) + "\r\n")
def run_script(self, script, arguments=(), answer=""):
if WINDOWS:
command = '"{}" /d /c call "{}" {}'.format(
os.environ["COMSPEC"], script, " ".join(arguments)
)
else:
command = ["bash", str(script), *arguments]
return subprocess.run(
command,
input=answer,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
errors="replace",
env=self.env,
cwd=self.root,
timeout=30,
)
def assert_success(self, result):
self.assertEqual(result.returncode, 0, result.stdout)
def event_lines(self):
return self.events.read_text().splitlines() if self.events.exists() else []
def test_default_answer_and_no_do_not_stop_or_delete(self):
for answer in ("", "\n", "n\n", "yes\n"):
with self.subTest(answer=answer):
self.env["CLEAN_SERVICE"] = "y"
self.assert_success(self.run_script(self.destroy, answer=answer))
self.assertTrue(self.marker.exists())
self.assertEqual(self.event_lines(), [])
def test_invalid_arguments_do_not_stop_or_delete(self):
for arguments in (("--unknown",), ("-f", "extra")):
with self.subTest(arguments=arguments):
result = self.run_script(self.destroy, arguments)
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertTrue(self.marker.exists())
self.assertEqual(self.event_lines(), [])
def test_force_cleans_defaults_from_an_unrelated_working_directory(self):
tracing = self.directory(self.installation / "datanode/tracing")
result = self.run_script(self.destroy, ("-f",))
self.assert_success(result)
self.assertFalse((self.installation / "data").exists())
self.assertFalse(tracing.exists())
self.assertTrue(self.keep.exists())
self.assertTrue(self.config.exists())
self.assertIn("stop -f", self.event_lines())
self.assertIn("IoTDB Edge clean done", result.stdout)
def test_yes_confirms_cleanup(self):
self.assert_success(self.run_script(self.destroy, answer="Y\n"))
self.assertFalse(self.marker.exists())
def test_stop_failure_preserves_all_data(self):
self.env["EDGE_TEST_STOP_STATUS"] = "42"
result = self.run_script(self.destroy, ("-f",))
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertTrue(self.marker.exists())
self.assertNotIn("clean done", result.stdout)
def test_all_configured_directories_and_multiple_tiers(self):
targets = []
lines = ["# dn_data_dirs=ignored", "! cn_system_dir=ignored"]
for index, key in enumerate(DIRECTORY_KEYS):
base = self.installation if index % 2 else self.root
target = self.directory(base / (key + " with spaces=[1]"))
value = target.relative_to(base) if base == self.installation else target
lines.append(" {} = {} ".format(key, value.as_posix()))
targets.append(target)
tiers = [
self.directory(self.installation / "tier one"),
self.directory(self.root / "absolute tier"),
self.directory(self.installation / "tier=two"),
]
lines.append(
" dn_data_dirs = tier one ; {}, tier=two ".format(tiers[1].as_posix())
)
# The previous value of a repeated property must not be removed.
superseded = targets.pop(DIRECTORY_KEYS.index("dn_data_dirs"))
self.properties(lines)
self.assert_success(self.run_script(self.destroy, ("-f",)))
for target in targets + tiers:
self.assertFalse(target.exists(), str(target))
self.assertTrue(superseded.exists())
self.assertTrue((self.config / "iotdb-system.properties").exists())
self.assertTrue(self.keep.exists())
def test_legacy_configuration_files(self):
cn = self.directory(self.root / "legacy cn")
dn = self.directory(self.root / "legacy dn")
self.properties(
["cn_system_dir=" + cn.as_posix()], "iotdb-confignode.properties"
)
self.properties(["dn_data_dirs=" + dn.as_posix()], "iotdb-datanode.properties")
self.assert_success(self.run_script(self.destroy, ("-f",)))
self.assertFalse(cn.exists())
self.assertFalse(dn.exists())
def test_custom_home_and_configuration(self):
self.env["IOTDB_HOME"] = str(self.installation)
self.config = self.root / "custom configuration"
self.config.mkdir()
self.env["IOTDB_CONF"] = str(self.config)
target = self.directory(self.root / "custom data")
self.properties(["dn_data_dirs=" + target.as_posix()])
self.assert_success(self.run_script(self.destroy, ("-f",)))
self.assertFalse(target.exists())
def test_unified_configuration_takes_precedence(self):
untouched = self.directory(self.root / "legacy data")
self.properties(
["dn_data_dirs=" + untouched.as_posix()], "iotdb-datanode.properties"
)
self.properties(["# Defaults"])
self.assert_success(self.run_script(self.destroy, ("-f",)))
self.assertTrue(untouched.exists())
def test_home_or_parent_is_rejected_before_any_deletion(self):
# Only disposable paths are used even if the guard regresses.
for value in (".", "data/..", ".."):
with self.subTest(value=value):
self.properties(["dn_data_dirs=" + value])
result = self.run_script(self.destroy, ("-f",))
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertIn("Refusing to remove", result.stdout)
self.assertTrue(self.marker.exists())
self.assertTrue(self.keep.exists())
def test_missing_stop_script_preserves_data(self):
self.stop.unlink()
result = self.run_script(self.destroy, ("-f",))
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertTrue(self.marker.exists())
def test_empty_property_uses_default_not_an_earlier_value(self):
untouched = self.directory(self.root / "superseded data")
self.properties(["dn_data_dirs=" + untouched.as_posix(), "dn_data_dirs= "])
self.assert_success(self.run_script(self.destroy, ("-f",)))
self.assertFalse(self.marker.exists())
self.assertTrue(untouched.exists())
@unittest.skipIf(WINDOWS, "Unix symbolic links")
def test_symlinked_parent_cannot_hide_a_home_directory(self):
(self.root / "home alias").symlink_to(
self.installation, target_is_directory=True
)
self.properties(
["dn_data_dirs=" + (self.root / "home alias/data/..").as_posix()]
)
result = self.run_script(self.destroy, ("-f",))
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertIn("Refusing to remove", result.stdout)
self.assertTrue(self.marker.exists())
@unittest.skipIf(WINDOWS, "Unix deletion errors")
def test_deletion_failure_is_not_reported_as_success(self):
self.write(self.bin / "rm", "#!/bin/bash\nexit 42\n")
result = self.run_script(self.destroy, ("-f",))
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertNotIn("clean done", result.stdout)
self.assertTrue(self.marker.exists())
@unittest.skipIf(
WINDOWS, "The Unix launcher supports a separate DataNode data home"
)
def test_data_home_does_not_change_confignode_home(self):
data_home = self.directory(self.root / "external data home")
self.env["IOTDB_DATA_HOME"] = str(data_home)
cn = self.directory(self.installation / "cn")
dn = self.directory(data_home / "dn")
keep_cn = self.directory(data_home / "cn")
keep_dn = self.directory(self.installation / "dn")
self.properties(["cn_system_dir=cn", "dn_data_dirs=dn"])
self.assert_success(self.run_script(self.destroy, ("-f",)))
self.assertFalse(cn.exists())
self.assertFalse(dn.exists())
self.assertTrue(keep_cn.exists())
self.assertTrue(keep_dn.exists())
@unittest.skipIf(WINDOWS, "Unix systemd integration")
def test_cleanup_stops_only_the_matching_systemd_service(self):
self.env["EDGE_TEST_SERVICE_PIDFILE"] = str(self.installation / "edge.pid")
self.assert_success(self.run_script(self.destroy, ("-f",)))
events = self.event_lines()
self.assertLess(
events.index("systemctl stop iotdb-edge"), events.index("stop -f")
)
@unittest.skipIf(WINDOWS, "Unix systemd integration")
def test_other_systemd_installation_is_not_stopped(self):
self.env["EDGE_TEST_SERVICE_PIDFILE"] = str(self.root / "another edge/edge.pid")
self.assert_success(self.run_script(self.destroy, ("-f",)))
self.assertNotIn("systemctl stop iotdb-edge", self.event_lines())
@unittest.skipIf(WINDOWS, "Unix systemd integration")
def test_systemd_stop_failure_preserves_data(self):
self.env["EDGE_TEST_SERVICE_PIDFILE"] = str(self.installation / "edge.pid")
self.env["EDGE_TEST_SYSTEMCTL_FAIL"] = "stop"
result = self.run_script(self.destroy, ("-f",))
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertTrue(self.marker.exists())
self.assertNotIn("stop -f", self.event_lines())
@unittest.skipIf(WINDOWS, "Unix systemd registration")
def test_daemon_tracks_the_forked_jvm_and_supports_java_home(self):
java_home = self.root / "custom java"
self.write(java_home / "bin/java", "#!/bin/bash\nexit 0\n")
self.env["JAVA_HOME"] = str(java_home)
result = self.run_script(self.daemon, answer="\n\n")
self.assert_success(result)
unit_path = Path(self.env["SYSTEMD_DIR"]) / "iotdb-edge.service"
unit = unit_path.read_text()
for setting in (
"Type=forking",
"PIDFile={}/edge.pid".format(self.installation),
"Restart=on-failure",
"SuccessExitStatus=143",
"RestartPreventExitStatus=SIGKILL",
"LimitNOFILE=65536",
"StartLimitIntervalSec=600s",
"StartLimitBurst=3",
'Environment="JAVA_HOME={}"'.format(java_home),
'Environment="PATH={}/bin:'.format(java_home),
'ExecStart="{}/sbin/start-edge.sh"'.format(self.installation),
'ExecStop="{}/sbin/stop-edge.sh"'.format(self.installation),
):
self.assertIn(setting, unit)
self.assertEqual(
self.event_lines(),
[
"systemctl daemon-reload",
"systemctl stop iotdb-edge",
"stop ",
"systemctl start iotdb-edge",
"systemctl enable iotdb-edge",
],
)
if shutil.which("systemd-analyze"):
check = subprocess.run(
["systemd-analyze", "verify", str(unit_path)],
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
timeout=30,
)
self.assertEqual(check.returncode, 0, check.stdout)
@unittest.skipIf(WINDOWS, "Unix systemd registration")
def test_daemon_path_java_and_declining_start_and_enable(self):
self.assert_success(self.run_script(self.daemon, answer="n\nn\n"))
self.assertEqual(self.event_lines(), ["systemctl daemon-reload"])
unit = (Path(self.env["SYSTEMD_DIR"]) / "iotdb-edge.service").read_text()
self.assertIn('Environment="JAVA_HOME="', unit)
self.assertIn('Environment="PATH={}"'.format(self.env["PATH"]), unit)
@unittest.skipIf(WINDOWS, "Unix systemd registration")
def test_daemon_rejects_invalid_java_home(self):
self.env["JAVA_HOME"] = str(self.root / "missing java")
result = self.run_script(self.daemon, answer="n\nn\n")
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertEqual(self.event_lines(), [])
@unittest.skipIf(WINDOWS, "Unix systemd registration")
def test_daemon_propagates_service_start_errors(self):
self.env["EDGE_TEST_SYSTEMCTL_FAIL"] = "start"
result = self.run_script(self.daemon, answer="y\ny\n")
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertNotIn("systemctl enable iotdb-edge", self.event_lines())
@unittest.skipUnless(WINDOWS, "Native Windows stop script")
def test_windows_force_stop_returns_without_pausing(self):
self.copy("sbin/windows/stop-edge.bat")
result = self.run_script(self.stop, ("-f",))
self.assert_success(result)
self.assertNotIn("Press any key", result.stdout)
class AssemblyTest(unittest.TestCase):
@staticmethod
def packaged(descriptor, path):
root = ET.parse(REPOSITORY / descriptor).getroot()
for file_set in root.findall("./fileSets/fileSet"):
directory = file_set.findtext("directory", "")
if not directory.endswith("/scripts/tools"):
continue
includes = [node.text for node in file_set.findall("./includes/include")]
excludes = [node.text for node in file_set.findall("./excludes/exclude")]
if (
not includes or any(fnmatch.fnmatchcase(path, p) for p in includes)
) and not any(fnmatch.fnmatchcase(path, p) for p in excludes):
return file_set.findtext("fileMode")
return None
def test_edge_ops_are_executable_and_only_in_edge_packages(self):
for path in (
"ops/daemon-edge.sh",
"ops/destroy-edge.sh",
"windows/ops/destroy-edge.bat",
):
self.assertEqual(
self.packaged("distribution/src/assembly/edge.xml", path), "0755"
)
for descriptor in (
"distribution/src/assembly/all.xml",
"distribution/src/assembly/datanode.xml",
"distribution/src/assembly/confignode.xml",
"iotdb-core/datanode/src/assembly/server.xml",
"iotdb-core/confignode/src/assembly/confignode.xml",
):
self.assertIsNone(
self.packaged(descriptor, path), descriptor + ": " + path
)
def test_edge_does_not_restore_standalone_ops(self):
paths = ["ops/daemon-{}.sh".format(node) for node in ("confignode", "datanode")]
for node in ("all", "confignode", "datanode"):
paths.extend(
(
"ops/destroy-{}.sh".format(node),
"windows/ops/destroy-{}.bat".format(node),
)
)
for path in paths:
self.assertIsNone(
self.packaged("distribution/src/assembly/edge.xml", path), path
)
if __name__ == "__main__":
unittest.main(verbosity=2)