VersionStatusUpdater,
)
from cephadmlib.container_lookup import infer_local_ceph_image, identify
+from cephadmlib.user_utils import setup_ssh_user
FuncT = TypeVar('FuncT', bound=Callable)
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
+
##################################
'--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)
--- /dev/null
+# 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)
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()
@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)
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