fix(macos): skip common role Linux baseline on Darwin

The common role's 'Base | *' tasks (timedatectl timezone, /etc/hostname,
hostname, /etc/hosts, ssh hardening, fail2ban, file limits, firewall) all run
with become: true against Linux-only tooling/paths and fail on macOS — the
reported timedatectl failure is just the first. Add patch_playbook_common_macos()
(post-clone, Darwin-only) that appends an ansible_os_family != 'Darwin' guard to
the whole Base block. Idempotent; verified against the real role; Linux
unchanged. Documents TC-MAC-014.
This commit is contained in:
Haitao Pan 2026-06-18 11:46:29 +00:00
parent 11701c6037
commit 6607d32920
3 changed files with 103 additions and 1 deletions

View File

@ -117,6 +117,16 @@
| **目录策略** | Linux 保持 `/etc/vault.d`、`/opt/vault/data`macOS 改用 Apple 标准 `~/Library/Application Support/vault`、`~/Library/Application Support/vault/data`;二进制路径 macOS 取 `/opt/homebrew/bin/vault`brew 安装位置),免去需 sudo 的 `/usr/local/bin` 软链依赖 |
| **修复方案** | role 位于独立 playbooks 仓库,无法从本仓库直接提交;沿用脚本既有的“克隆后打补丁”机制(参见 `patch_playbook_user_systemd`),在 `setup-ai-workspace-all-in-one.sh` 新增 `patch_playbook_vault_macos()`,仅在 Darwin 下对克隆出的 vault 角色:①给目录创建任务追加 `ansible_os_family != 'Darwin'` 守卫;②把 `vault_config_dir`/`vault_data_dir`/`vault_binary_path` 改为按 OS 的三元表达式;③在 `macos.yml` 前置创建用户属主的数据目录(含 launchd 日志目录 `~/.local/state/xworkspace`)。该补丁对 `curl \| bash` 与本地执行两条路径均生效,幂等,且不改动 Linux 行为 |
## TC-MAC-014: common 角色 Linux 基线timedatectl 等)在 macOS 失败
| 项目 | 内容 |
|------|------|
| **触发文件** | `roles/vhosts/common/tasks/main.yml` |
| **触发报错** | `TASK [common : Base | set timezone]``[Errno 2] No such file or directory: b'timedatectl'`macOS 无 systemd 的 `timedatectl` |
| **根因** | `common` 角色的 `Base | *` 系列任务是 Linux 服务器基线:`timedatectl` 设时区、改写 `/etc/hostname`、`/etc/hosts`、设主机名、加固 SSH、配置 fail2ban、调文件句柄上限、放行防火墙端口。全部 `become: true` 且依赖 Linux 专有工具/路径,在 macOS`become=false`)下会逐条失败,`set timezone` 只是第一个 |
| **修复方案** | 经评估这些基线对 macOS 本机开发部署既不适用也无权限执行,故在 `setup-ai-workspace-all-in-one.sh` 新增 `patch_playbook_common_macos()`(同样走克隆后打补丁),仅在 Darwin 下为整个 `Base | *` 块追加 `ansible_os_family != 'Darwin'` 守卫(共 9 处7 个任务追加 `when`2 个已有 `when` 列表追加该条件)。`import_tasks` 的 `when` 会传播到子任务,因此 ssh 加固/fail2ban/limits/firewall 子任务一并跳过。幂等、YAML 合法、Linux 行为不变 |
| **备注** | 用户仅点名 `set timezone`,但其后的 Base 任务会以相同原因连环失败,故一并守卫以避免逐个往返 |
---
## 修复维度总结
@ -127,7 +137,8 @@
| 权限收缩 (become: false) | TC-002, TC-006, TC-007, TC-008, TC-009 |
| 用户组适配 (staff vs ubuntu) | TC-003, TC-010 |
| 目录路径降级 ($HOME vs /home/ubuntu, /opt, /etc) | TC-004, TC-006, TC-009, TC-010, TC-012, TC-013 |
| 克隆后补丁注入 (post-clone patch) | TC-013 |
| 克隆后补丁注入 (post-clone patch) | TC-013, TC-014 |
| Linux 基线整体跳过 (skip Linux baseline on Darwin) | TC-014 |
| 包管理器绕过 (skip apt on Darwin) | TC-008, TC-010 |
| 模板变量解耦 (remove nvm/nodejs_version) | TC-005 |
| 路径空格兼容 (argv vs string) | TC-011 |

View File

@ -117,3 +117,21 @@ failed (item=/opt/vault/data): Permission denied: b'/opt/vault'
**验证**`bash -n` 通过;用真实 vault 角色副本跑补丁YAML 合法、Darwin 守卫计数 5→6、二次执行幂等、Linux 渲染仍为原系统路径、macOS 渲染为 Apple 标准路径。
**注意(运行方式)**:上一轮 `bash scripts/setup-...sh | bash -` 的管道是错误用法(会把脚本日志再喂给第二个 bash。本地执行应去掉管道`bash scripts/setup-ai-workspace-all-in-one.sh`。
---
## 9. 续common 角色 Linux 基线TC-MAC-01419:41
**进展**vault 修复后部署推进到 161 个任务,新阻塞点为 `common : Base | set timezone`
```
fatal: timedatectl set-timezone Asia/Shanghai -> [Errno 2] No such file or directory: b'timedatectl'
```
**根因**`common` 的 `Base | *` 是 Linux 服务器基线时区、hostname、/etc/hosts、SSH 加固、fail2ban、limits、防火墙全部 `become: true` + Linux 专有工具/路径。`set timezone` 只是第一个,其余会连环失败。
**解法**:新增 `patch_playbook_common_macos()`(仍走克隆后打补丁),仅在 Darwin 下给整个 Base 块加 `ansible_os_family != 'Darwin'` 守卫9 处)。`import_tasks` 的 `when` 自动传播到子任务,故 ssh/fail2ban/limits/firewall 一并跳过。
**验证**`bash -n` 通过;用真实 `common/tasks/main.yml` 跑补丁YAML 合法、守卫计数 9、二次执行幂等、Linux 段落Debian/RedHat/addon不变。
**说明**:用户仅点名 timezone但为避免逐个往返已一并守卫同源会失败的 Base 任务。

View File

@ -1177,6 +1177,78 @@ macos_path.write_text(macos_text)
PY
}
# The common role's "Base | *" tasks configure a Linux server: set timezone via
# timedatectl, rewrite /etc/hostname + /etc/hosts, set the hostname, harden ssh,
# configure fail2ban, raise file limits and open firewall ports. All of them run
# with become: true and target Linux-only tooling/paths, so they fail on macOS
# (e.g. timedatectl is absent). Patch the cloned role to skip the entire Base
# baseline on Darwin. Linux is untouched.
patch_playbook_common_macos() {
local main_file="roles/vhosts/common/tasks/main.yml"
[ -f "$main_file" ] || return 0
python3 - <<'PY'
from pathlib import Path
path = Path("roles/vhosts/common/tasks/main.yml")
text = path.read_text()
guard = " when: ansible_os_family != 'Darwin'\n"
# Tasks that end with a trailing attribute and have no `when:` yet -> append guard.
append_blocks = [
('- name: Base | set timezone\n'
' ansible.builtin.command: "timedatectl set-timezone Asia/Shanghai"\n'
' changed_when: false\n'
' become: true\n'),
('- name: Base | render /etc/hostname\n'
' ansible.builtin.template:\n'
' src: templates/hostname.j2\n'
' dest: /etc/hostname\n'
' owner: root\n'
' group: root\n'
' mode: "0644"\n'
' become: true\n'),
('- name: Base | set hostname\n'
' ansible.builtin.hostname:\n'
' name: "{{ inventory_hostname }}"\n'
' become: true\n'),
('- name: Base | update /etc/hosts\n'
' ansible.builtin.template:\n'
' src: templates/hosts\n'
' dest: /etc/hosts\n'
' owner: root\n'
' group: root\n'
' mode: "0644"\n'
' become: true\n'),
('- name: Base | harden ssh\n'
' ansible.builtin.script: files/secure_ssh.sh\n'
' become: true\n'),
('- name: Base | harden ssh config\n'
' ansible.builtin.import_tasks: harden_ssh.yml\n'
' tags: [ssh, security]\n'),
('- name: Base | configure fail2ban\n'
' ansible.builtin.import_tasks: fail2ban.yml\n'
' tags: [fail2ban, security]\n'),
]
for block in append_blocks:
if block in text and (block + guard) not in text:
text = text.replace(block, block + guard, 1)
# Tasks that already have a `when:` list -> add the Darwin condition to it.
when_blocks = [
(' when:\n'
' - common_security_limits.enabled | default(true) | bool\n'),
(' when:\n'
' - common_firewall.enabled | default(true) | bool\n'),
]
extra = " - ansible_os_family != 'Darwin'\n"
for block in when_blocks:
if block in text and (block + extra) not in text:
text = text.replace(block, block + extra, 1)
path.write_text(text)
PY
}
ensure_core_skills_source() {
if [ "${AI_WORKSPACE_PREFETCH_COMPLETED:-false}" = "true" ] &&
[ -d "$XWORKSPACE_CORE_SKILLS_DIR/skills" ]; then
@ -1958,6 +2030,7 @@ fi
patch_playbook_user_systemd
if [ "$(detect_os)" = "darwin" ]; then
patch_playbook_vault_macos
patch_playbook_common_macos
fi
prefetch_independent_sources
ensure_core_skills_source