]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/cephadm: Setup user's sudoers and public keys before setting cephadm user
authorShweta Bhosale <Shweta.Bhosale1@ibm.com>
Mon, 22 Dec 2025 10:50:56 +0000 (16:20 +0530)
committerShweta Bhosale <Shweta.Bhosale1@ibm.com>
Tue, 10 Feb 2026 05:00:37 +0000 (10:30 +0530)
Fixes: https://tracker.ceph.com/issues/74045
Signed-off-by: Shweta Bhosale <Shweta.Bhosale1@ibm.com>
src/cephadm/cephadm.py
src/cephadm/cephadmlib/user_utils.py [new file with mode: 0644]
src/pybind/mgr/cephadm/module.py

index 4e255e551656bf9e793c3bfa7916937c0730adf9..c9e699215acd50af2b6143cca91bb3e19a4c90da 100755 (executable)
@@ -203,6 +203,7 @@ from cephadmlib.listing_updaters import (
     VersionStatusUpdater,
 )
 from cephadmlib.container_lookup import infer_local_ceph_image, identify
+from cephadmlib.user_utils import setup_ssh_user
 
 
 FuncT = TypeVar('FuncT', bound=Callable)
@@ -4461,6 +4462,20 @@ def change_maintenance_mode(ctx: CephadmContext) -> str:
                     return f'success - systemd target {target} enabled and started'
         return f'success - systemd target {target} enabled and started'
 
+
+def command_setup_ssh_user(ctx: CephadmContext) -> int:
+    """
+    Setup SSH user on the local host by configuring passwordless sudo and
+    copying SSH public key to user's authorized_keys.
+    """
+    if not ctx.ssh_user:
+        raise Error('--ssh-user is required')
+    if not ctx.ssh_pub_key:
+        raise Error('--ssh-pub-key is required')
+
+    setup_ssh_user(ctx, ctx.ssh_user, ctx.ssh_pub_key)
+    return 0
+
 ##################################
 
 
@@ -5181,6 +5196,18 @@ def _get_parser():
         '--expect-hostname',
         help='Set hostname')
 
+    parser_setup_ssh_user = subparsers.add_parser(
+        'setup-ssh-user', help='setup SSH user with passwordless sudo and SSH key')
+    parser_setup_ssh_user.set_defaults(func=command_setup_ssh_user)
+    parser_setup_ssh_user.add_argument(
+        '--ssh-user',
+        required=True,
+        help='SSH user to setup')
+    parser_setup_ssh_user.add_argument(
+        '--ssh-pub-key',
+        required=True,
+        help='SSH public key to add to user authorized_keys')
+
     parser_add_repo = subparsers.add_parser(
         'add-repo', help='configure package repository')
     parser_add_repo.set_defaults(func=command_add_repo)
diff --git a/src/cephadm/cephadmlib/user_utils.py b/src/cephadm/cephadmlib/user_utils.py
new file mode 100644 (file)
index 0000000..39b21d3
--- /dev/null
@@ -0,0 +1,107 @@
+# user_utils.py - user management utility functions
+
+import logging
+import os
+import pwd
+from typing import Tuple
+
+from .call_wrappers import call, CallVerbosity
+from .context import CephadmContext
+from .exceptions import Error
+from .exe_utils import find_program
+from .ssh import authorize_ssh_key
+
+logger = logging.getLogger()
+
+
+def validate_user_exists(username: str) -> Tuple[int, int, str]:
+    """Validate that a user exists and return their uid, gid, and home directory.
+    Args:
+        username: The username to validate
+    Returns:
+        Tuple of (uid, gid, home_directory)
+    Raises:
+        Error: If the user does not exist
+    """
+    try:
+        pwd_entry = pwd.getpwnam(username)
+        return pwd_entry.pw_uid, pwd_entry.pw_gid, pwd_entry.pw_dir
+    except KeyError:
+        raise Error(
+            f'User {username} does not exist on this host. '
+            f'Please create the user first: useradd -m -s /bin/bash {username}'
+        )
+
+
+def setup_sudoers(ctx: CephadmContext, username: str) -> None:
+    """Setup passwordless sudo for a user.
+    """
+    sudoers_file = f'/etc/sudoers.d/{username}'
+    sudoers_content = f'{username} ALL=(ALL) NOPASSWD: ALL\n'
+
+    logger.info('Setting up sudoers for user %s', username)
+    try:
+        # Write sudoers file with proper permissions
+        with open(sudoers_file, 'w') as f:
+            f.write(sudoers_content)
+        os.chmod(sudoers_file, 0o440)
+        os.chown(sudoers_file, 0, 0)
+
+        # Validate sudoers syntax
+        visudo_cmd = find_program('visudo')
+        _out, _err, code = call(
+            ctx,
+            [visudo_cmd, '-c', '-f', sudoers_file],
+            verbosity=CallVerbosity.DEBUG
+        )
+        if code != 0:
+            # Clean up invalid file
+            try:
+                os.remove(sudoers_file)
+            except OSError:
+                pass
+            raise Error(f'Invalid sudoers syntax: {_err}')
+        logger.info('Successfully configured sudoers for user %s', username)
+    except Error:
+        raise
+    except Exception as e:
+        msg = f'Failed to setup sudoers for user {username}: {e}'
+        logger.exception(msg)
+        raise Error(msg)
+
+
+def setup_ssh_user(ctx: CephadmContext, username: str, ssh_pub_key: str) -> None:
+    """Setup SSH user with passwordless sudo and SSH key authorization.
+    This function performs the following operations:
+    1. Validates that the user exists on the system
+    2. Sets up passwordless sudo for the user (skipped for root)
+    3. Authorizes the SSH public key for the user
+    """
+    # Verify we're running as root
+    if os.geteuid() != 0:
+        raise Error('This operation must be run as root')
+
+    if not ssh_pub_key or ssh_pub_key.isspace():
+        raise Error('SSH public key is required and cannot be empty')
+
+    logger.info('Setting up SSH user %s on this host', username)
+
+    # Validate user exists (will raise Error if not)
+    validate_user_exists(username)
+
+    # Setup sudoers (skip for root user)
+    if username != 'root':
+        setup_sudoers(ctx, username)
+    else:
+        logger.debug('Skipping sudoers setup for root user')
+
+    # Setup SSH key using existing function from ssh.py
+    try:
+        authorize_ssh_key(ssh_pub_key, username)
+        logger.info('Successfully authorized SSH key for user %s', username)
+    except Exception as e:
+        msg = f'Failed to authorize SSH key for user {username}: {e}'
+        logger.exception(msg)
+        raise Error(msg)
+
+    logger.info('Successfully configured SSH user %s on this host', username)
index c815bbb128798dbb35511bbbc6a44448c91c22b2..6c823e3a18d4adcca5da256e2a32b9896d708c42 100644 (file)
@@ -1170,6 +1170,90 @@ class CephadmOrchestrator(orchestrator.Orchestrator, MgrModule):
 
         return True, err, ret
 
+    def _setup_user_on_host(self, host: str, user: str, ssh_pub_key: str,
+                            addr: Optional[str] = None) -> None:
+        """
+        Setup sudoers and copy SSH key by calling cephadm setup-ssh-user command.
+        User must already exist on the host.
+        For root user, only SSH key is copied (sudoers setup is skipped).
+        """
+        self.log.debug('Setting up user %s on host %s', user, host)
+        try:
+            out, err, code = self.wait_async(
+                CephadmServe(self)._run_cephadm(
+                    host,
+                    cephadmNoImage,
+                    'setup-ssh-user',
+                    ['--ssh-user', user, '--ssh-pub-key', ssh_pub_key],
+                    addr=addr,
+                    error_ok=False,
+                    no_fsid=True
+                )
+            )
+            if code != 0:
+                msg = f'Failed to setup user {user} on host {host}: {err}'
+                self.log.error(msg)
+                raise OrchestratorError(msg)
+            self.log.info('Successfully set up user %s on host %s', user, host)
+        except OrchestratorError:
+            raise
+        except Exception as e:
+            msg = f'Failed to setup user {user} on host {host}: {e}'
+            self.log.exception(msg)
+            raise OrchestratorError(msg)
+
+    def _setup_user_on_all_hosts(self, user: str) -> None:
+        """
+        Setup sudoers and copy SSH key on all hosts in the cluster.
+        For root user, only SSH key is copied (sudoers setup is skipped).
+        """
+        if not self.ssh_pub:
+            raise OrchestratorError(
+                'No SSH public key configured. '
+                'Please generate or set SSH keys first using '
+                '`ceph cephadm generate-key` or `ceph cephadm set-pub-key`.'
+            )
+
+        hosts = self.cache.get_hosts()
+        if not hosts:
+            self.log.warning('No hosts in inventory, skipping user setup')
+            return
+
+        self.log.info('Setting up user %s on %s host(s)', user, len(hosts))
+
+        @forall_hosts
+        def setup_user_on_host(host: str) -> Tuple[str, Optional[str]]:
+            """Returns (host, error_message) tuple. error_message is None on success."""
+            try:
+                assert self.ssh_pub
+                self._setup_user_on_host(host, user, self.ssh_pub)
+                return (host, None)
+            except Exception as e:
+                self.log.error('Failed to setup user %s on host %s: %s', user, host, e)
+                return (host, str(e))
+
+        results = setup_user_on_host(hosts)
+        failed_hosts = [(host, error) for host, error in results if error is not None]
+        if failed_hosts:
+            self.log.error('Failed to setup user %s on %s of %s host(s)', user, len(failed_hosts), len(hosts))
+            error_parts = [f'Failed to setup user {user} on the following hosts:']
+            for host, error in failed_hosts:
+                error_parts.append(f'  - {host}: {error}')
+            error_parts.extend([
+                f'\nPlease ensure user {user} exists on these hosts and retry:',
+                f'  1. Create user: useradd -m -s /bin/bash {user}',
+                f'  2. Retry: ceph cephadm set-user {user}',
+                '\nOr manually complete the setup:',
+                f'  1. Setup sudoers: echo "{user} ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/{user}',
+                f'  2. Set permissions: chmod 440 /etc/sudoers.d/{user}',
+                '  3. Copy SSH key: ceph cephadm get-pub-key > ~/ceph.pub',
+                f'  4. Add key: ssh-copy-id -f -i ~/ceph.pub {user}@HOST',
+                '\nAnd use --skip-pre-steps flag to skip automatic setup.'
+            ])
+            raise OrchestratorError('\n'.join(error_parts))
+
+        self.log.info('Successfully set up user %s on all %s host(s)', user, len(hosts))
+
     def _validate_and_set_ssh_val(self, what: str, new: Optional[str], old: Optional[str]) -> None:
         self.set_store(what, new)
         self.ssh._reconfig_ssh()
@@ -1333,14 +1417,30 @@ class CephadmOrchestrator(orchestrator.Orchestrator, MgrModule):
 
     @CephadmCLICommand.Read(
         'cephadm set-user')
-    def set_ssh_user(self, user: str) -> Tuple[int, str, str]:
+    def set_ssh_user(self, user: str, skip_pre_steps: bool = False) -> Tuple[int, str, str]:
         """
-        Set user for SSHing to cluster hosts, passwordless sudo will be needed for non-root users
+        Set user for SSHing to cluster hosts, passwordless sudo will be needed for non-root users.
+
+        This command will automatically setup passwordless sudo for the user and
+        copy SSH public key to the user's authorized_keys.
+        Use --skip-pre-steps if you have already manually configured the user on all hosts.
         """
         current_user = self.ssh_user
         if user == current_user:
             return 0, "value unchanged", ""
 
+        if user != 'root' and not skip_pre_steps:
+            self.log.info('Setting up SSH user %s on all cluster hosts', user)
+            try:
+                self._setup_user_on_all_hosts(user)
+            except OrchestratorError as e:
+                self.log.error('Failed to setup user %s: %s', user, e)
+                return -errno.EINVAL, '', str(e)
+            except Exception as e:
+                msg = f'Failed to setup user {user} on all hosts: {e}'
+                self.log.exception(msg)
+                return -errno.EINVAL, '', msg
+
         self._validate_and_set_ssh_val('ssh_user', user, current_user)
         current_ssh_config = self._get_ssh_config()
         new_ssh_config = re.sub(r"(\s{2}User\s)(.*)", r"\1" + user, current_ssh_config.stdout)
@@ -1919,6 +2019,7 @@ Then run the following:
             self.update_maintenance_healthcheck()
         self.event.set()  # refresh stray health check
         self.log.info('Added host %s' % spec.hostname)
+
         return "Added host '{}' with addr '{}'".format(spec.hostname, spec.addr)
 
     @handle_orch_error