Skip to content

Commit 875df85

Browse files
authored
[crmsh-4.6] Fix: sh: Avoid unnecessary sudo calls in ClusterShell SSH commands (bsc#1229416) (#1965)
The `subprocess_run_without_input` method in `ClusterShell` was unconditionally including `sudo -H -u ${user}` in its SSH command construction, even when the target user is the same as the remote user. This commit modifies the logic to conditionally include `sudo -H -u ${user}` only when the target `user` is different from the `remote_user`. This prevents unnecessary `sudo` calls, which is important in environments where `sudo` might not be installed or configured, and improves the robustness of SSH command execution.
2 parents 52a1e2b + 91a961d commit 875df85

File tree

7 files changed

+89
-37
lines changed

7 files changed

+89
-37
lines changed

crmsh/bootstrap.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -182,29 +182,45 @@ def initialize_user(self):
182182
"""
183183
users_of_specified_hosts: 'not_specified', 'specified', 'no_hosts'
184184
"""
185-
if self.cluster_node is None and self.user_at_node_list is None:
186-
users_of_specified_hosts = 'no_hosts'
187-
elif self.cluster_node is not None and '@' in self.cluster_node:
188-
users_of_specified_hosts = 'specified'
189-
elif self.user_at_node_list is not None and any('@' in x for x in self.user_at_node_list):
190-
users_of_specified_hosts = 'specified'
191-
else:
192-
users_of_specified_hosts = 'not_specified'
193185
sudoer = userdir.get_sudoer()
194-
has_sudoer = sudoer is not None
195-
if users_of_specified_hosts == 'specified':
196-
if has_sudoer:
197-
self.current_user = sudoer
186+
if self.cluster_node is not None:
187+
parts = self.cluster_node.split('@', 1)
188+
if len(parts) == 2:
189+
user, host = parts
190+
cluster_node_user = user
198191
else:
192+
cluster_node_user = 'root'
193+
if cluster_node_user == 'root':
194+
assert userdir.getuser() == 'root'
195+
self.current_user = 'root'
196+
elif sudoer is None:
199197
utils.fatal("Unsupported config: local node is using root and remote nodes is using non-root users.")
200-
elif users_of_specified_hosts == 'not_specified':
201-
assert userdir.getuser() == 'root'
202-
self.current_user = 'root'
203-
elif users_of_specified_hosts == 'no_hosts':
198+
else:
199+
self.current_user = sudoer
200+
elif self.user_at_node_list:
201+
has_root = False
202+
has_non_root = False
203+
for item in self.user_at_node_list:
204+
parts = item.split('@', 1)
205+
if len(parts) == 2:
206+
user, host = parts
207+
has_root = has_root or user == 'root'
208+
has_non_root = has_non_root or user != 'root'
209+
else:
210+
has_root = True
211+
if has_root and has_non_root:
212+
utils.fatal("Unsupported config: mixing root and non-root users in a cluster.")
213+
elif has_root:
214+
assert userdir.getuser() == 'root'
215+
self.current_user = 'root'
216+
else:
217+
if sudoer is None:
218+
utils.fatal("Unsupported config: local node is using root and remote nodes is using non-root users.")
219+
else:
220+
self.current_user = sudoer
221+
else:
204222
assert userdir.getuser() == 'root'
205223
self.current_user = 'root'
206-
else:
207-
raise AssertionError('Bad parameter user_of_specified_hosts: {}'.format(users_of_specified_hosts))
208224

209225
def _validate_sbd_option(self):
210226
"""

crmsh/prun/prun.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
_DEFAULT_CONCURRENCY = 32
1414

15-
_SUDO_SFTP_SERVER = 'sudo PATH=/usr/lib/ssh:/usr/lib/openssh:/usr/libexec/ssh:/usr/libexec/openssh /bin/sh -c "exec sftp-server"'
15+
_SUDO_SFTP_SERVER_OPTION = '-s \'sudo PATH=/usr/lib/ssh:/usr/lib/openssh:/usr/libexec/ssh:/usr/libexec/openssh /bin/sh -c "exec sftp-server"\''
1616

1717

1818
class ProcessResult:
@@ -117,7 +117,10 @@ def _build_run_task(remote: str, cmdline: str) -> Task:
117117
)
118118
else:
119119
local_sudoer, remote_sudoer = UserOfHost.instance().user_pair_for_ssh(remote)
120-
shell = 'ssh {} {}@{} sudo -H /bin/sh'.format(crmsh.constants.SSH_OPTION, remote_sudoer, remote)
120+
if remote_sudoer == "root":
121+
shell = 'ssh {} {}@{} /bin/bash'.format(crmsh.constants.SSH_OPTION, remote_sudoer, remote)
122+
else:
123+
shell = 'ssh {} {}@{} sudo -H /bin/bash'.format(crmsh.constants.SSH_OPTION, remote_sudoer, remote)
121124
if local_sudoer == crmsh.userdir.getuser():
122125
args = ['/bin/sh', '-c', shell]
123126
elif os.geteuid() == 0:
@@ -188,10 +191,10 @@ def pcopy_to_remote(
188191

189192
def _build_copy_task(ssh: str, script: str, host: str):
190193
_, remote_sudoer = UserOfHost.instance().user_pair_for_ssh(host)
191-
cmd = "sftp {} {} -o BatchMode=yes -s '{}' -b - {}@{}".format(
194+
cmd = "sftp {} {} -o BatchMode=yes {} -b - {}@{}".format(
192195
ssh,
193196
crmsh.constants.SSH_OPTION,
194-
_SUDO_SFTP_SERVER,
197+
_SUDO_SFTP_SERVER_OPTION if remote_sudoer != 'root' else '',
195198
remote_sudoer, _enclose_inet6_addr(host),
196199
)
197200
return Task(
@@ -254,10 +257,10 @@ def pfetch_from_remote(
254257

255258
def _build_fetch_task( ssh: str, host: str, src: str, dst: str, flags: str) -> Task:
256259
_, remote_sudoer = UserOfHost.instance().user_pair_for_ssh(host)
257-
cmd = "sftp {} {} -o BatchMode=yes -s '{}' -b - {}@{}".format(
260+
cmd = "sftp {} {} -o BatchMode=yes {} -b - {}@{}".format(
258261
ssh,
259262
crmsh.constants.SSH_OPTION,
260-
_SUDO_SFTP_SERVER,
263+
_SUDO_SFTP_SERVER_OPTION if remote_sudoer != 'root' else '',
261264
remote_sudoer, _enclose_inet6_addr(host),
262265
)
263266
os.makedirs(f"{dst}/{host}", exist_ok=True)

crmsh/prun/runner.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ def __init__(
3636
# Caller can pass arbitrary data to context, it is kept untouched.
3737
self.context = context
3838

39+
def __repr__(self):
40+
return f"TaskArgumentsEq({self.args}, {self.input}, {self.stdout_config}, {self.stderr_config}, {self.context}"
3941

4042
class Runner:
4143
def __init__(self, concurrency):

crmsh/sh.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -339,17 +339,16 @@ def subprocess_run_without_input(self, host: typing.Optional[str], user: typing.
339339
if user is None:
340340
user = 'root'
341341
local_user, remote_user = self.user_of_host.user_pair_for_ssh(host)
342+
ssh_agent_options = '-A' if self.forward_ssh_agent else ''
343+
if user == remote_user:
344+
shell = f'ssh {ssh_agent_options} {constants.SSH_OPTION} -o BatchMode=yes {remote_user}@{host} /bin/bash'
345+
else:
346+
preserve_env_options = '--preserve-env=SSH_AUTH_SOCK' if self.forward_ssh_agent else ''
347+
shell = f'ssh {ssh_agent_options} {constants.SSH_OPTION} -o BatchMode=yes {remote_user}@{host}' \
348+
f' sudo -H -u {user} {preserve_env_options} /bin/bash'
342349
result = self.local_shell.su_subprocess_run(
343350
local_user,
344-
'ssh {} {} -o BatchMode=yes {}@{} sudo -H -u {} {} /bin/sh'.format(
345-
'-A' if self.forward_ssh_agent else '',
346-
constants.SSH_OPTION,
347-
remote_user,
348-
host,
349-
user,
350-
'--preserve-env=SSH_AUTH_SOCK' if self.forward_ssh_agent else '',
351-
constants.SSH_OPTION,
352-
),
351+
shell,
353352
input=cmd.encode('utf-8'),
354353
start_new_session=True,
355354
**kwargs,

test/features/qdevice_setup_remove.feature

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,15 @@ Feature: corosync qdevice/qnetd setup/remove process
148148
Then Service "corosync-qdevice" is "started" on "hanode1"
149149
And Service "corosync-qdevice" is "started" on "hanode2"
150150

151+
@skip_non_root
152+
@clean
153+
Scenario: Initialize qnetd without "sudo" installed (bsc#1229416)
154+
When Run "mv /usr/bin/sudo /usr/bin/sudo.bak" on "qnetd-node"
155+
When Run "crm cluster init -y --qnetd-hostname root@qnetd-node" on "hanode1"
156+
Then Service "corosync-qdevice" is "started" on "hanode1"
157+
Then Service "corosync-qnetd" is "started" on "qnetd-node"
158+
When Run "mv /usr/bin/sudo.bak /usr/bin/sudo" on "qnetd-node"
159+
151160
@skip_non_root
152161
@clean
153162
Scenario: Missing crm/crm.conf (bsc#1209193)

test/unittests/test_bootstrap.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,10 @@ def test_initialize_user_cluster_node_with_user_without_sudoer(self, mock_getuse
324324
context.user_at_node_list = None
325325
with self.assertRaises(ValueError):
326326
context.initialize_user()
327+
context.cluster_node = 'root@node1'
328+
context.user_at_node_list = None
329+
context.initialize_user()
330+
self.assertEqual('root', context.current_user)
327331

328332
@mock.patch('crmsh.userdir.get_sudoer')
329333
@mock.patch('crmsh.userdir.getuser')
@@ -346,6 +350,10 @@ def test_initialize_user_cluster_node_with_user_with_sudoer(self, mock_getuser:
346350
context.user_at_node_list = None
347351
context.initialize_user()
348352
self.assertEqual('bob', context.current_user)
353+
context.cluster_node = 'root@node1'
354+
context.user_at_node_list = None
355+
context.initialize_user()
356+
self.assertEqual('root', context.current_user)
349357

350358
@mock.patch('crmsh.userdir.get_sudoer')
351359
@mock.patch('crmsh.userdir.getuser')
@@ -368,6 +376,10 @@ def test_initialize_user_node_list_with_user_without_sudoer(self, mock_getuser:
368376
context.cluster_node = None
369377
with self.assertRaises(ValueError):
370378
context.initialize_user()
379+
context.user_at_node_list = ['root@node1', 'root@node2']
380+
context.cluster_node = None
381+
context.initialize_user()
382+
self.assertEqual('root', context.current_user)
371383

372384
@mock.patch('crmsh.userdir.get_sudoer')
373385
@mock.patch('crmsh.userdir.getuser')
@@ -386,10 +398,18 @@ def test_initialize_user_node_list_with_user_with_sudoer(self, mock_getuser: moc
386398
mock_getuser.return_value = 'root'
387399
mock_get_sudoer.return_value = 'bob'
388400
context = bootstrap.Context()
401+
context.user_at_node_list = ['alice@node1', 'root@node2']
402+
context.cluster_node = None
403+
with self.assertRaises(ValueError):
404+
context.initialize_user()
389405
context.user_at_node_list = ['alice@node1', 'alice@node2']
390406
context.cluster_node = None
391407
context.initialize_user()
392408
self.assertEqual('bob', context.current_user)
409+
context.user_at_node_list = ['root@node1', 'root@node2']
410+
context.cluster_node = None
411+
context.initialize_user()
412+
self.assertEqual('root', context.current_user)
393413

394414

395415
class TestBootstrap(unittest.TestCase):

test/unittests/test_prun.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ def test_prun(
4040
])
4141
mock_runner_add_task.assert_has_calls([
4242
mock.call(TaskArgumentsEq(
43-
['su', 'alice', '--login', '-c', 'ssh {} bob@host1 sudo -H /bin/sh'.format(crmsh.constants.SSH_OPTION)],
43+
['su', 'alice', '--login', '-c', 'ssh {} bob@host1 sudo -H /bin/bash'.format(crmsh.constants.SSH_OPTION)],
4444
b'foo',
4545
stdout=crmsh.prun.runner.Task.Capture,
4646
stderr=crmsh.prun.runner.Task.Capture,
4747
context={"host": 'host1', "ssh_user": 'bob'},
4848
)),
4949
mock.call(TaskArgumentsEq(
50-
['su', 'alice', '--login', '-c', 'ssh {} bob@host2 sudo -H /bin/sh'.format(crmsh.constants.SSH_OPTION)],
50+
['su', 'alice', '--login', '-c', 'ssh {} bob@host2 sudo -H /bin/bash'.format(crmsh.constants.SSH_OPTION)],
5151
b'bar',
5252
stdout=crmsh.prun.runner.Task.Capture,
5353
stderr=crmsh.prun.runner.Task.Capture,
@@ -90,14 +90,14 @@ def test_prun_root(
9090
])
9191
mock_runner_add_task.assert_has_calls([
9292
mock.call(TaskArgumentsEq(
93-
['/bin/sh', '-c', 'ssh {} root@host1 sudo -H /bin/sh'.format(crmsh.constants.SSH_OPTION)],
93+
['/bin/sh', '-c', 'ssh {} root@host1 /bin/bash'.format(crmsh.constants.SSH_OPTION)],
9494
b'foo',
9595
stdout=crmsh.prun.runner.Task.Capture,
9696
stderr=crmsh.prun.runner.Task.Capture,
9797
context={"host": 'host1', "ssh_user": 'root'},
9898
)),
9999
mock.call(TaskArgumentsEq(
100-
['/bin/sh', '-c', 'ssh {} root@host2 sudo -H /bin/sh'.format(crmsh.constants.SSH_OPTION)],
100+
['/bin/sh', '-c', 'ssh {} root@host2 /bin/bash'.format(crmsh.constants.SSH_OPTION)],
101101
b'bar',
102102
stdout=crmsh.prun.runner.Task.Capture,
103103
stderr=crmsh.prun.runner.Task.Capture,
@@ -155,3 +155,6 @@ def __eq__(self, other):
155155
and self.stdout_config == other.stdout_config \
156156
and self.stderr_config == other.stderr_config \
157157
and self.context == other.context
158+
159+
def __repr__(self):
160+
return f"TaskArgumentsEq({self.args}, {self.input}, {self.stdout_config}, {self.stderr_config}, {self.context}"

0 commit comments

Comments
 (0)