From: Shweta Bhosale Date: Mon, 22 Dec 2025 10:50:56 +0000 (+0530) Subject: mgr/cephadm: Setup user's sudoers and public keys before setting cephadm user X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=c16fa47662523cf2f67339a7b9f3d4efcdb33ea6;p=ceph-ci.git mgr/cephadm: Setup user's sudoers and public keys before setting cephadm user 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 4e255e55165..c9e699215ac 100755 --- a/src/cephadm/cephadm.py +++ b/src/cephadm/cephadm.py @@ -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 index 00000000000..39b21d3c44f --- /dev/null +++ b/src/cephadm/cephadmlib/user_utils.py @@ -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) diff --git a/src/pybind/mgr/cephadm/module.py b/src/pybind/mgr/cephadm/module.py index c815bbb1287..6c823e3a18d 100644 --- a/src/pybind/mgr/cephadm/module.py +++ b/src/pybind/mgr/cephadm/module.py @@ -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