From: Shweta Bhosale Date: Tue, 2 Dec 2025 05:07:00 +0000 (+0530) Subject: cephadm: added cephadm exec command to execute shell commands X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=79924f8fc91ff01aedacbcd5f5e14c89b9c38722;p=ceph.git cephadm: added cephadm exec command to execute shell commands Fixes: https://tracker.ceph.com/issues/74045 Signed-off-by: Shweta Bhosale --- diff --git a/src/cephadm/cephadm.py b/src/cephadm/cephadm.py index ea292ebb911..d8c99403f51 100755 --- a/src/cephadm/cephadm.py +++ b/src/cephadm/cephadm.py @@ -74,6 +74,7 @@ from cephadmlib.context_getters import ( from cephadmlib.exceptions import ( ClusterAlreadyExists, Error, + TimeoutExpired, UnauthorizedRegistryError, DaemonStartException, ) @@ -3565,6 +3566,41 @@ def command_logs(ctx: CephadmContext) -> None: ################################## +def command_exec(ctx: CephadmContext) -> int: + """ + Execute a shell command on the host + Return Codes: + - `0`: Command executed successfully + - `124`: Command timed out + - `1`: Error during execution + - Other: Return code from the executed command + """ + if not ctx.command: + raise Error('No command provided to execute') + cmd = ctx.command + logger.debug('Executing command: %s' % ' '.join(cmd)) + try: + stdout, stderr, returncode = call( + ctx, + cmd, + verbosity=CallVerbosity.SILENT, + timeout=ctx.timeout + ) + if stdout: + sys.stdout.write(stdout) + if stderr: + sys.stderr.write(stderr) + return returncode + except TimeoutExpired: + logger.exception('Command timed out after %s seconds' % ctx.timeout) + return 124 + except Exception as e: + logger.exception('Error executing command: %s' % str(e)) + return 1 + +################################## + + def command_list_networks(ctx): # type: (CephadmContext) -> None r = list_networks(ctx) @@ -5299,6 +5335,16 @@ def _get_parser(): 'command', nargs='*', help='additional journalctl args') + parser_exec = subparsers.add_parser( + 'exec', help='execute a shell command on the host') + parser_exec.set_defaults(func=command_exec) + parser_exec.add_argument( + '--command', nargs=argparse.REMAINDER, + help='command to execute') + parser_exec.add_argument( + '--fsid', + help='cluster FSID') + parser_bootstrap = subparsers.add_parser( 'bootstrap', help='bootstrap a cluster (mon + mgr daemons)') parser_bootstrap.set_defaults(func=command_bootstrap) diff --git a/src/cephadm/tests/test_cephadm.py b/src/cephadm/tests/test_cephadm.py index 7d2402d8b9b..ac2bab1f865 100644 --- a/src/cephadm/tests/test_cephadm.py +++ b/src/cephadm/tests/test_cephadm.py @@ -3274,3 +3274,60 @@ class TestRmClusterConfigCleanup(fake_filesystem_unittest.TestCase): assert os.path.exists('/etc/ceph/ceph.conf') assert os.path.isdir('/etc/ceph/ceph.conf') + + +class TestExec(object): + """Test cases for the 'cephadm exec' command""" + + @mock.patch.object(_cephadm, 'call') + @mock.patch('cephadm.logger') + def test_exec_non_zero_exit(self, _logger, mock_call): + """Test command execution with non-zero exit code""" + mock_call.return_value = ('', 'command not found\n', 127) + + cmd = ['exec', '--command', 'nonexistent_command'] + with with_cephadm_ctx(cmd, mock_cephadm_call_fn=False) as ctx: + ctx.command = ['nonexistent_command'] + ctx.timeout = 300 + retval = _cephadm.command_exec(ctx) + assert retval == 127 + + @mock.patch('subprocess.run') + @mock.patch('cephadm.logger') + def test_exec_empty_command_list(self, _logger, mock_run): + """Test command_exec with empty command list""" + cmd = ['exec', '--command'] + with with_cephadm_ctx(cmd) as ctx: + ctx.command = [] + ctx.timeout = 300 + with pytest.raises(_cephadm.Error, match='No command provided to execute'): + _cephadm.command_exec(ctx) + + @mock.patch.object(_cephadm, 'call') + @mock.patch('cephadm.logger') + @mock.patch('sys.stdout') + @mock.patch('sys.stderr') + def test_exec_with_stdout_and_stderr(self, mock_stderr, mock_stdout, _logger, mock_call): + """Test command execution with both stdout and stderr""" + mock_call.return_value = ('Standard output\n', 'Standard error\n', 2) + + cmd = ['exec', '--command', 'test_command'] + with with_cephadm_ctx(cmd, mock_cephadm_call_fn=False) as ctx: + ctx.command = ['test_command'] + ctx.timeout = 300 + retval = _cephadm.command_exec(ctx) + assert retval == 2 + mock_stdout.write.assert_called_once_with('Standard output\n') + mock_stderr.write.assert_called_once_with('Standard error\n') + + @mock.patch.object(_cephadm, 'call') + @mock.patch('cephadm.logger') + def test_exec_general_exception(self, _logger, mock_call): + """Test command execution with general exception""" + mock_call.side_effect = Exception('Unexpected error') + cmd = ['exec', '--command', 'test'] + with with_cephadm_ctx(cmd, mock_cephadm_call_fn=False) as ctx: + ctx.command = ['test'] + ctx.timeout = 300 + retval = _cephadm.command_exec(ctx) + assert retval == 1