from cephadmlib.exceptions import (
ClusterAlreadyExists,
Error,
+ TimeoutExpired,
UnauthorizedRegistryError,
DaemonStartException,
)
##################################
+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)
'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)
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