]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
cephadm: added cephadm exec command to execute shell commands
authorShweta Bhosale <Shweta.Bhosale1@ibm.com>
Tue, 2 Dec 2025 05:07:00 +0000 (10:37 +0530)
committerShweta Bhosale <Shweta.Bhosale1@ibm.com>
Thu, 11 Jun 2026 05:10:33 +0000 (10:40 +0530)
Fixes: https://tracker.ceph.com/issues/74045
Signed-off-by: Shweta Bhosale <Shweta.Bhosale1@ibm.com>
src/cephadm/cephadm.py
src/cephadm/tests/test_cephadm.py

index ea292ebb91101160a05e24229cd9f64048155a96..d8c99403f516e31cd73efe7e4d38e80ba9a63600 100755 (executable)
@@ -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)
index 7d2402d8b9b30ed0f0001f5353638d4dd2af023a..ac2bab1f865e65627d9b6a53e205bef448052455 100644 (file)
@@ -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