- 运维
- 配置管理
- 后端
【免费下载链接】salt
Software to automate the management and configuration of infrastructure and applications at scale.
导读
本文讲解 Salt 在 macOS 平台上的 Homebrew 软件包管理模块mac_brew_pkg中一次典型缺陷的修复:homebrew_prefix()探测函数会在每次调用时触发su密码提示(或在非 TTY 环境下报su: Sorry),罪魁祸首是cmdmod.run在 macOS 上对runas参数的无条件su -l包装。读完本文,你将理解 Salt 执行模块在 macOS 上的提权实现细节、该缺陷的根因与修复思路,以及配套回归测试如何锁定修复行为。
背景:homebrew_prefix()是 mac_brew_pkg 模块的基石
mac_brew_pkg是 Salt 在 macOS 上管理 Homebrew 软件包的执行模块,其虚拟名称为pkg,仅当osgrain 为MacOS且能找到brew二进制时才加载(见 salt/modules/mac_brew_pkg.py 中的__virtual__())。而homebrew_prefix()用于探测 Homebrew 的安装前缀(Intel Mac 通常为/usr/local,Apple Silicon Mac 通常为/opt/homebrew),是模块内众多功能的依赖基础,例如:
_homebrew_bin()通过它拼接出brew二进制完整路径(<prefix>/bin/brew),见 salt/modules/mac_brew_pkg.py;_call_brew()基于该路径构造实际的brew命令调用(salt/modules/mac_brew_pkg.py);__virtual__()中也会通过_homebrew_bin()校验brew是否存在。
因此,homebrew_prefix()几乎在模块每次被实际使用时都会被调用,任何低效或引发交互式提示的实现都会被放大为高频故障。
问题根因:cmdmod.run在 macOS 上对runas的无条件su -l包装
修复前的homebrew_prefix()会无条件地把 brew 二进制所有者作为runas传给cmdmod.run,以brew --prefix探测前缀。问题在于cmdmod.run在 macOS 平台上的特殊处理逻辑(见 salt/modules/cmdmod.py):
if runas and salt.utils.platform.is_darwin(): # ... 组装为 login shell 命令 cmd = f"su -l {_cmd_quote(runas)} -c {_cmd_quote(cmd)}" # Set runas to None, because if you try to run `su -l` after changing # user, su will prompt for the password of the user and cause salt to # hang. runas = None从源码结构看,macOS 上任何非空runas都会导致命令被包装成su -l <user> -c ...。当 brew 二进制的所有者恰好就是当前进程用户(例如非 root 用户通过 salt-ssh 管理自己所有的 Homebrew 环境)时,这条su -l依旧会被执行,从而:
- 在交互式终端触发
su密码提示; - 在非 TTY 调用(如 salt-ssh、自动化任务)中直接报
su: Sorry并失败。
由于homebrew_prefix()是高频基础调用,该问题在每次调用时都会复现,属于典型的"探测函数反而导致命令无法执行"的回归缺陷。
修复方案:仅在所有者不同时传递runas
修复后的homebrew_prefix()不再无条件传递runas,而是在调用cmdmod.run之前先比较 brew 二进制所有者与当前进程用户(见 salt/modules/mac_brew_pkg.py):
# Try brew --prefix otherwise try: log.debug("Trying to find homebrew prefix by running 'brew --prefix'") brew = _homebrew_os_bin() if brew is not None: # Check if the found brew command is the right one import salt.modules.cmdmod import salt.modules.file runas = salt.modules.file.get_user(brew) # Only pass runas when the brew binary is owned by a different # user than the current process. On macOS, ``cmdmod.run`` with a # truthy ``runas`` wraps the command in ``su -l <user> -c ...`` # unconditionally, which triggers a password prompt (or # ``su: Sorry`` on non-tty invocations) even when the target user # is the current user. See #69027. try: if runas == getpass.getuser(): runas = None except Exception: # pylint: disable=broad-except # getpass.getuser() can raise on unusual environments (e.g. # empty passwd db); fall back to sending runas as-is. pass ret = salt.modules.cmdmod.run( "brew --prefix", runas=runas, output_loglevel="trace", raise_err=True ) return ret except CommandExecutionError as exc: log.debug( "Unable to find homebrew prefix by running 'brew --prefix'. Error: %s", exc ) return None关键逻辑拆解:
- 所有者探测:通过
salt.modules.file.get_user(brew)获取 brew 二进制的所有者用户名(get_user基于文件stats返回user字段,见 salt/modules/file.py); - 条件归零:用
getpass.getuser()取得当前进程用户,若二者相同则将runas置为None,从而在cmdmod.run的 macOS 分支中跳过su -l包装(因为该分支要求runas为真值); - 异常兜底:
getpass.getuser()在异常环境(如空的 passwd 数据库)可能抛异常,此时按原样透传runas,保证功能不因兜底失败而中断; - 行为保持:当 brew 二进制所有者确实与当前用户不同时,
runas依然被传递,su -l包装照常生效——修复只消除"对自己执行 su"这一无意义且有害的场景,不改变正常提权路径。
同时,函数整体保留了原有的回退语义:优先使用HOMEBREW_PREFIX环境变量(salt/modules/mac_brew_pkg.py),无法执行brew --prefix时捕获CommandExecutionError并返回None。
回归测试:两条测试锁定修复行为
仓库中的单元测试文件 tests/pytests/unit/modules/test_mac_brew_pkg.py 针对该修复补充了两条成对的回归测试:
测试一:brew 所有者等于当前用户时不得传递runas(test_homebrew_prefix_no_su_when_brew_owner_is_current_user,tests/pytests/unit/modules/test_mac_brew_pkg.py)
该测试注释明确把场景描述为 #69027 回归测试:修复前homebrew_prefix()无条件把runas=<brew 二进制所有者>传给cmdmod.run,而 macOS 上即使目标用户就是当前用户也会包装成su -l,导致每次 salt-ssh 非 root 调用(用户自持 Homebrew)都会触发密码提示或su: Sorry。测试断言:
_, kwargs = run_mock.call_args assert kwargs.get("runas") is None, ( "homebrew_prefix() must not pass runas=<current user> to cmdmod.run; " "on macOS this wraps the probe in `su -l` and triggers a password " "prompt (issue #69027)" )测试二:brew 所有者是其他用户时仍须传递runas(test_homebrew_prefix_still_uses_runas_when_brew_owned_by_other_user,tests/pytests/unit/modules/test_mac_brew_pkg.py)
作为互补用例,当file.get_user返回brewowner而getpass.getuser返回someoneelse时,runas必须保持为"brewowner",确保正常的按所有者提权路径不被破坏:
_, kwargs = run_mock.call_args assert kwargs.get("runas") == "brewowner"两条测试一正一反,共同界定了修复的精确边界:只在"所有者就是当前用户"时跳过提权包装,其余场景行为不变。
修复带来的实际影响
- 消除高频交互提示:
homebrew_prefix()的每次调用不再因自我su而触发密码提示或su: Sorry错误,非 root 用户自持 Homebrew 的 salt-ssh 场景可以稳定执行; - 减少无意义系统调用:避免了为探测前缀而启动一次多余的子进程
su -l包装,缩短了pkg系列命令的响应链路; - 不影响既有权限模型:所有者在其他用户时依旧以
su -l提权,管理员模式的 Homebrew 管理行为与修复前一致。
运维建议
对于 macOS minion / salt-ssh 用户,模块头部文档还给出了规避探测开销的推荐做法:为 salt-minion 设置HOMEBREW_PREFIX环境变量(Intel 为/usr/local,Apple Silicon 为/opt/homebrew),这样homebrew_prefix()会直接命中环境变量分支,跳过brew --prefix子进程调用(见 salt/modules/mac_brew_pkg.py)。该建议在修复前后都成立,可进一步降低每次pkg调用时的探测成本。
- 运维
- 配置管理
- 后端
【免费下载链接】salt
Software to automate the management and configuration of infrastructure and applications at scale.
相关推荐
Salt salt-ssh 修复:relenv Minion 配置不再嵌入 `__master_opts__`,根治 Argument list too long
Salt salt ssh 修复:relenv Minion 配置不再嵌入 __master_opts__ ,根治 Argument list too long
运维配置管理后端Area51模型动画事件触发条件:代码示例
Area51模型动画事件触发条件:代码示例 在游戏开发中,模型动画事件(Animation Event)是连接动画序列与游戏逻辑的关键桥梁。Area51项目通过
Salt manage 状态检测修复深度解析:`manage.status`/`manage.up`/`manage.down` 不再误报无响应 minion
Salt manage 状态检测修复深度解析: manage.status / manage.up / manage.down 不再误报无响应 minion 导
运维配置管理后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考