From 0a53f12d77a789acdb8a6cd6aabb1dd6506bca00 Mon Sep 17 00:00:00 2001 From: LiranAbir Date: Tue, 22 Sep 2026 11:44:13 +0300 Subject: [PATCH 1/4] Waive cluster bus protected mode for non-TLS cluster envs redis/redis#15722 (merged 2026-09-15, backported to 8.2/8.4/8.6 the same day) introduces cluster-bus-port-protected-mode, defaulting to yes. A node started with cluster-enabled yes and tls-cluster disabled now refuses to start, because its cluster bus port would be unauthenticated. That is exactly how the oss-cluster env is built, so every shard of it dies at startup and all cluster-topology tests fail at connect with "Connection refused". The refusal happens during config validation, before the server opens its logfile, so the only artifact left behind is an empty log - which made this expensive to diagnose downstream (MOD-18751: every oss-cluster leg of the RedisTimeSeries, RedisBloom and RedisJSON nightlies, public and dev forks, x64, arm64 and macOS, red since 2026-09-15). Waive the protection: the bus ports of a test env are bound to localhost on an ephemeral host, which is the condition the directive documents for waiving it, and redis waived it the same way in its own harness in that PR. It grants no new exposure, since the bus port was equally unauthenticated before. The tls-cluster path is untouched and keeps authenticating the bus. The option does not exist before 8.2.10 / 8.4.7 / 8.6.7 / 8.9.241, and an unknown directive is itself fatal, so pass it only to a server that knows it. Reuse the version we already read, rather than calling _getRedisVersion() a second time per shard: it spawns redis-server --version and polls in 0.1s steps. Co-Authored-By: Claude Opus 5 --- RLTest/redis_std.py | 25 ++++++++++++++++++++++++- tests/unit/test_redis_std.py | 12 +++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/RLTest/redis_std.py b/RLTest/redis_std.py index 6ad0065..210b30d 100644 --- a/RLTest/redis_std.py +++ b/RLTest/redis_std.py @@ -17,6 +17,23 @@ SLAVE = 'slave' +def hasClusterBusProtectedMode(version): + """Whether this redis knows 'cluster-bus-port-protected-mode'. + + Added by redis/redis#15722 on 2026-09-15 and backported to the 8.2, 8.4 and + 8.6 lines the same day, so the first version carrying it differs per line. + `version` is encoded as major * 10000 + minor * 100 + patch, the way + StandardEnv._getRedisVersion() returns it. + """ + # The unstable entry is the one version cannot pin down: the option landed + # while version.h already said 8.9.241, so an 8.9.x from before it reads as + # supporting the option. Harmless in practice - unstable is built from tip. + for line, first in ((80900, 80900), (80600, 80607), (80400, 80407), (80200, 80210)): + if version >= line: + return version >= first + return False + + class StandardEnv(object): def __init__(self, redisBinaryPath, port=6379, modulePath=None, moduleArgs=None, outputFilesFormat=None, dbDirPath=None, useSlaves=False, serverId=1, password=None, libPath=None, clusterEnabled=False, decodeResponses=False, @@ -178,6 +195,7 @@ def _getRedisVersion(self): return int(v[0]) * 10000 + int(v[1]) * 100 + int(v[2]) def createCmdArgs(self, role): + redisVersion = self._getRedisVersion() cmdArgs = [] if self.debugger: cmdArgs += self.debugger.generate_command(self._getValgrindFilePath(role) if not self.noCatch else None) @@ -233,6 +251,11 @@ def createCmdArgs(self, role): '--cluster-node-timeout', '5000' if self.clusterNodeTimeout is None else str(self.clusterNodeTimeout)] if self.useTLS: cmdArgs += ['--tls-cluster', 'yes'] + elif hasClusterBusProtectedMode(redisVersion): + # Without tls-cluster the cluster bus port is unauthenticated, + # and redis refuses to start unless that is acknowledged. The + # bus ports of a test env are local and short-lived, so waive it. + cmdArgs += ['--cluster-bus-port-protected-mode', 'no'] if self.useAof: cmdArgs += ['--appendonly', 'yes'] cmdArgs += ['--appendfilename', self._getFileName(role, '.aof')] @@ -247,7 +270,7 @@ def createCmdArgs(self, role): cmdArgs += ['--tls-replication', 'yes'] - if self._getRedisVersion() > 70000: + if redisVersion > 70000: if self.enableDebugCommand: cmdArgs += ['--enable-debug-command', 'yes'] if self.enableProtectedConfigs: diff --git a/tests/unit/test_redis_std.py b/tests/unit/test_redis_std.py index 8a24e8f..cb1ad48 100644 --- a/tests/unit/test_redis_std.py +++ b/tests/unit/test_redis_std.py @@ -3,7 +3,7 @@ import tempfile from unittest import TestCase -from RLTest.redis_std import StandardEnv +from RLTest.redis_std import StandardEnv, hasClusterBusProtectedMode from tests.unit.test_common import REDIS_BINARY, TLS_CERT, TLS_KEY, TLS_CACERT tlsCertFile = 'fake_redis.crt' @@ -83,6 +83,16 @@ def test_has_interactive_debugger(self): std_env = StandardEnv(redisBinaryPath=REDIS_BINARY, outputFilesFormat='%s-test') assert std_env.has_interactive_debugger == None + def test_has_cluster_bus_protected_mode(self): + # First version carrying redis/redis#15722 on each release line, and + # unstable, where version.h read 8.9.241 when the option landed. + for version in (80210, 80407, 80607, 81141): + assert hasClusterBusProtectedMode(version), version + # Last version of each backported line without it, plus the lines that + # never got it at all. + for version in (80209, 80406, 80606, 80000, 70400, 60200): + assert not hasClusterBusProtectedMode(version), version + def test_create_cmd_args_default(self): std_env = StandardEnv(redisBinaryPath=REDIS_BINARY, outputFilesFormat='%s-test') role = 'master' From 1424af4f34b25bfcd282ed330498a66c8a83d076 Mon Sep 17 00:00:00 2001 From: LiranAbir Date: Tue, 22 Sep 2026 15:22:00 +0300 Subject: [PATCH 2/4] Probe the binary for the option instead of gating on its version The version gate was wrong for four bands of redis. cluster-bus-port-protected- mode was added mid-release-line and backported, so the first release carrying it differs per line - 8.2.10, 8.4.7, 8.6.7, 8.8.3 and 8.10.2 - and the gate claimed support from 8.8.0 and 8.10.0, where the option does not exist. It also claimed support for any development build, whose placeholder version says nothing about the commit it was built from. In each of those cases the option was passed to a redis that rejects it, turning a working plain-cluster environment into the very startup failure this change exists to fix. Adding the two missing boundaries would patch the symptom and leave the cause: a version number cannot say which commit a binary came from, so the next backport or development build breaks it again. So ask the binary. It is started once, with --port 0 so that it exits as soon as its configuration has loaded, which neither binds a port nor leaves a server behind; an unknown directive is rejected earlier, while the configuration is still being parsed. The answer is cached per binary path, as it is needed once per server started. Tests cover the bands the version gate got wrong, using a stand-in redis whose reported version and actual support for the option disagree - which is exactly what a version number cannot get right. Verified as well against a real redis 7.2.6, which correctly probes as unsupported. Found in review by gabsow, who also reproduced the development-build case. Co-Authored-By: Claude Opus 5 --- RLTest/redis_std.py | 47 ++++++++++++++++-------- tests/unit/test_redis_std.py | 71 +++++++++++++++++++++++++++++++----- 2 files changed, 92 insertions(+), 26 deletions(-) diff --git a/RLTest/redis_std.py b/RLTest/redis_std.py index 210b30d..0ff99ca 100644 --- a/RLTest/redis_std.py +++ b/RLTest/redis_std.py @@ -17,21 +17,37 @@ SLAVE = 'slave' -def hasClusterBusProtectedMode(version): - """Whether this redis knows 'cluster-bus-port-protected-mode'. +_clusterBusProtectedModeSupport = {} - Added by redis/redis#15722 on 2026-09-15 and backported to the 8.2, 8.4 and - 8.6 lines the same day, so the first version carrying it differs per line. - `version` is encoded as major * 10000 + minor * 100 + patch, the way - StandardEnv._getRedisVersion() returns it. + +def hasClusterBusProtectedMode(redisBinaryPath): + """Whether this redis accepts 'cluster-bus-port-protected-mode'. + + Asked of the binary instead of inferred from its version. redis/redis#15722 + added the option mid-line and it was backported, so the first release + carrying it differs per line - 8.2.10, 8.4.7, 8.6.7, 8.8.3, 8.10.2 - and a + development build reports a placeholder version that says nothing about the + commit it was built from. Only the binary can answer. + + '--port 0' makes redis exit as soon as its configuration has loaded, so this + neither binds a port nor leaves a server behind. An unknown directive is + rejected earlier, while the configuration is still being parsed, and that is + what tells the two cases apart: both exit non-zero. + + The answer is cached per binary, as it is asked once per server started. """ - # The unstable entry is the one version cannot pin down: the option landed - # while version.h already said 8.9.241, so an 8.9.x from before it reads as - # supporting the option. Harmless in practice - unstable is built from tip. - for line, first in ((80900, 80900), (80600, 80607), (80400, 80407), (80200, 80210)): - if version >= line: - return version >= first - return False + if redisBinaryPath not in _clusterBusProtectedModeSupport: + p = subprocess.Popen([redisBinaryPath, '--port', '0', + '--cluster-bus-port-protected-mode', 'no'], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + try: + output = p.communicate(timeout=30)[0].decode('utf-8', 'replace') + except subprocess.TimeoutExpired: + # Still running, so the configuration was accepted. + p.kill() + output = '' + _clusterBusProtectedModeSupport[redisBinaryPath] = 'Bad directive' not in output + return _clusterBusProtectedModeSupport[redisBinaryPath] class StandardEnv(object): @@ -195,7 +211,6 @@ def _getRedisVersion(self): return int(v[0]) * 10000 + int(v[1]) * 100 + int(v[2]) def createCmdArgs(self, role): - redisVersion = self._getRedisVersion() cmdArgs = [] if self.debugger: cmdArgs += self.debugger.generate_command(self._getValgrindFilePath(role) if not self.noCatch else None) @@ -251,7 +266,7 @@ def createCmdArgs(self, role): '--cluster-node-timeout', '5000' if self.clusterNodeTimeout is None else str(self.clusterNodeTimeout)] if self.useTLS: cmdArgs += ['--tls-cluster', 'yes'] - elif hasClusterBusProtectedMode(redisVersion): + elif hasClusterBusProtectedMode(self.redisBinaryPath): # Without tls-cluster the cluster bus port is unauthenticated, # and redis refuses to start unless that is acknowledged. The # bus ports of a test env are local and short-lived, so waive it. @@ -270,7 +285,7 @@ def createCmdArgs(self, role): cmdArgs += ['--tls-replication', 'yes'] - if redisVersion > 70000: + if self._getRedisVersion() > 70000: if self.enableDebugCommand: cmdArgs += ['--enable-debug-command', 'yes'] if self.enableProtectedConfigs: diff --git a/tests/unit/test_redis_std.py b/tests/unit/test_redis_std.py index cb1ad48..35ed787 100644 --- a/tests/unit/test_redis_std.py +++ b/tests/unit/test_redis_std.py @@ -3,7 +3,7 @@ import tempfile from unittest import TestCase -from RLTest.redis_std import StandardEnv, hasClusterBusProtectedMode +from RLTest.redis_std import MASTER, StandardEnv, hasClusterBusProtectedMode from tests.unit.test_common import REDIS_BINARY, TLS_CERT, TLS_KEY, TLS_CACERT tlsCertFile = 'fake_redis.crt' @@ -83,15 +83,66 @@ def test_has_interactive_debugger(self): std_env = StandardEnv(redisBinaryPath=REDIS_BINARY, outputFilesFormat='%s-test') assert std_env.has_interactive_debugger == None - def test_has_cluster_bus_protected_mode(self): - # First version carrying redis/redis#15722 on each release line, and - # unstable, where version.h read 8.9.241 when the option landed. - for version in (80210, 80407, 80607, 81141): - assert hasClusterBusProtectedMode(version), version - # Last version of each backported line without it, plus the lines that - # never got it at all. - for version in (80209, 80406, 80606, 80000, 70400, 60200): - assert not hasClusterBusProtectedMode(version), version + def _fakeRedisBinary(self, name, startupOutput, version='8.8.0'): + """A stand-in redis that reports `version` and prints `startupOutput`. + + Lets the probe be tested against a binary whose version and whose actual + support for the option disagree, which is the case version numbers + cannot get right. + """ + path = os.path.join(self.test_dir, name) + with open(path, 'w') as f: + f.write('#!/bin/sh\n' + 'case "$1" in --version) echo "Redis server v=%s sha=00000000:0 bits=64"; exit 0;; esac\n' + 'echo "%s"\n' + 'exit 1\n' % (version, startupOutput)) + os.chmod(path, 0o755) + return path + + def test_has_cluster_bus_protected_mode_probes_the_binary(self): + unsupported = self._fakeRedisBinary('redis-unsupported', + 'Bad directive or wrong number of arguments') + supported = self._fakeRedisBinary('redis-supported', + 'Configured to not listen anywhere, exiting.') + assert not hasClusterBusProtectedMode(unsupported) + assert hasClusterBusProtectedMode(supported) + # Answer is cached, so removing the binary changes nothing. + os.remove(unsupported) + assert not hasClusterBusProtectedMode(unsupported) + + def test_create_cmd_args_cluster_bus_protected_mode(self): + flag = ['--cluster-bus-port-protected-mode', 'no'] + + def args(binary, **kwargs): + env = StandardEnv(redisBinaryPath=binary, outputFilesFormat='%s-test', + dbDirPath=self.test_dir, **kwargs) + return env.createCmdArgs(MASTER) + + # The option was added mid-release-line and backported, so a version + # number cannot say whether a given build has it: 8.8.0 through 8.8.2 do + # not, 8.8.3 does, and the same holds for 8.10.1 versus 8.10.2. Passing + # it to a build without it is fatal, so these must not be waived blind. + for version in ('8.8.0', '8.8.2', '8.10.1', '255.255.255'): + binary = self._fakeRedisBinary('redis-no-option-' + version, + 'Bad directive or wrong number of arguments', version) + assert flag[0] not in args(binary, clusterEnabled=True), version + + for version in ('8.8.3', '8.10.2', '255.255.255'): + binary = self._fakeRedisBinary('redis-with-option-' + version, + 'Configured to not listen anywhere, exiting.', version) + cmdArgs = args(binary, clusterEnabled=True) + assert cmdArgs[-2:] == flag, (version, cmdArgs) + + supported = self._fakeRedisBinary('redis-plain', + 'Configured to not listen anywhere, exiting.') + # Only a cluster node opens a bus port, and tls-cluster authenticates it. + assert flag[0] not in args(supported) + tlsArgs = args(supported, clusterEnabled=True, useTLS=True, + tlsCertFile=os.path.join(self.test_dir, tlsCertFile), + tlsKeyFile=os.path.join(self.test_dir, tlsKeyFile), + tlsCaCertFile=os.path.join(self.test_dir, tlsCaCertFile)) + assert flag[0] not in tlsArgs + assert '--tls-cluster' in tlsArgs def test_create_cmd_args_default(self): std_env = StandardEnv(redisBinaryPath=REDIS_BINARY, outputFilesFormat='%s-test') From ac45ccfb4bd924cc6d9fd72659d32a7d08c624fa Mon Sep 17 00:00:00 2001 From: LiranAbir Date: Wed, 23 Sep 2026 10:55:51 +0300 Subject: [PATCH 3/4] Let the caller ask for cluster-bus-port-protected-mode Replaces the capability probe of the previous two commits, and with it the attempt to have RLTest work out on its own whether to pass the option. Nothing available to RLTest can decide that. cluster-bus-port-protected-mode was added by redis/redis#15722 and backported mid-line, so support does not follow from a version number: 8.2.10, 8.4.7, 8.6.7, 8.8.3 and 8.10.2 have it while their earlier patches do not, and the 8.12 line that enables it by default reports 8.9.241 in version.h - the same version as builds from before the change, which reject the option. Probing the binary instead needed the server's own diagnostics to tell acceptance from rejection, and those differ per build, as review of the previous commit showed. The caller does know, because it knows what it built. So take it as an option, the way every other flag here is taken: clusterBusPortProtectedMode on StandardEnv and Env, Defaults.cluster_bus_port_protected_mode, and --cluster_bus_port_protected_mode on the command line. None, the default, passes nothing and leaves behaviour as it is today. Only a cluster node opens a bus port, so the option is emitted alongside the other cluster directives and never for a standalone or replica process. MOD-18751. Co-Authored-By: Claude Opus 5 --- RLTest/__main__.py | 7 ++++ RLTest/env.py | 4 +++ RLTest/redis_std.py | 48 ++++++--------------------- tests/unit/test_redis_std.py | 63 +----------------------------------- 4 files changed, 21 insertions(+), 101 deletions(-) diff --git a/RLTest/__main__.py b/RLTest/__main__.py index 9bb7143..f9d783e 100644 --- a/RLTest/__main__.py +++ b/RLTest/__main__.py @@ -148,6 +148,12 @@ def do_normal_conn(self, line): '--cluster_node_timeout', default=5000, help='sets the node timeout on cluster in milliseconds') +parser.add_argument( + '--cluster_bus_port_protected_mode', default=None, choices=['yes', 'no'], + help='sets cluster-bus-port-protected-mode; only pass it to a redis that has the option ' + '(8.12 and up, or a backported 8.2.10/8.4.7/8.6.7/8.8.3/8.10.2), as an unknown ' + 'directive stops the server from starting') + parser.add_argument( '--cluster-start-timeout', default=40, type=int, help='timeout in seconds to wait for cluster to be ready (default 40 seconds). ' @@ -543,6 +549,7 @@ def __init__(self): Defaults.tls_passphrase = self.args.tls_passphrase Defaults.oss_password = self.args.oss_password Defaults.cluster_node_timeout = self.args.cluster_node_timeout + Defaults.cluster_bus_port_protected_mode = self.args.cluster_bus_port_protected_mode Defaults.cluster_start_timeout = self.args.cluster_start_timeout if Defaults.cluster_start_timeout < 5: raise Exception('--cluster-start-timeout must be at least 5 seconds') diff --git a/RLTest/env.py b/RLTest/env.py index ed6e68d..82085f0 100644 --- a/RLTest/env.py +++ b/RLTest/env.py @@ -142,6 +142,7 @@ class Defaults: randomize_ports = False oss_password = None cluster_node_timeout = None + cluster_bus_port_protected_mode = None cluster_start_timeout = 40 curr_test_name = None port = 6379 @@ -208,6 +209,7 @@ def __init__(self, testName=None, testDescription=None, module=None, useAof=None, useRdbPreamble=None, forceTcp=False, useTLS=False, tlsCertFile=None, tlsKeyFile=None, tlsCaCertFile=None, tlsPassphrase=None, logDir=None, redisBinaryPath=None, dmcBinaryPath=None, redisEnterpriseBinaryPath=None, noDefaultModuleArgs=False, clusterNodeTimeout = None, + clusterBusPortProtectedMode=None, freshEnv=False, enableDebugCommand=None, enableModuleCommand=None, enableProtectedConfigs=None, protocol=None, terminateRetries=None, terminateRetrySecs=None, redisConfigFile=None, dualTLS=False, startupGraceSecs=None): @@ -247,6 +249,7 @@ def __init__(self, testName=None, testDescription=None, module=None, self.dmcBinaryPath = expandBinary(dmcBinaryPath) if dmcBinaryPath else Defaults.proxy_binary self.redisEnterpriseBinaryPath = expandBinary(redisEnterpriseBinaryPath) if redisEnterpriseBinaryPath else Defaults.re_binary self.clusterNodeTimeout = clusterNodeTimeout if clusterNodeTimeout else Defaults.cluster_node_timeout + self.clusterBusPortProtectedMode = clusterBusPortProtectedMode if clusterBusPortProtectedMode is not None else Defaults.cluster_bus_port_protected_mode self.port = Defaults.port self.enableDebugCommand = enableDebugCommand if enableDebugCommand is not None else Defaults.enable_debug_command self.enableProtectedConfigs = enableProtectedConfigs if enableProtectedConfigs is not None\ @@ -373,6 +376,7 @@ def getEnvKwargs(self): 'tlsKeyFile': self.tlsKeyFile, 'tlsCaCertFile': self.tlsCaCertFile, 'clusterNodeTimeout': self.clusterNodeTimeout, + 'clusterBusPortProtectedMode': self.clusterBusPortProtectedMode, 'tlsPassphrase': self.tlsPassphrase, 'port': self.port, 'enableDebugCommand': self.enableDebugCommand, diff --git a/RLTest/redis_std.py b/RLTest/redis_std.py index 0ff99ca..aa5337f 100644 --- a/RLTest/redis_std.py +++ b/RLTest/redis_std.py @@ -17,44 +17,11 @@ SLAVE = 'slave' -_clusterBusProtectedModeSupport = {} - - -def hasClusterBusProtectedMode(redisBinaryPath): - """Whether this redis accepts 'cluster-bus-port-protected-mode'. - - Asked of the binary instead of inferred from its version. redis/redis#15722 - added the option mid-line and it was backported, so the first release - carrying it differs per line - 8.2.10, 8.4.7, 8.6.7, 8.8.3, 8.10.2 - and a - development build reports a placeholder version that says nothing about the - commit it was built from. Only the binary can answer. - - '--port 0' makes redis exit as soon as its configuration has loaded, so this - neither binds a port nor leaves a server behind. An unknown directive is - rejected earlier, while the configuration is still being parsed, and that is - what tells the two cases apart: both exit non-zero. - - The answer is cached per binary, as it is asked once per server started. - """ - if redisBinaryPath not in _clusterBusProtectedModeSupport: - p = subprocess.Popen([redisBinaryPath, '--port', '0', - '--cluster-bus-port-protected-mode', 'no'], - stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - try: - output = p.communicate(timeout=30)[0].decode('utf-8', 'replace') - except subprocess.TimeoutExpired: - # Still running, so the configuration was accepted. - p.kill() - output = '' - _clusterBusProtectedModeSupport[redisBinaryPath] = 'Bad directive' not in output - return _clusterBusProtectedModeSupport[redisBinaryPath] - - class StandardEnv(object): def __init__(self, redisBinaryPath, port=6379, modulePath=None, moduleArgs=None, outputFilesFormat=None, dbDirPath=None, useSlaves=False, serverId=1, password=None, libPath=None, clusterEnabled=False, decodeResponses=False, useAof=False, useRdbPreamble=True, debugger=None, sanitizer=None, noCatch=False, noLog=False, unix=False, verbose=False, useTLS=False, - tlsCertFile=None, tlsKeyFile=None, tlsCaCertFile=None, clusterNodeTimeout=None, tlsPassphrase=None, enableDebugCommand=False, protocol=2, + tlsCertFile=None, tlsKeyFile=None, tlsCaCertFile=None, clusterNodeTimeout=None, clusterBusPortProtectedMode=None, tlsPassphrase=None, enableDebugCommand=False, protocol=2, terminateRetries=None, terminateRetrySecs=None, enableProtectedConfigs=False, enableModuleCommand=False, loglevel=None, redisConfigFile=None, dualTLS=False, startupGraceSecs=0.1 ): @@ -96,6 +63,11 @@ def __init__(self, redisBinaryPath, port=6379, modulePath=None, moduleArgs=None, self.tlsKeyFile = tlsKeyFile self.tlsCaCertFile = tlsCaCertFile self.clusterNodeTimeout = clusterNodeTimeout + # None emits nothing. Set it only for a redis that has the option, as an unknown + # directive stops the server from starting: 8.12 and up, where it also defaults to + # enabled and so refuses an unauthenticated cluster bus, or one of the backports + # (8.2.10, 8.4.7, 8.6.7, 8.8.3, 8.10.2), where it defaults to disabled. + self.clusterBusPortProtectedMode = clusterBusPortProtectedMode self.tlsPassphrase = tlsPassphrase self.enableDebugCommand = enableDebugCommand self.enableModuleCommand = enableModuleCommand @@ -266,11 +238,9 @@ def createCmdArgs(self, role): '--cluster-node-timeout', '5000' if self.clusterNodeTimeout is None else str(self.clusterNodeTimeout)] if self.useTLS: cmdArgs += ['--tls-cluster', 'yes'] - elif hasClusterBusProtectedMode(self.redisBinaryPath): - # Without tls-cluster the cluster bus port is unauthenticated, - # and redis refuses to start unless that is acknowledged. The - # bus ports of a test env are local and short-lived, so waive it. - cmdArgs += ['--cluster-bus-port-protected-mode', 'no'] + if self.clusterBusPortProtectedMode is not None: + cmdArgs += ['--cluster-bus-port-protected-mode', + 'yes' if self.clusterBusPortProtectedMode in (True, 'yes') else 'no'] if self.useAof: cmdArgs += ['--appendonly', 'yes'] cmdArgs += ['--appendfilename', self._getFileName(role, '.aof')] diff --git a/tests/unit/test_redis_std.py b/tests/unit/test_redis_std.py index 35ed787..8a24e8f 100644 --- a/tests/unit/test_redis_std.py +++ b/tests/unit/test_redis_std.py @@ -3,7 +3,7 @@ import tempfile from unittest import TestCase -from RLTest.redis_std import MASTER, StandardEnv, hasClusterBusProtectedMode +from RLTest.redis_std import StandardEnv from tests.unit.test_common import REDIS_BINARY, TLS_CERT, TLS_KEY, TLS_CACERT tlsCertFile = 'fake_redis.crt' @@ -83,67 +83,6 @@ def test_has_interactive_debugger(self): std_env = StandardEnv(redisBinaryPath=REDIS_BINARY, outputFilesFormat='%s-test') assert std_env.has_interactive_debugger == None - def _fakeRedisBinary(self, name, startupOutput, version='8.8.0'): - """A stand-in redis that reports `version` and prints `startupOutput`. - - Lets the probe be tested against a binary whose version and whose actual - support for the option disagree, which is the case version numbers - cannot get right. - """ - path = os.path.join(self.test_dir, name) - with open(path, 'w') as f: - f.write('#!/bin/sh\n' - 'case "$1" in --version) echo "Redis server v=%s sha=00000000:0 bits=64"; exit 0;; esac\n' - 'echo "%s"\n' - 'exit 1\n' % (version, startupOutput)) - os.chmod(path, 0o755) - return path - - def test_has_cluster_bus_protected_mode_probes_the_binary(self): - unsupported = self._fakeRedisBinary('redis-unsupported', - 'Bad directive or wrong number of arguments') - supported = self._fakeRedisBinary('redis-supported', - 'Configured to not listen anywhere, exiting.') - assert not hasClusterBusProtectedMode(unsupported) - assert hasClusterBusProtectedMode(supported) - # Answer is cached, so removing the binary changes nothing. - os.remove(unsupported) - assert not hasClusterBusProtectedMode(unsupported) - - def test_create_cmd_args_cluster_bus_protected_mode(self): - flag = ['--cluster-bus-port-protected-mode', 'no'] - - def args(binary, **kwargs): - env = StandardEnv(redisBinaryPath=binary, outputFilesFormat='%s-test', - dbDirPath=self.test_dir, **kwargs) - return env.createCmdArgs(MASTER) - - # The option was added mid-release-line and backported, so a version - # number cannot say whether a given build has it: 8.8.0 through 8.8.2 do - # not, 8.8.3 does, and the same holds for 8.10.1 versus 8.10.2. Passing - # it to a build without it is fatal, so these must not be waived blind. - for version in ('8.8.0', '8.8.2', '8.10.1', '255.255.255'): - binary = self._fakeRedisBinary('redis-no-option-' + version, - 'Bad directive or wrong number of arguments', version) - assert flag[0] not in args(binary, clusterEnabled=True), version - - for version in ('8.8.3', '8.10.2', '255.255.255'): - binary = self._fakeRedisBinary('redis-with-option-' + version, - 'Configured to not listen anywhere, exiting.', version) - cmdArgs = args(binary, clusterEnabled=True) - assert cmdArgs[-2:] == flag, (version, cmdArgs) - - supported = self._fakeRedisBinary('redis-plain', - 'Configured to not listen anywhere, exiting.') - # Only a cluster node opens a bus port, and tls-cluster authenticates it. - assert flag[0] not in args(supported) - tlsArgs = args(supported, clusterEnabled=True, useTLS=True, - tlsCertFile=os.path.join(self.test_dir, tlsCertFile), - tlsKeyFile=os.path.join(self.test_dir, tlsKeyFile), - tlsCaCertFile=os.path.join(self.test_dir, tlsCaCertFile)) - assert flag[0] not in tlsArgs - assert '--tls-cluster' in tlsArgs - def test_create_cmd_args_default(self): std_env = StandardEnv(redisBinaryPath=REDIS_BINARY, outputFilesFormat='%s-test') role = 'master' From 75fbf913922d0e7c63adf0a90e19750582e5b64f Mon Sep 17 00:00:00 2001 From: LiranAbir Date: Wed, 23 Sep 2026 11:03:30 +0300 Subject: [PATCH 4/4] Compare envs on cluster-bus-port-protected-mode Without it in EnvCompareParams, a test asking for a different setting can be handed a reused environment whose shards were started with the previous one, and the difference is invisible: the option changes whether a node will start at all, not anything observable on a node that did start. Requested in review by gabsow. Co-Authored-By: Claude Opus 5 --- RLTest/env.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RLTest/env.py b/RLTest/env.py index 82085f0..47a4915 100644 --- a/RLTest/env.py +++ b/RLTest/env.py @@ -189,7 +189,8 @@ class Env: RTestInstance = None EnvCompareParams = ['module', 'moduleArgs', 'env', 'useSlaves', 'shardsCount', 'useAof', 'useRdbPreamble', 'forceTcp', 'enableDebugCommand', 'enableProtectedConfigs', - 'enableModuleCommand', 'protocol', 'password'] + 'enableModuleCommand', 'protocol', 'password', + 'clusterBusPortProtectedMode'] def __new__(cls, *args, **kwargs): if cls is Env and Defaults.env_class is not None: