refactor(deploy): extract python playbook patches into an external script
This commit is contained in:
parent
bf1762a912
commit
d24a4dc0fe
660
scripts/patch-macos-playbooks.py
Executable file
660
scripts/patch-macos-playbooks.py
Executable file
@ -0,0 +1,660 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
This script is invoked by setup-ai-workspace-all-in-one.sh to patch playbooks for macOS.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
def main():
|
||||
def patch_0():
|
||||
|
||||
path = Path("setup-xworkspace-console.yaml")
|
||||
text = path.read_text()
|
||||
|
||||
commands = {
|
||||
'su - {{ xworkspace_console_user }} -c "systemctl --user daemon-reload"': 'systemctl --user daemon-reload',
|
||||
'su - {{ xworkspace_console_user }} -c "systemctl --user restart xworkspace-console.service"': 'systemctl --user restart xworkspace-console.service',
|
||||
'su - {{ xworkspace_console_user }} -c "systemctl --user restart xworkspace-ttyd.service"': 'systemctl --user restart xworkspace-ttyd.service',
|
||||
}
|
||||
|
||||
def wrapped(systemctl_command: str) -> str:
|
||||
lines = [
|
||||
'uid="$(id -u {{ xworkspace_console_user }})"',
|
||||
'loginctl enable-linger {{ xworkspace_console_user }} || true',
|
||||
'systemctl start "user@${uid}.service" || true',
|
||||
f'runuser -u {{{{ xworkspace_console_user }}}} -- env XDG_RUNTIME_DIR="/run/user/${{uid}}" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${{uid}}/bus" {systemctl_command}',
|
||||
]
|
||||
return "\n ".join(lines)
|
||||
|
||||
updated = text
|
||||
for old, command in commands.items():
|
||||
updated = updated.replace(old, wrapped(command))
|
||||
|
||||
if updated != text:
|
||||
path.write_text(updated)
|
||||
|
||||
patch_0()
|
||||
|
||||
def patch_1():
|
||||
|
||||
vars_path = Path("roles/vhosts/vault/vars/main.yml")
|
||||
tasks_path = Path("roles/vhosts/vault/tasks/main.yml")
|
||||
macos_path = Path("roles/vhosts/vault/tasks/macos.yml")
|
||||
|
||||
# 1) Make vault dirs and binary path OS-conditional (Linux unchanged).
|
||||
vars_text = vars_path.read_text()
|
||||
vars_subs = {
|
||||
"vault_binary_path: /usr/local/bin/vault":
|
||||
"vault_binary_path: \"{{ '/opt/homebrew/bin/vault' if ansible_os_family == 'Darwin' else '/usr/local/bin/vault' }}\"",
|
||||
"vault_config_dir: /etc/vault.d":
|
||||
"vault_config_dir: \"{{ (ansible_env.HOME ~ '/Library/Application Support/vault') if ansible_os_family == 'Darwin' else '/etc/vault.d' }}\"",
|
||||
"vault_data_dir: /opt/vault/data":
|
||||
"vault_data_dir: \"{{ (ansible_env.HOME ~ '/Library/Application Support/vault/data') if ansible_os_family == 'Darwin' else '/opt/vault/data' }}\"",
|
||||
}
|
||||
for old, new in vars_subs.items():
|
||||
if old in vars_text:
|
||||
vars_text = vars_text.replace(old, new)
|
||||
vars_path.write_text(vars_text)
|
||||
|
||||
# 2) Skip the root-owned directory creation task on macOS.
|
||||
tasks_text = tasks_path.read_text()
|
||||
dir_when_old = (
|
||||
' loop:\n'
|
||||
' - "{{ vault_config_dir }}"\n'
|
||||
' - "{{ vault_data_dir }}"\n'
|
||||
' when:\n'
|
||||
' - vault_deploy_mode == "standalone"\n'
|
||||
)
|
||||
dir_when_new = dir_when_old + " - ansible_os_family != 'Darwin'\n"
|
||||
if dir_when_old in tasks_text and " - ansible_os_family != 'Darwin'\n\n- name: Deploy standalone Vault systemd" not in tasks_text:
|
||||
tasks_text = tasks_text.replace(dir_when_old, dir_when_new, 1)
|
||||
|
||||
# 2b) The admin bootstrap runs files/init_vault_admin.sh, which require_cmd's
|
||||
# vault/jq/curl/base64. On macOS those live under Homebrew, which is not on the
|
||||
# minimal PATH ansible.builtin.script uses; prepend the Homebrew bin dirs so the
|
||||
# helper can find them.
|
||||
boot_old = (
|
||||
' --ui-url {{ vault_admin_ui_url | quote }}\n'
|
||||
' no_log: true\n'
|
||||
)
|
||||
boot_new = (
|
||||
' --ui-url {{ vault_admin_ui_url | quote }}\n'
|
||||
' environment:\n'
|
||||
' PATH: "/opt/homebrew/bin:/usr/local/bin:{{ ansible_env.PATH }}"\n'
|
||||
' no_log: true\n'
|
||||
)
|
||||
if boot_old in tasks_text and boot_new not in tasks_text:
|
||||
tasks_text = tasks_text.replace(boot_old, boot_new, 1)
|
||||
|
||||
tasks_path.write_text(tasks_text)
|
||||
|
||||
# 2d) init_vault_admin.sh resolves the admin entity_id by logging in as the
|
||||
# user. Once the login MFA enforcement it creates exists, that login is
|
||||
# MFA-gated and returns no entity_id, so re-runs fail with "missing entityID".
|
||||
# Resolve the entity via its userpass entity-alias instead (idempotent).
|
||||
init_path = Path("roles/vhosts/vault/files/init_vault_admin.sh")
|
||||
if init_path.exists():
|
||||
init_text = init_path.read_text()
|
||||
login_old = (
|
||||
'bootstrap_json="$(vault write -format=json "auth/userpass/login/${USERNAME}" password="$PASSWORD")"\n'
|
||||
'entity_id="$(printf \'%s\' "$bootstrap_json" | jq -r \'.auth.entity_id\')"\n'
|
||||
'bootstrap_token="$(printf \'%s\' "$bootstrap_json" | jq -r \'.auth.client_token\')"\n'
|
||||
)
|
||||
login_new = (
|
||||
'entity_id=""\n'
|
||||
'# bootstrap_token kept defined (empty) so any later "vault token revoke\n'
|
||||
'# $bootstrap_token" line stays valid under set -u; we no longer log in.\n'
|
||||
'bootstrap_token=""\n'
|
||||
'for alias_id in $(vault list -format=json identity/entity-alias/id 2>/dev/null | jq -r \'.[]?\'); do\n'
|
||||
' alias_json="$(vault read -format=json "identity/entity-alias/id/${alias_id}" 2>/dev/null || true)"\n'
|
||||
' alias_name="$(printf \'%s\' "$alias_json" | jq -r \'.data.name // empty\')"\n'
|
||||
' alias_mount="$(printf \'%s\' "$alias_json" | jq -r \'.data.mount_accessor // empty\')"\n'
|
||||
' if [[ "$alias_name" == "$USERNAME" && "$alias_mount" == "$userpass_accessor" ]]; then\n'
|
||||
' entity_id="$(printf \'%s\' "$alias_json" | jq -r \'.data.canonical_id // empty\')"\n'
|
||||
' break\n'
|
||||
' fi\n'
|
||||
'done\n'
|
||||
'\n'
|
||||
'if [[ -z "$entity_id" ]]; then\n'
|
||||
' entity_id="$(vault write -format=json identity/entity name="$USERNAME" policies="$POLICY_NAME" | jq -r \'.data.id\')"\n'
|
||||
' vault write identity/entity-alias name="$USERNAME" canonical_id="$entity_id" mount_accessor="$userpass_accessor" >/dev/null\n'
|
||||
'fi\n'
|
||||
)
|
||||
if login_old in init_text:
|
||||
init_text = init_text.replace(login_old, login_new, 1)
|
||||
# Note: we intentionally do NOT delete the later "vault token revoke
|
||||
# $bootstrap_token" line — on some revisions it is wrapped in an if/fi,
|
||||
# and removing it would leave an empty then-block (syntax error). With
|
||||
# bootstrap_token="" set above, the revoke is a harmless no-op.
|
||||
init_path.write_text(init_text)
|
||||
|
||||
# 3) Create the macOS vault dirs (user-owned) before the launchd plist is laid down.
|
||||
macos_text = macos_path.read_text()
|
||||
dir_task = (
|
||||
"- name: Ensure macOS Vault directories exist\n"
|
||||
" ansible.builtin.file:\n"
|
||||
" path: \"{{ item }}\"\n"
|
||||
" state: directory\n"
|
||||
" mode: \"0755\"\n"
|
||||
" loop:\n"
|
||||
" - \"{{ vault_config_dir }}\"\n"
|
||||
" - \"{{ vault_data_dir }}\"\n"
|
||||
" - \"{{ ansible_env.HOME }}/.local/state/xworkspace\"\n\n"
|
||||
)
|
||||
anchor = "- name: Install HashiCorp Tap\n"
|
||||
if "Ensure macOS Vault directories exist" not in macos_text and anchor in macos_text:
|
||||
macos_text = macos_text.replace(anchor, dir_task + anchor, 1)
|
||||
|
||||
# jq is not preinstalled on macOS and the Linux apt task that installs it is
|
||||
# Darwin-skipped, yet init_vault_admin.sh requires it. Install it via Homebrew.
|
||||
vault_brew_old = (
|
||||
"- name: Install Vault via Homebrew\n"
|
||||
" ansible.builtin.command: brew install hashicorp/tap/vault\n"
|
||||
" args:\n"
|
||||
" creates: /opt/homebrew/bin/vault\n"
|
||||
" changed_when: true\n"
|
||||
)
|
||||
jq_task = (
|
||||
"\n- name: Install jq via Homebrew (required by Vault admin bootstrap)\n"
|
||||
" ansible.builtin.command: brew install jq\n"
|
||||
" args:\n"
|
||||
" creates: /opt/homebrew/bin/jq\n"
|
||||
" changed_when: true\n"
|
||||
)
|
||||
if vault_brew_old in macos_text and "Install jq via Homebrew" not in macos_text:
|
||||
macos_text = macos_text.replace(vault_brew_old, vault_brew_old + jq_task, 1)
|
||||
macos_path.write_text(macos_text)
|
||||
|
||||
patch_1()
|
||||
|
||||
def patch_2():
|
||||
|
||||
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)
|
||||
|
||||
patch_2()
|
||||
|
||||
def patch_3():
|
||||
|
||||
path = Path("roles/vhosts/postgres/tasks/macos.yml")
|
||||
text = path.read_text()
|
||||
old = (
|
||||
"- name: Ensure PostgreSQL 16 is installed via Homebrew\n"
|
||||
" community.general.homebrew:\n"
|
||||
" name: postgresql@16\n"
|
||||
" state: present\n"
|
||||
)
|
||||
new = (
|
||||
"- name: Ensure PostgreSQL 16 is installed via Homebrew\n"
|
||||
" ansible.builtin.command: brew install postgresql@16\n"
|
||||
" environment:\n"
|
||||
" PATH: \"/opt/homebrew/bin:/usr/local/bin:{{ ansible_env.PATH }}\"\n"
|
||||
" HOMEBREW_NO_AUTO_UPDATE: \"1\"\n"
|
||||
" register: postgresql_brew_install\n"
|
||||
" changed_when: >-\n"
|
||||
" 'already installed' not in (postgresql_brew_install.stderr | default(''))\n"
|
||||
" and 'already installed' not in (postgresql_brew_install.stdout | default(''))\n"
|
||||
" failed_when: postgresql_brew_install.rc != 0\n"
|
||||
)
|
||||
if old in text:
|
||||
text = text.replace(old, new, 1)
|
||||
path.write_text(text)
|
||||
|
||||
patch_3()
|
||||
|
||||
def patch_4():
|
||||
|
||||
path = Path("roles/vhosts/litellm/tasks/main.yml")
|
||||
text = path.read_text()
|
||||
old = (
|
||||
"- name: Install LiteLLM prerequisites (macOS)\n"
|
||||
" community.general.homebrew:\n"
|
||||
" name: python@3.13\n"
|
||||
" state: present\n"
|
||||
" when: ansible_os_family == 'Darwin'\n"
|
||||
)
|
||||
new = (
|
||||
"- name: Install LiteLLM prerequisites (macOS)\n"
|
||||
" ansible.builtin.command: brew install python@3.13\n"
|
||||
" environment:\n"
|
||||
" PATH: \"/opt/homebrew/bin:/usr/local/bin:{{ ansible_env.PATH }}\"\n"
|
||||
" HOMEBREW_NO_AUTO_UPDATE: \"1\"\n"
|
||||
" register: litellm_brew_python\n"
|
||||
" changed_when: >-\n"
|
||||
" 'already installed' not in (litellm_brew_python.stderr | default(''))\n"
|
||||
" and 'already installed' not in (litellm_brew_python.stdout | default(''))\n"
|
||||
" failed_when: litellm_brew_python.rc != 0\n"
|
||||
" when: ansible_os_family == 'Darwin'\n"
|
||||
)
|
||||
if old in text:
|
||||
text = text.replace(old, new, 1)
|
||||
|
||||
# The config dir and env-file tasks hardcode owner/group root, which cannot be
|
||||
# chowned under become=false on macOS. Make ownership OS-conditional (service
|
||||
# user/group on Darwin, root on Linux). The config dir path itself is relocated
|
||||
# to a user-writable location via the litellm_config_dir extra-var.
|
||||
owner_subs = [
|
||||
(
|
||||
' path: "{{ litellm_config_dir }}"\n'
|
||||
' state: directory\n'
|
||||
' owner: root\n'
|
||||
' group: root\n'
|
||||
' mode: "0755"\n',
|
||||
' path: "{{ litellm_config_dir }}"\n'
|
||||
' state: directory\n'
|
||||
' owner: "{{ litellm_service_user if ansible_os_family == \'Darwin\' else \'root\' }}"\n'
|
||||
' group: "{{ litellm_service_group if ansible_os_family == \'Darwin\' else \'root\' }}"\n'
|
||||
' mode: "0755"\n',
|
||||
),
|
||||
(
|
||||
' dest: "{{ litellm_env_file }}"\n'
|
||||
' owner: root\n'
|
||||
' group: root\n'
|
||||
' mode: "0600"\n',
|
||||
' dest: "{{ litellm_env_file }}"\n'
|
||||
' owner: "{{ litellm_service_user if ansible_os_family == \'Darwin\' else \'root\' }}"\n'
|
||||
' group: "{{ litellm_service_group if ansible_os_family == \'Darwin\' else \'root\' }}"\n'
|
||||
' mode: "0600"\n',
|
||||
),
|
||||
]
|
||||
for o, n in owner_subs:
|
||||
if o in text:
|
||||
text = text.replace(o, n, 1)
|
||||
|
||||
path.write_text(text)
|
||||
|
||||
# provision-database.yml runs psql with become_user postgres, which has no
|
||||
# equivalent on macOS Homebrew (no postgres system user, no passwordless sudo,
|
||||
# psql off-PATH). On Darwin run without escalation as the current user (the brew
|
||||
# DB superuser) and put the postgresql@16 bin on PATH. Linux unchanged.
|
||||
prov_path = Path("roles/vhosts/litellm/tasks/provision-database.yml")
|
||||
if prov_path.exists():
|
||||
prov = prov_path.read_text()
|
||||
prov_old = (
|
||||
" args:\n"
|
||||
" executable: /bin/bash\n"
|
||||
" become: true\n"
|
||||
" become_user: \"{{ 'root' if litellm_database_provisioner == 'docker' else 'postgres' }}\"\n"
|
||||
)
|
||||
prov_new = (
|
||||
" args:\n"
|
||||
" executable: /bin/bash\n"
|
||||
" environment:\n"
|
||||
" PATH: \"/opt/homebrew/opt/postgresql@16/bin:/usr/local/opt/postgresql@16/bin:{{ ansible_env.PATH }}\"\n"
|
||||
" become: \"{{ ansible_os_family != 'Darwin' }}\"\n"
|
||||
" become_user: \"{{ 'root' if litellm_database_provisioner == 'docker' else 'postgres' }}\"\n"
|
||||
)
|
||||
if prov_old in prov:
|
||||
prov = prov.replace(prov_old, prov_new)
|
||||
prov_path.write_text(prov)
|
||||
|
||||
patch_4()
|
||||
|
||||
def patch_5():
|
||||
|
||||
path = Path("setup-xworkspace-console.yaml")
|
||||
if path.exists():
|
||||
text = path.read_text()
|
||||
|
||||
# 1. Skip release archive download/validate/install tasks on macOS.
|
||||
download_old = (
|
||||
" - name: Download XWorkspace Console runtime release\n"
|
||||
" ansible.builtin.get_url:\n"
|
||||
" url: \"https://github.com/ai-workspace-lab/xworkspace-console/releases/latest/download/xworkspace-console-runtime-{{ ansible_system | lower }}-{{ 'amd64' if ansible_architecture in ['x86_64', 'amd64'] else 'arm64' }}.tar.gz\"\n"
|
||||
" dest: \"/tmp/xworkspace-console-runtime.tar.gz\"\n"
|
||||
" mode: \"0644\"\n"
|
||||
" force: true\n"
|
||||
" when: xworkspace_console_runtime_archive | length == 0"
|
||||
)
|
||||
download_new = (
|
||||
" - name: Download XWorkspace Console runtime release\n"
|
||||
" ansible.builtin.get_url:\n"
|
||||
" url: \"https://github.com/ai-workspace-lab/xworkspace-console/releases/latest/download/xworkspace-console-runtime-{{ ansible_system | lower }}-{{ 'amd64' if ansible_architecture in ['x86_64', 'amd64'] else 'arm64' }}.tar.gz\"\n"
|
||||
" dest: \"/tmp/xworkspace-console-runtime.tar.gz\"\n"
|
||||
" mode: \"0644\"\n"
|
||||
" force: true\n"
|
||||
" when:\n"
|
||||
" - xworkspace_console_runtime_archive | length == 0\n"
|
||||
" - ansible_os_family != 'Darwin'"
|
||||
)
|
||||
if download_old in text:
|
||||
text = text.replace(download_old, download_new, 1)
|
||||
|
||||
validate_old = (
|
||||
" - name: Validate packaged XWorkspace Console runtime\n"
|
||||
" ansible.builtin.stat:\n"
|
||||
" path: \"{{ xworkspace_console_runtime_archive_resolved }}\"\n"
|
||||
" register: xworkspace_console_runtime_archive_stat"
|
||||
)
|
||||
validate_new = (
|
||||
" - name: Validate packaged XWorkspace Console runtime\n"
|
||||
" ansible.builtin.stat:\n"
|
||||
" path: \"{{ xworkspace_console_runtime_archive_resolved }}\"\n"
|
||||
" register: xworkspace_console_runtime_archive_stat\n"
|
||||
" when: ansible_os_family != 'Darwin'"
|
||||
)
|
||||
if validate_old in text and (validate_old + "\n when:") not in text:
|
||||
text = text.replace(validate_old, validate_new, 1)
|
||||
|
||||
require_old = (
|
||||
" - name: Require packaged XWorkspace Console runtime\n"
|
||||
" ansible.builtin.assert:\n"
|
||||
" that:\n"
|
||||
" - xworkspace_console_runtime_archive_stat.stat.exists | default(false)\n"
|
||||
" fail_msg: \"A valid XWORKSPACE_CONSOLE_RUNTIME_ARCHIVE is required or download failed.\""
|
||||
)
|
||||
require_new = (
|
||||
" - name: Require packaged XWorkspace Console runtime\n"
|
||||
" ansible.builtin.assert:\n"
|
||||
" that:\n"
|
||||
" - xworkspace_console_runtime_archive_stat.stat.exists | default(false)\n"
|
||||
" fail_msg: \"A valid XWORKSPACE_CONSOLE_RUNTIME_ARCHIVE is required or download failed.\"\n"
|
||||
" when: ansible_os_family != 'Darwin'"
|
||||
)
|
||||
if require_old in text and (require_old + "\n when:") not in text:
|
||||
text = text.replace(require_old, require_new, 1)
|
||||
|
||||
marker_old = (
|
||||
" - name: Inspect installed XWorkspace Console runtime marker\n"
|
||||
" ansible.builtin.slurp:\n"
|
||||
" path: \"{{ xworkspace_console_runtime_marker }}\"\n"
|
||||
" register: xworkspace_console_runtime_marker_content\n"
|
||||
" failed_when: false"
|
||||
)
|
||||
marker_new = (
|
||||
" - name: Inspect installed XWorkspace Console runtime marker\n"
|
||||
" ansible.builtin.slurp:\n"
|
||||
" path: \"{{ xworkspace_console_runtime_marker }}\"\n"
|
||||
" register: xworkspace_console_runtime_marker_content\n"
|
||||
" failed_when: false\n"
|
||||
" when: ansible_os_family != 'Darwin'"
|
||||
)
|
||||
if marker_old in text and (marker_old + "\n when:") not in text:
|
||||
text = text.replace(marker_old, marker_new, 1)
|
||||
|
||||
install_old = (
|
||||
" - name: Install packaged XWorkspace Console runtime\n"
|
||||
" ansible.builtin.unarchive:\n"
|
||||
" src: \"{{ xworkspace_console_runtime_archive_resolved }}\"\n"
|
||||
" dest: \"{{ xworkspace_console_repo_dir | dirname }}\"\n"
|
||||
" remote_src: true\n"
|
||||
" owner: \"{{ xworkspace_console_user }}\"\n"
|
||||
" group: \"{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}\"\n"
|
||||
" when:\n"
|
||||
" - xworkspace_console_runtime_archive_stat.stat.exists | default(false)\n"
|
||||
" - >-\n"
|
||||
" (xworkspace_console_runtime_marker_content.content | default('') | b64decode | trim)\n"
|
||||
" != (xworkspace_console_runtime_archive_stat.stat.checksum | default(''))\n"
|
||||
" or not (xworkspace_console_api_binary is file)\n"
|
||||
" or not ((xworkspace_console_dashboard_dir ~ '/dist/index.html') is file)"
|
||||
)
|
||||
install_new = (
|
||||
" - name: Install packaged XWorkspace Console runtime\n"
|
||||
" ansible.builtin.unarchive:\n"
|
||||
" src: \"{{ xworkspace_console_runtime_archive_resolved }}\"\n"
|
||||
" dest: \"{{ xworkspace_console_repo_dir | dirname }}\"\n"
|
||||
" remote_src: true\n"
|
||||
" owner: \"{{ xworkspace_console_user }}\"\n"
|
||||
" group: \"{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}\"\n"
|
||||
" when:\n"
|
||||
" - ansible_os_family != 'Darwin'\n"
|
||||
" - xworkspace_console_runtime_archive_stat.stat.exists | default(false)\n"
|
||||
" - >-\n"
|
||||
" (xworkspace_console_runtime_marker_content.content | default('') | b64decode | trim)\n"
|
||||
" != (xworkspace_console_runtime_archive_stat.stat.checksum | default(''))\n"
|
||||
" or not (xworkspace_console_api_binary is file)\n"
|
||||
" or not ((xworkspace_console_dashboard_dir ~ '/dist/index.html') is file)"
|
||||
)
|
||||
if install_old in text:
|
||||
text = text.replace(install_old, install_new, 1)
|
||||
|
||||
record_old = (
|
||||
" - name: Record installed XWorkspace Console runtime checksum\n"
|
||||
" ansible.builtin.copy:\n"
|
||||
" dest: \"{{ xworkspace_console_runtime_marker }}\"\n"
|
||||
" owner: \"{{ xworkspace_console_user }}\"\n"
|
||||
" group: \"{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}\"\n"
|
||||
" mode: \"0644\"\n"
|
||||
" content: \"{{ xworkspace_console_runtime_archive_stat.stat.checksum }}\\n\"\n"
|
||||
" when:\n"
|
||||
" - xworkspace_console_runtime_archive_stat.stat.exists | default(false)"
|
||||
)
|
||||
record_new = (
|
||||
" - name: Record installed XWorkspace Console runtime checksum\n"
|
||||
" ansible.builtin.copy:\n"
|
||||
" dest: \"{{ xworkspace_console_runtime_marker }}\"\n"
|
||||
" owner: \"{{ xworkspace_console_user }}\"\n"
|
||||
" group: \"{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}\"\n"
|
||||
" mode: \"0644\"\n"
|
||||
" content: \"{{ xworkspace_console_runtime_archive_stat.stat.checksum }}\\n\"\n"
|
||||
" when:\n"
|
||||
" - ansible_os_family != 'Darwin'\n"
|
||||
" - xworkspace_console_runtime_archive_stat.stat.exists | default(false)"
|
||||
)
|
||||
if record_old in text:
|
||||
text = text.replace(record_old, record_new, 1)
|
||||
|
||||
# 2. Inject Clone and Build tasks on macOS (Darwin).
|
||||
anchor = " - name: Deploy AI Workspace portal service configuration"
|
||||
injected_tasks = (
|
||||
" - name: Check if xworkspace-console repo already exists (macOS)\n"
|
||||
" ansible.builtin.stat:\n"
|
||||
" path: \"{{ xworkspace_console_repo_dir }}/.git\"\n"
|
||||
" register: xworkspace_console_git_stat_macos\n"
|
||||
" when: ansible_os_family == 'Darwin'\n"
|
||||
"\n"
|
||||
" - name: Clone xworkspace-console repository (macOS)\n"
|
||||
" ansible.builtin.git:\n"
|
||||
" repo: \"{{ xworkspace_console_source_repo }}\"\n"
|
||||
" dest: \"{{ xworkspace_console_repo_dir }}\"\n"
|
||||
" version: \"{{ xworkspace_console_source_version }}\"\n"
|
||||
" depth: 1\n"
|
||||
" become_user: \"{{ xworkspace_console_user }}\"\n"
|
||||
" when:\n"
|
||||
" - ansible_os_family == 'Darwin'\n"
|
||||
" - not (xworkspace_console_git_stat_macos.stat.exists | default(false))\n"
|
||||
"\n"
|
||||
" - name: Build dashboard assets on target (macOS)\n"
|
||||
" ansible.builtin.shell: |\n"
|
||||
" set -euo pipefail\n"
|
||||
" cd \"{{ xworkspace_console_dashboard_dir }}\"\n"
|
||||
" source_commit=\"$(git -C \"{{ xworkspace_console_repo_dir }}\" rev-parse HEAD)\"\n"
|
||||
" marker=\".ai-workspace-build-commit\"\n"
|
||||
" if [ -f \"dist/index.html\" ] && [ \"$(cat \"$marker\" 2>/dev/null || true)\" = \"$source_commit\" ]; then\n"
|
||||
" echo \"build=unchanged\"\n"
|
||||
" exit 0\n"
|
||||
" fi\n"
|
||||
" npm install && npm run build\n"
|
||||
" printf '%s\\n' \"$source_commit\" > \"$marker\"\n"
|
||||
" echo \"build=changed\"\n"
|
||||
" args:\n"
|
||||
" executable: /bin/bash\n"
|
||||
" become_user: \"{{ xworkspace_console_user }}\"\n"
|
||||
" register: xworkspace_console_dashboard_build_macos\n"
|
||||
" changed_when: \"'build=changed' in (xworkspace_console_dashboard_build_macos.stdout | default(''))\"\n"
|
||||
" when: ansible_os_family == 'Darwin'\n"
|
||||
"\n"
|
||||
)
|
||||
if anchor in text and "Clone xworkspace-console repository (macOS)" not in text:
|
||||
text = text.replace(anchor, injected_tasks + anchor, 1)
|
||||
|
||||
path.write_text(text)
|
||||
|
||||
# Patch xworkspace_console_macos.yml to ensure LaunchAgents directory exists
|
||||
macos_path = Path("xworkspace_console_macos.yml")
|
||||
if macos_path.exists():
|
||||
macos_text = macos_path.read_text()
|
||||
launchagents_task = (
|
||||
"- name: Ensure macOS LaunchAgents directory exists\n"
|
||||
" ansible.builtin.file:\n"
|
||||
" path: \"{{ ansible_env.HOME }}/Library/LaunchAgents\"\n"
|
||||
" state: directory\n"
|
||||
" mode: \"0755\"\n\n"
|
||||
)
|
||||
if "Ensure macOS LaunchAgents directory exists" not in macos_text:
|
||||
if macos_text.startswith("---\n"):
|
||||
macos_text = "---\n" + launchagents_task + macos_text[4:]
|
||||
else:
|
||||
macos_text = launchagents_task + macos_text
|
||||
macos_path.write_text(macos_text)
|
||||
|
||||
patch_5()
|
||||
|
||||
def patch_6():
|
||||
|
||||
path = Path("roles/vhosts/gateway_openclaw/tasks/main.yml")
|
||||
if path.exists():
|
||||
text = path.read_text()
|
||||
|
||||
download_old = (
|
||||
"- name: Download OpenClaw Multi-Session Plugins offline archive\n"
|
||||
" ansible.builtin.get_url:\n"
|
||||
" url: \"{{ gateway_openclaw_multi_session_plugin_archive_url }}\"\n"
|
||||
" dest: \"/tmp/openclaw-multi-session-plugins.tar.gz\"\n"
|
||||
" mode: \"0644\""
|
||||
)
|
||||
download_new = (
|
||||
"- name: Download OpenClaw Multi-Session Plugins offline archive\n"
|
||||
" ansible.builtin.get_url:\n"
|
||||
" url: \"{{ gateway_openclaw_multi_session_plugin_archive_url }}\"\n"
|
||||
" dest: \"/tmp/openclaw-multi-session-plugins.tar.gz\"\n"
|
||||
" mode: \"0644\"\n"
|
||||
" when: ansible_os_family != 'Darwin'"
|
||||
)
|
||||
if download_old in text:
|
||||
text = text.replace(download_old, download_new, 1)
|
||||
|
||||
extract_old = (
|
||||
"- name: Extract OpenClaw Multi-Session Plugins\n"
|
||||
" ansible.builtin.unarchive:\n"
|
||||
" src: \"/tmp/openclaw-multi-session-plugins.tar.gz\"\n"
|
||||
" dest: \"{{ gateway_openclaw_home }}/.openclaw/extensions\"\n"
|
||||
" remote_src: true\n"
|
||||
" owner: \"{{ gateway_openclaw_service_user }}\"\n"
|
||||
" group: \"{{ gateway_openclaw_service_group }}\"\n"
|
||||
" mode: \"0755\"\n"
|
||||
" become: \"{{ ansible_os_family != 'Darwin' }}\"\n"
|
||||
" notify: Restart openclaw"
|
||||
)
|
||||
extract_new = (
|
||||
"- name: Extract OpenClaw Multi-Session Plugins\n"
|
||||
" ansible.builtin.unarchive:\n"
|
||||
" src: \"/tmp/openclaw-multi-session-plugins.tar.gz\"\n"
|
||||
" dest: \"{{ gateway_openclaw_home }}/.openclaw/extensions\"\n"
|
||||
" remote_src: true\n"
|
||||
" owner: \"{{ gateway_openclaw_service_user }}\"\n"
|
||||
" group: \"{{ gateway_openclaw_service_group }}\"\n"
|
||||
" mode: \"0755\"\n"
|
||||
" become: \"{{ ansible_os_family != 'Darwin' }}\"\n"
|
||||
" notify: Restart openclaw\n"
|
||||
" when: ansible_os_family != 'Darwin'"
|
||||
)
|
||||
if extract_old in text:
|
||||
text = text.replace(extract_old, extract_new, 1)
|
||||
|
||||
anchor = "- name: Ensure OpenClaw global plugin npm directory exists"
|
||||
injected = (
|
||||
"- name: Check if openclaw-multi-session-plugins repo exists (macOS)\n"
|
||||
" ansible.builtin.stat:\n"
|
||||
" path: \"{{ gateway_openclaw_multi_session_plugin_dir | default('/tmp/openclaw-multi-session-plugins') }}/.git\"\n"
|
||||
" register: openclaw_plugin_git_stat_macos\n"
|
||||
" when: ansible_os_family == 'Darwin'\n"
|
||||
"\n"
|
||||
"- name: Clone openclaw-multi-session-plugins repository (macOS)\n"
|
||||
" ansible.builtin.git:\n"
|
||||
" repo: \"https://github.com/ai-workspace-lab/openclaw-multi-session-plugins.git\"\n"
|
||||
" dest: \"{{ gateway_openclaw_multi_session_plugin_dir | default('/tmp/openclaw-multi-session-plugins') }}\"\n"
|
||||
" version: main\n"
|
||||
" depth: 1\n"
|
||||
" become_user: \"{{ gateway_openclaw_service_user }}\"\n"
|
||||
" when:\n"
|
||||
" - ansible_os_family == 'Darwin'\n"
|
||||
" - not (openclaw_plugin_git_stat_macos.stat.exists | default(false))\n"
|
||||
"\n"
|
||||
"- name: Build openclaw-multi-session-plugins (macOS)\n"
|
||||
" ansible.builtin.shell: |\n"
|
||||
" set -euo pipefail\n"
|
||||
" cd \"{{ gateway_openclaw_multi_session_plugin_dir | default('/tmp/openclaw-multi-session-plugins') }}\"\n"
|
||||
" npm install && npm run build\n"
|
||||
" args:\n"
|
||||
" executable: /bin/bash\n"
|
||||
" become_user: \"{{ gateway_openclaw_service_user }}\"\n"
|
||||
" when: ansible_os_family == 'Darwin'\n"
|
||||
"\n"
|
||||
"- name: Link openclaw-multi-session-plugins to extensions (macOS)\n"
|
||||
" ansible.builtin.file:\n"
|
||||
" src: \"{{ gateway_openclaw_multi_session_plugin_dir | default('/tmp/openclaw-multi-session-plugins') }}\"\n"
|
||||
" dest: \"{{ gateway_openclaw_home }}/.openclaw/extensions/openclaw-multi-session-plugins\"\n"
|
||||
" state: link\n"
|
||||
" owner: \"{{ gateway_openclaw_service_user }}\"\n"
|
||||
" group: \"{{ gateway_openclaw_service_group }}\"\n"
|
||||
" become_user: \"{{ gateway_openclaw_service_user }}\"\n"
|
||||
" when: ansible_os_family == 'Darwin'\n"
|
||||
" notify: Restart openclaw\n"
|
||||
"\n"
|
||||
)
|
||||
if anchor in text and "Clone openclaw-multi-session-plugins repository (macOS)" not in text:
|
||||
text = text.replace(anchor, injected + anchor, 1)
|
||||
|
||||
path.write_text(text)
|
||||
|
||||
patch_6()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -1141,709 +1141,23 @@ ensure_port_available() {
|
||||
error "Port $port is already in use by pid $pid ($command_name). Stop it or choose another port."
|
||||
}
|
||||
|
||||
patch_playbook_user_systemd() {
|
||||
local playbook="setup-xworkspace-console.yaml"
|
||||
if [ ! -f "$playbook" ]; then
|
||||
return
|
||||
fi
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
|
||||
path = Path("setup-xworkspace-console.yaml")
|
||||
text = path.read_text()
|
||||
|
||||
commands = {
|
||||
'su - {{ xworkspace_console_user }} -c "systemctl --user daemon-reload"': 'systemctl --user daemon-reload',
|
||||
'su - {{ xworkspace_console_user }} -c "systemctl --user restart xworkspace-console.service"': 'systemctl --user restart xworkspace-console.service',
|
||||
'su - {{ xworkspace_console_user }} -c "systemctl --user restart xworkspace-ttyd.service"': 'systemctl --user restart xworkspace-ttyd.service',
|
||||
}
|
||||
|
||||
def wrapped(systemctl_command: str) -> str:
|
||||
lines = [
|
||||
'uid="$(id -u {{ xworkspace_console_user }})"',
|
||||
'loginctl enable-linger {{ xworkspace_console_user }} || true',
|
||||
'systemctl start "user@${uid}.service" || true',
|
||||
f'runuser -u {{{{ xworkspace_console_user }}}} -- env XDG_RUNTIME_DIR="/run/user/${{uid}}" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${{uid}}/bus" {systemctl_command}',
|
||||
]
|
||||
return "\n ".join(lines)
|
||||
|
||||
updated = text
|
||||
for old, command in commands.items():
|
||||
updated = updated.replace(old, wrapped(command))
|
||||
|
||||
if updated != text:
|
||||
path.write_text(updated)
|
||||
PY
|
||||
}
|
||||
|
||||
# On macOS the vault role's "Ensure standalone Vault directories exist" task
|
||||
# targets /etc/vault.d and /opt/vault/data with owner: root. Those paths are not
|
||||
# writable under become=false and are non-standard for macOS, so patch the
|
||||
# cloned role to: (1) skip that root-owned directory task on Darwin, (2) point
|
||||
# the vault dirs/binary at Apple-standard, user-writable locations, and (3)
|
||||
# create the data dir (user-owned) in the macОS task path. Linux is untouched.
|
||||
patch_playbook_vault_macos() {
|
||||
local vars_file="roles/vhosts/vault/vars/main.yml"
|
||||
local tasks_file="roles/vhosts/vault/tasks/main.yml"
|
||||
local macos_file="roles/vhosts/vault/tasks/macos.yml"
|
||||
[ -f "$vars_file" ] && [ -f "$tasks_file" ] && [ -f "$macos_file" ] || return 0
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
|
||||
vars_path = Path("roles/vhosts/vault/vars/main.yml")
|
||||
tasks_path = Path("roles/vhosts/vault/tasks/main.yml")
|
||||
macos_path = Path("roles/vhosts/vault/tasks/macos.yml")
|
||||
|
||||
# 1) Make vault dirs and binary path OS-conditional (Linux unchanged).
|
||||
vars_text = vars_path.read_text()
|
||||
vars_subs = {
|
||||
"vault_binary_path: /usr/local/bin/vault":
|
||||
"vault_binary_path: \"{{ '/opt/homebrew/bin/vault' if ansible_os_family == 'Darwin' else '/usr/local/bin/vault' }}\"",
|
||||
"vault_config_dir: /etc/vault.d":
|
||||
"vault_config_dir: \"{{ (ansible_env.HOME ~ '/Library/Application Support/vault') if ansible_os_family == 'Darwin' else '/etc/vault.d' }}\"",
|
||||
"vault_data_dir: /opt/vault/data":
|
||||
"vault_data_dir: \"{{ (ansible_env.HOME ~ '/Library/Application Support/vault/data') if ansible_os_family == 'Darwin' else '/opt/vault/data' }}\"",
|
||||
}
|
||||
for old, new in vars_subs.items():
|
||||
if old in vars_text:
|
||||
vars_text = vars_text.replace(old, new)
|
||||
vars_path.write_text(vars_text)
|
||||
|
||||
# 2) Skip the root-owned directory creation task on macOS.
|
||||
tasks_text = tasks_path.read_text()
|
||||
dir_when_old = (
|
||||
' loop:\n'
|
||||
' - "{{ vault_config_dir }}"\n'
|
||||
' - "{{ vault_data_dir }}"\n'
|
||||
' when:\n'
|
||||
' - vault_deploy_mode == "standalone"\n'
|
||||
)
|
||||
dir_when_new = dir_when_old + " - ansible_os_family != 'Darwin'\n"
|
||||
if dir_when_old in tasks_text and " - ansible_os_family != 'Darwin'\n\n- name: Deploy standalone Vault systemd" not in tasks_text:
|
||||
tasks_text = tasks_text.replace(dir_when_old, dir_when_new, 1)
|
||||
|
||||
# 2b) The admin bootstrap runs files/init_vault_admin.sh, which require_cmd's
|
||||
# vault/jq/curl/base64. On macOS those live under Homebrew, which is not on the
|
||||
# minimal PATH ansible.builtin.script uses; prepend the Homebrew bin dirs so the
|
||||
# helper can find them.
|
||||
boot_old = (
|
||||
' --ui-url {{ vault_admin_ui_url | quote }}\n'
|
||||
' no_log: true\n'
|
||||
)
|
||||
boot_new = (
|
||||
' --ui-url {{ vault_admin_ui_url | quote }}\n'
|
||||
' environment:\n'
|
||||
' PATH: "/opt/homebrew/bin:/usr/local/bin:{{ ansible_env.PATH }}"\n'
|
||||
' no_log: true\n'
|
||||
)
|
||||
if boot_old in tasks_text and boot_new not in tasks_text:
|
||||
tasks_text = tasks_text.replace(boot_old, boot_new, 1)
|
||||
|
||||
tasks_path.write_text(tasks_text)
|
||||
|
||||
# 2d) init_vault_admin.sh resolves the admin entity_id by logging in as the
|
||||
# user. Once the login MFA enforcement it creates exists, that login is
|
||||
# MFA-gated and returns no entity_id, so re-runs fail with "missing entityID".
|
||||
# Resolve the entity via its userpass entity-alias instead (idempotent).
|
||||
init_path = Path("roles/vhosts/vault/files/init_vault_admin.sh")
|
||||
if init_path.exists():
|
||||
init_text = init_path.read_text()
|
||||
login_old = (
|
||||
'bootstrap_json="$(vault write -format=json "auth/userpass/login/${USERNAME}" password="$PASSWORD")"\n'
|
||||
'entity_id="$(printf \'%s\' "$bootstrap_json" | jq -r \'.auth.entity_id\')"\n'
|
||||
'bootstrap_token="$(printf \'%s\' "$bootstrap_json" | jq -r \'.auth.client_token\')"\n'
|
||||
)
|
||||
login_new = (
|
||||
'entity_id=""\n'
|
||||
'# bootstrap_token kept defined (empty) so any later "vault token revoke\n'
|
||||
'# $bootstrap_token" line stays valid under set -u; we no longer log in.\n'
|
||||
'bootstrap_token=""\n'
|
||||
'for alias_id in $(vault list -format=json identity/entity-alias/id 2>/dev/null | jq -r \'.[]?\'); do\n'
|
||||
' alias_json="$(vault read -format=json "identity/entity-alias/id/${alias_id}" 2>/dev/null || true)"\n'
|
||||
' alias_name="$(printf \'%s\' "$alias_json" | jq -r \'.data.name // empty\')"\n'
|
||||
' alias_mount="$(printf \'%s\' "$alias_json" | jq -r \'.data.mount_accessor // empty\')"\n'
|
||||
' if [[ "$alias_name" == "$USERNAME" && "$alias_mount" == "$userpass_accessor" ]]; then\n'
|
||||
' entity_id="$(printf \'%s\' "$alias_json" | jq -r \'.data.canonical_id // empty\')"\n'
|
||||
' break\n'
|
||||
' fi\n'
|
||||
'done\n'
|
||||
'\n'
|
||||
'if [[ -z "$entity_id" ]]; then\n'
|
||||
' entity_id="$(vault write -format=json identity/entity name="$USERNAME" policies="$POLICY_NAME" | jq -r \'.data.id\')"\n'
|
||||
' vault write identity/entity-alias name="$USERNAME" canonical_id="$entity_id" mount_accessor="$userpass_accessor" >/dev/null\n'
|
||||
'fi\n'
|
||||
)
|
||||
if login_old in init_text:
|
||||
init_text = init_text.replace(login_old, login_new, 1)
|
||||
# Note: we intentionally do NOT delete the later "vault token revoke
|
||||
# $bootstrap_token" line — on some revisions it is wrapped in an if/fi,
|
||||
# and removing it would leave an empty then-block (syntax error). With
|
||||
# bootstrap_token="" set above, the revoke is a harmless no-op.
|
||||
init_path.write_text(init_text)
|
||||
|
||||
# 3) Create the macOS vault dirs (user-owned) before the launchd plist is laid down.
|
||||
macos_text = macos_path.read_text()
|
||||
dir_task = (
|
||||
"- name: Ensure macOS Vault directories exist\n"
|
||||
" ansible.builtin.file:\n"
|
||||
" path: \"{{ item }}\"\n"
|
||||
" state: directory\n"
|
||||
" mode: \"0755\"\n"
|
||||
" loop:\n"
|
||||
" - \"{{ vault_config_dir }}\"\n"
|
||||
" - \"{{ vault_data_dir }}\"\n"
|
||||
" - \"{{ ansible_env.HOME }}/.local/state/xworkspace\"\n\n"
|
||||
)
|
||||
anchor = "- name: Install HashiCorp Tap\n"
|
||||
if "Ensure macOS Vault directories exist" not in macos_text and anchor in macos_text:
|
||||
macos_text = macos_text.replace(anchor, dir_task + anchor, 1)
|
||||
|
||||
# jq is not preinstalled on macOS and the Linux apt task that installs it is
|
||||
# Darwin-skipped, yet init_vault_admin.sh requires it. Install it via Homebrew.
|
||||
vault_brew_old = (
|
||||
"- name: Install Vault via Homebrew\n"
|
||||
" ansible.builtin.command: brew install hashicorp/tap/vault\n"
|
||||
" args:\n"
|
||||
" creates: /opt/homebrew/bin/vault\n"
|
||||
" changed_when: true\n"
|
||||
)
|
||||
jq_task = (
|
||||
"\n- name: Install jq via Homebrew (required by Vault admin bootstrap)\n"
|
||||
" ansible.builtin.command: brew install jq\n"
|
||||
" args:\n"
|
||||
" creates: /opt/homebrew/bin/jq\n"
|
||||
" changed_when: true\n"
|
||||
)
|
||||
if vault_brew_old in macos_text and "Install jq via Homebrew" not in macos_text:
|
||||
macos_text = macos_text.replace(vault_brew_old, vault_brew_old + jq_task, 1)
|
||||
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
|
||||
}
|
||||
|
||||
# The postgres native (macOS) path installs postgresql@16 via the
|
||||
# community.general.homebrew module, which auto-detects a brew prefix and can
|
||||
# pick a stale Intel Homebrew at /usr/local that crashes on newer macOS versions
|
||||
# ("unknown or unsupported macOS version"). Replace it with a brew command that
|
||||
# runs the brew on PATH (Apple Silicon prefix first), matching vault/openclaw.
|
||||
patch_playbook_postgres_macos() {
|
||||
local macos_file="roles/vhosts/postgres/tasks/macos.yml"
|
||||
[ -f "$macos_file" ] || return 0
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
|
||||
path = Path("roles/vhosts/postgres/tasks/macos.yml")
|
||||
text = path.read_text()
|
||||
old = (
|
||||
"- name: Ensure PostgreSQL 16 is installed via Homebrew\n"
|
||||
" community.general.homebrew:\n"
|
||||
" name: postgresql@16\n"
|
||||
" state: present\n"
|
||||
)
|
||||
new = (
|
||||
"- name: Ensure PostgreSQL 16 is installed via Homebrew\n"
|
||||
" ansible.builtin.command: brew install postgresql@16\n"
|
||||
" environment:\n"
|
||||
" PATH: \"/opt/homebrew/bin:/usr/local/bin:{{ ansible_env.PATH }}\"\n"
|
||||
" HOMEBREW_NO_AUTO_UPDATE: \"1\"\n"
|
||||
" register: postgresql_brew_install\n"
|
||||
" changed_when: >-\n"
|
||||
" 'already installed' not in (postgresql_brew_install.stderr | default(''))\n"
|
||||
" and 'already installed' not in (postgresql_brew_install.stdout | default(''))\n"
|
||||
" failed_when: postgresql_brew_install.rc != 0\n"
|
||||
)
|
||||
if old in text:
|
||||
text = text.replace(old, new, 1)
|
||||
path.write_text(text)
|
||||
PY
|
||||
}
|
||||
|
||||
# litellm installs python@3.13 via the community.general.homebrew module on
|
||||
# macOS, which has the same stale-Intel-Homebrew crash risk. Replace it with a
|
||||
# brew command using the PATH brew (Apple Silicon prefix first).
|
||||
patch_playbook_litellm_macos() {
|
||||
local main_file="roles/vhosts/litellm/tasks/main.yml"
|
||||
[ -f "$main_file" ] || return 0
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
|
||||
path = Path("roles/vhosts/litellm/tasks/main.yml")
|
||||
text = path.read_text()
|
||||
old = (
|
||||
"- name: Install LiteLLM prerequisites (macOS)\n"
|
||||
" community.general.homebrew:\n"
|
||||
" name: python@3.13\n"
|
||||
" state: present\n"
|
||||
" when: ansible_os_family == 'Darwin'\n"
|
||||
)
|
||||
new = (
|
||||
"- name: Install LiteLLM prerequisites (macOS)\n"
|
||||
" ansible.builtin.command: brew install python@3.13\n"
|
||||
" environment:\n"
|
||||
" PATH: \"/opt/homebrew/bin:/usr/local/bin:{{ ansible_env.PATH }}\"\n"
|
||||
" HOMEBREW_NO_AUTO_UPDATE: \"1\"\n"
|
||||
" register: litellm_brew_python\n"
|
||||
" changed_when: >-\n"
|
||||
" 'already installed' not in (litellm_brew_python.stderr | default(''))\n"
|
||||
" and 'already installed' not in (litellm_brew_python.stdout | default(''))\n"
|
||||
" failed_when: litellm_brew_python.rc != 0\n"
|
||||
" when: ansible_os_family == 'Darwin'\n"
|
||||
)
|
||||
if old in text:
|
||||
text = text.replace(old, new, 1)
|
||||
|
||||
# The config dir and env-file tasks hardcode owner/group root, which cannot be
|
||||
# chowned under become=false on macOS. Make ownership OS-conditional (service
|
||||
# user/group on Darwin, root on Linux). The config dir path itself is relocated
|
||||
# to a user-writable location via the litellm_config_dir extra-var.
|
||||
owner_subs = [
|
||||
(
|
||||
' path: "{{ litellm_config_dir }}"\n'
|
||||
' state: directory\n'
|
||||
' owner: root\n'
|
||||
' group: root\n'
|
||||
' mode: "0755"\n',
|
||||
' path: "{{ litellm_config_dir }}"\n'
|
||||
' state: directory\n'
|
||||
' owner: "{{ litellm_service_user if ansible_os_family == \'Darwin\' else \'root\' }}"\n'
|
||||
' group: "{{ litellm_service_group if ansible_os_family == \'Darwin\' else \'root\' }}"\n'
|
||||
' mode: "0755"\n',
|
||||
),
|
||||
(
|
||||
' dest: "{{ litellm_env_file }}"\n'
|
||||
' owner: root\n'
|
||||
' group: root\n'
|
||||
' mode: "0600"\n',
|
||||
' dest: "{{ litellm_env_file }}"\n'
|
||||
' owner: "{{ litellm_service_user if ansible_os_family == \'Darwin\' else \'root\' }}"\n'
|
||||
' group: "{{ litellm_service_group if ansible_os_family == \'Darwin\' else \'root\' }}"\n'
|
||||
' mode: "0600"\n',
|
||||
),
|
||||
]
|
||||
for o, n in owner_subs:
|
||||
if o in text:
|
||||
text = text.replace(o, n, 1)
|
||||
|
||||
path.write_text(text)
|
||||
|
||||
# provision-database.yml runs psql with become_user postgres, which has no
|
||||
# equivalent on macOS Homebrew (no postgres system user, no passwordless sudo,
|
||||
# psql off-PATH). On Darwin run without escalation as the current user (the brew
|
||||
# DB superuser) and put the postgresql@16 bin on PATH. Linux unchanged.
|
||||
prov_path = Path("roles/vhosts/litellm/tasks/provision-database.yml")
|
||||
if prov_path.exists():
|
||||
prov = prov_path.read_text()
|
||||
prov_old = (
|
||||
" args:\n"
|
||||
" executable: /bin/bash\n"
|
||||
" become: true\n"
|
||||
" become_user: \"{{ 'root' if litellm_database_provisioner == 'docker' else 'postgres' }}\"\n"
|
||||
)
|
||||
prov_new = (
|
||||
" args:\n"
|
||||
" executable: /bin/bash\n"
|
||||
" environment:\n"
|
||||
" PATH: \"/opt/homebrew/opt/postgresql@16/bin:/usr/local/opt/postgresql@16/bin:{{ ansible_env.PATH }}\"\n"
|
||||
" become: \"{{ ansible_os_family != 'Darwin' }}\"\n"
|
||||
" become_user: \"{{ 'root' if litellm_database_provisioner == 'docker' else 'postgres' }}\"\n"
|
||||
)
|
||||
if prov_old in prov:
|
||||
prov = prov.replace(prov_old, prov_new)
|
||||
prov_path.write_text(prov)
|
||||
PY
|
||||
}
|
||||
|
||||
# The remote setup-xworkspace-console playbook downloads a prebuilt runtime
|
||||
# archive for Linux, but no Darwin release archives are built. On Darwin, skip
|
||||
# downloading/unpacking and instead clone the git repository and build from source.
|
||||
patch_playbook_console_macos() {
|
||||
local main_file="setup-xworkspace-console.yaml"
|
||||
local macos_file="xworkspace_console_macos.yml"
|
||||
[ -f "$main_file" ] || return 0
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
|
||||
path = Path("setup-xworkspace-console.yaml")
|
||||
if path.exists():
|
||||
text = path.read_text()
|
||||
|
||||
# 1. Skip release archive download/validate/install tasks on macOS.
|
||||
download_old = (
|
||||
" - name: Download XWorkspace Console runtime release\n"
|
||||
" ansible.builtin.get_url:\n"
|
||||
" url: \"https://github.com/ai-workspace-lab/xworkspace-console/releases/latest/download/xworkspace-console-runtime-{{ ansible_system | lower }}-{{ 'amd64' if ansible_architecture in ['x86_64', 'amd64'] else 'arm64' }}.tar.gz\"\n"
|
||||
" dest: \"/tmp/xworkspace-console-runtime.tar.gz\"\n"
|
||||
" mode: \"0644\"\n"
|
||||
" force: true\n"
|
||||
" when: xworkspace_console_runtime_archive | length == 0"
|
||||
)
|
||||
download_new = (
|
||||
" - name: Download XWorkspace Console runtime release\n"
|
||||
" ansible.builtin.get_url:\n"
|
||||
" url: \"https://github.com/ai-workspace-lab/xworkspace-console/releases/latest/download/xworkspace-console-runtime-{{ ansible_system | lower }}-{{ 'amd64' if ansible_architecture in ['x86_64', 'amd64'] else 'arm64' }}.tar.gz\"\n"
|
||||
" dest: \"/tmp/xworkspace-console-runtime.tar.gz\"\n"
|
||||
" mode: \"0644\"\n"
|
||||
" force: true\n"
|
||||
" when:\n"
|
||||
" - xworkspace_console_runtime_archive | length == 0\n"
|
||||
" - ansible_os_family != 'Darwin'"
|
||||
)
|
||||
if download_old in text:
|
||||
text = text.replace(download_old, download_new, 1)
|
||||
|
||||
validate_old = (
|
||||
" - name: Validate packaged XWorkspace Console runtime\n"
|
||||
" ansible.builtin.stat:\n"
|
||||
" path: \"{{ xworkspace_console_runtime_archive_resolved }}\"\n"
|
||||
" register: xworkspace_console_runtime_archive_stat"
|
||||
)
|
||||
validate_new = (
|
||||
" - name: Validate packaged XWorkspace Console runtime\n"
|
||||
" ansible.builtin.stat:\n"
|
||||
" path: \"{{ xworkspace_console_runtime_archive_resolved }}\"\n"
|
||||
" register: xworkspace_console_runtime_archive_stat\n"
|
||||
" when: ansible_os_family != 'Darwin'"
|
||||
)
|
||||
if validate_old in text and (validate_old + "\n when:") not in text:
|
||||
text = text.replace(validate_old, validate_new, 1)
|
||||
|
||||
require_old = (
|
||||
" - name: Require packaged XWorkspace Console runtime\n"
|
||||
" ansible.builtin.assert:\n"
|
||||
" that:\n"
|
||||
" - xworkspace_console_runtime_archive_stat.stat.exists | default(false)\n"
|
||||
" fail_msg: \"A valid XWORKSPACE_CONSOLE_RUNTIME_ARCHIVE is required or download failed.\""
|
||||
)
|
||||
require_new = (
|
||||
" - name: Require packaged XWorkspace Console runtime\n"
|
||||
" ansible.builtin.assert:\n"
|
||||
" that:\n"
|
||||
" - xworkspace_console_runtime_archive_stat.stat.exists | default(false)\n"
|
||||
" fail_msg: \"A valid XWORKSPACE_CONSOLE_RUNTIME_ARCHIVE is required or download failed.\"\n"
|
||||
" when: ansible_os_family != 'Darwin'"
|
||||
)
|
||||
if require_old in text and (require_old + "\n when:") not in text:
|
||||
text = text.replace(require_old, require_new, 1)
|
||||
|
||||
marker_old = (
|
||||
" - name: Inspect installed XWorkspace Console runtime marker\n"
|
||||
" ansible.builtin.slurp:\n"
|
||||
" path: \"{{ xworkspace_console_runtime_marker }}\"\n"
|
||||
" register: xworkspace_console_runtime_marker_content\n"
|
||||
" failed_when: false"
|
||||
)
|
||||
marker_new = (
|
||||
" - name: Inspect installed XWorkspace Console runtime marker\n"
|
||||
" ansible.builtin.slurp:\n"
|
||||
" path: \"{{ xworkspace_console_runtime_marker }}\"\n"
|
||||
" register: xworkspace_console_runtime_marker_content\n"
|
||||
" failed_when: false\n"
|
||||
" when: ansible_os_family != 'Darwin'"
|
||||
)
|
||||
if marker_old in text and (marker_old + "\n when:") not in text:
|
||||
text = text.replace(marker_old, marker_new, 1)
|
||||
|
||||
install_old = (
|
||||
" - name: Install packaged XWorkspace Console runtime\n"
|
||||
" ansible.builtin.unarchive:\n"
|
||||
" src: \"{{ xworkspace_console_runtime_archive_resolved }}\"\n"
|
||||
" dest: \"{{ xworkspace_console_repo_dir | dirname }}\"\n"
|
||||
" remote_src: true\n"
|
||||
" owner: \"{{ xworkspace_console_user }}\"\n"
|
||||
" group: \"{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}\"\n"
|
||||
" when:\n"
|
||||
" - xworkspace_console_runtime_archive_stat.stat.exists | default(false)\n"
|
||||
" - >-\n"
|
||||
" (xworkspace_console_runtime_marker_content.content | default('') | b64decode | trim)\n"
|
||||
" != (xworkspace_console_runtime_archive_stat.stat.checksum | default(''))\n"
|
||||
" or not (xworkspace_console_api_binary is file)\n"
|
||||
" or not ((xworkspace_console_dashboard_dir ~ '/dist/index.html') is file)"
|
||||
)
|
||||
install_new = (
|
||||
" - name: Install packaged XWorkspace Console runtime\n"
|
||||
" ansible.builtin.unarchive:\n"
|
||||
" src: \"{{ xworkspace_console_runtime_archive_resolved }}\"\n"
|
||||
" dest: \"{{ xworkspace_console_repo_dir | dirname }}\"\n"
|
||||
" remote_src: true\n"
|
||||
" owner: \"{{ xworkspace_console_user }}\"\n"
|
||||
" group: \"{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}\"\n"
|
||||
" when:\n"
|
||||
" - ansible_os_family != 'Darwin'\n"
|
||||
" - xworkspace_console_runtime_archive_stat.stat.exists | default(false)\n"
|
||||
" - >-\n"
|
||||
" (xworkspace_console_runtime_marker_content.content | default('') | b64decode | trim)\n"
|
||||
" != (xworkspace_console_runtime_archive_stat.stat.checksum | default(''))\n"
|
||||
" or not (xworkspace_console_api_binary is file)\n"
|
||||
" or not ((xworkspace_console_dashboard_dir ~ '/dist/index.html') is file)"
|
||||
)
|
||||
if install_old in text:
|
||||
text = text.replace(install_old, install_new, 1)
|
||||
|
||||
record_old = (
|
||||
" - name: Record installed XWorkspace Console runtime checksum\n"
|
||||
" ansible.builtin.copy:\n"
|
||||
" dest: \"{{ xworkspace_console_runtime_marker }}\"\n"
|
||||
" owner: \"{{ xworkspace_console_user }}\"\n"
|
||||
" group: \"{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}\"\n"
|
||||
" mode: \"0644\"\n"
|
||||
" content: \"{{ xworkspace_console_runtime_archive_stat.stat.checksum }}\\n\"\n"
|
||||
" when:\n"
|
||||
" - xworkspace_console_runtime_archive_stat.stat.exists | default(false)"
|
||||
)
|
||||
record_new = (
|
||||
" - name: Record installed XWorkspace Console runtime checksum\n"
|
||||
" ansible.builtin.copy:\n"
|
||||
" dest: \"{{ xworkspace_console_runtime_marker }}\"\n"
|
||||
" owner: \"{{ xworkspace_console_user }}\"\n"
|
||||
" group: \"{{ 'staff' if ansible_os_family == 'Darwin' else xworkspace_console_user }}\"\n"
|
||||
" mode: \"0644\"\n"
|
||||
" content: \"{{ xworkspace_console_runtime_archive_stat.stat.checksum }}\\n\"\n"
|
||||
" when:\n"
|
||||
" - ansible_os_family != 'Darwin'\n"
|
||||
" - xworkspace_console_runtime_archive_stat.stat.exists | default(false)"
|
||||
)
|
||||
if record_old in text:
|
||||
text = text.replace(record_old, record_new, 1)
|
||||
|
||||
# 2. Inject Clone and Build tasks on macOS (Darwin).
|
||||
anchor = " - name: Deploy AI Workspace portal service configuration"
|
||||
injected_tasks = (
|
||||
" - name: Check if xworkspace-console repo already exists (macOS)\n"
|
||||
" ansible.builtin.stat:\n"
|
||||
" path: \"{{ xworkspace_console_repo_dir }}/.git\"\n"
|
||||
" register: xworkspace_console_git_stat_macos\n"
|
||||
" when: ansible_os_family == 'Darwin'\n"
|
||||
"\n"
|
||||
" - name: Clone xworkspace-console repository (macOS)\n"
|
||||
" ansible.builtin.git:\n"
|
||||
" repo: \"{{ xworkspace_console_source_repo }}\"\n"
|
||||
" dest: \"{{ xworkspace_console_repo_dir }}\"\n"
|
||||
" version: \"{{ xworkspace_console_source_version }}\"\n"
|
||||
" depth: 1\n"
|
||||
" become_user: \"{{ xworkspace_console_user }}\"\n"
|
||||
" when:\n"
|
||||
" - ansible_os_family == 'Darwin'\n"
|
||||
" - not (xworkspace_console_git_stat_macos.stat.exists | default(false))\n"
|
||||
"\n"
|
||||
" - name: Build dashboard assets on target (macOS)\n"
|
||||
" ansible.builtin.shell: |\n"
|
||||
" set -euo pipefail\n"
|
||||
" cd \"{{ xworkspace_console_dashboard_dir }}\"\n"
|
||||
" source_commit=\"$(git -C \"{{ xworkspace_console_repo_dir }}\" rev-parse HEAD)\"\n"
|
||||
" marker=\".ai-workspace-build-commit\"\n"
|
||||
" if [ -f \"dist/index.html\" ] && [ \"$(cat \"$marker\" 2>/dev/null || true)\" = \"$source_commit\" ]; then\n"
|
||||
" echo \"build=unchanged\"\n"
|
||||
" exit 0\n"
|
||||
" fi\n"
|
||||
" npm install && npm run build\n"
|
||||
" printf '%s\\n' \"$source_commit\" > \"$marker\"\n"
|
||||
" echo \"build=changed\"\n"
|
||||
" args:\n"
|
||||
" executable: /bin/bash\n"
|
||||
" become_user: \"{{ xworkspace_console_user }}\"\n"
|
||||
" register: xworkspace_console_dashboard_build_macos\n"
|
||||
" changed_when: \"'build=changed' in (xworkspace_console_dashboard_build_macos.stdout | default(''))\"\n"
|
||||
" when: ansible_os_family == 'Darwin'\n"
|
||||
"\n"
|
||||
)
|
||||
if anchor in text and "Clone xworkspace-console repository (macOS)" not in text:
|
||||
text = text.replace(anchor, injected_tasks + anchor, 1)
|
||||
|
||||
path.write_text(text)
|
||||
|
||||
# Patch xworkspace_console_macos.yml to ensure LaunchAgents directory exists
|
||||
macos_path = Path("xworkspace_console_macos.yml")
|
||||
if macos_path.exists():
|
||||
macos_text = macos_path.read_text()
|
||||
launchagents_task = (
|
||||
"- name: Ensure macOS LaunchAgents directory exists\n"
|
||||
" ansible.builtin.file:\n"
|
||||
" path: \"{{ ansible_env.HOME }}/Library/LaunchAgents\"\n"
|
||||
" state: directory\n"
|
||||
" mode: \"0755\"\n\n"
|
||||
)
|
||||
if "Ensure macOS LaunchAgents directory exists" not in macos_text:
|
||||
if macos_text.startswith("---\n"):
|
||||
macos_text = "---\n" + launchagents_task + macos_text[4:]
|
||||
else:
|
||||
macos_text = launchagents_task + macos_text
|
||||
macos_path.write_text(macos_text)
|
||||
PY
|
||||
}
|
||||
|
||||
patch_playbook_openclaw_macos() {
|
||||
local main_file="roles/vhosts/gateway_openclaw/tasks/main.yml"
|
||||
[ -f "$main_file" ] || return 0
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
|
||||
path = Path("roles/vhosts/gateway_openclaw/tasks/main.yml")
|
||||
if path.exists():
|
||||
text = path.read_text()
|
||||
patch_playbooks_for_macos() {
|
||||
info "Fetching and running macOS playbook patches..."
|
||||
local patch_script="/tmp/patch-macos-playbooks.py"
|
||||
local raw_url="https://raw.githubusercontent.com/ai-workspace-lab/xworkspace-console/main/scripts/patch-macos-playbooks.py"
|
||||
|
||||
download_old = (
|
||||
"- name: Download OpenClaw Multi-Session Plugins offline archive\n"
|
||||
" ansible.builtin.get_url:\n"
|
||||
" url: \"{{ gateway_openclaw_multi_session_plugin_archive_url }}\"\n"
|
||||
" dest: \"/tmp/openclaw-multi-session-plugins.tar.gz\"\n"
|
||||
" mode: \"0644\""
|
||||
)
|
||||
download_new = (
|
||||
"- name: Download OpenClaw Multi-Session Plugins offline archive\n"
|
||||
" ansible.builtin.get_url:\n"
|
||||
" url: \"{{ gateway_openclaw_multi_session_plugin_archive_url }}\"\n"
|
||||
" dest: \"/tmp/openclaw-multi-session-plugins.tar.gz\"\n"
|
||||
" mode: \"0644\"\n"
|
||||
" when: ansible_os_family != 'Darwin'"
|
||||
)
|
||||
if download_old in text:
|
||||
text = text.replace(download_old, download_new, 1)
|
||||
|
||||
extract_old = (
|
||||
"- name: Extract OpenClaw Multi-Session Plugins\n"
|
||||
" ansible.builtin.unarchive:\n"
|
||||
" src: \"/tmp/openclaw-multi-session-plugins.tar.gz\"\n"
|
||||
" dest: \"{{ gateway_openclaw_home }}/.openclaw/extensions\"\n"
|
||||
" remote_src: true\n"
|
||||
" owner: \"{{ gateway_openclaw_service_user }}\"\n"
|
||||
" group: \"{{ gateway_openclaw_service_group }}\"\n"
|
||||
" mode: \"0755\"\n"
|
||||
" become: \"{{ ansible_os_family != 'Darwin' }}\"\n"
|
||||
" notify: Restart openclaw"
|
||||
)
|
||||
extract_new = (
|
||||
"- name: Extract OpenClaw Multi-Session Plugins\n"
|
||||
" ansible.builtin.unarchive:\n"
|
||||
" src: \"/tmp/openclaw-multi-session-plugins.tar.gz\"\n"
|
||||
" dest: \"{{ gateway_openclaw_home }}/.openclaw/extensions\"\n"
|
||||
" remote_src: true\n"
|
||||
" owner: \"{{ gateway_openclaw_service_user }}\"\n"
|
||||
" group: \"{{ gateway_openclaw_service_group }}\"\n"
|
||||
" mode: \"0755\"\n"
|
||||
" become: \"{{ ansible_os_family != 'Darwin' }}\"\n"
|
||||
" notify: Restart openclaw\n"
|
||||
" when: ansible_os_family != 'Darwin'"
|
||||
)
|
||||
if extract_old in text:
|
||||
text = text.replace(extract_old, extract_new, 1)
|
||||
|
||||
anchor = "- name: Ensure OpenClaw global plugin npm directory exists"
|
||||
injected = (
|
||||
"- name: Check if openclaw-multi-session-plugins repo exists (macOS)\n"
|
||||
" ansible.builtin.stat:\n"
|
||||
" path: \"{{ gateway_openclaw_multi_session_plugin_dir | default('/tmp/openclaw-multi-session-plugins') }}/.git\"\n"
|
||||
" register: openclaw_plugin_git_stat_macos\n"
|
||||
" when: ansible_os_family == 'Darwin'\n"
|
||||
"\n"
|
||||
"- name: Clone openclaw-multi-session-plugins repository (macOS)\n"
|
||||
" ansible.builtin.git:\n"
|
||||
" repo: \"https://github.com/ai-workspace-lab/openclaw-multi-session-plugins.git\"\n"
|
||||
" dest: \"{{ gateway_openclaw_multi_session_plugin_dir | default('/tmp/openclaw-multi-session-plugins') }}\"\n"
|
||||
" version: main\n"
|
||||
" depth: 1\n"
|
||||
" become_user: \"{{ gateway_openclaw_service_user }}\"\n"
|
||||
" when:\n"
|
||||
" - ansible_os_family == 'Darwin'\n"
|
||||
" - not (openclaw_plugin_git_stat_macos.stat.exists | default(false))\n"
|
||||
"\n"
|
||||
"- name: Build openclaw-multi-session-plugins (macOS)\n"
|
||||
" ansible.builtin.shell: |\n"
|
||||
" set -euo pipefail\n"
|
||||
" cd \"{{ gateway_openclaw_multi_session_plugin_dir | default('/tmp/openclaw-multi-session-plugins') }}\"\n"
|
||||
" npm install && npm run build\n"
|
||||
" args:\n"
|
||||
" executable: /bin/bash\n"
|
||||
" become_user: \"{{ gateway_openclaw_service_user }}\"\n"
|
||||
" when: ansible_os_family == 'Darwin'\n"
|
||||
"\n"
|
||||
"- name: Link openclaw-multi-session-plugins to extensions (macOS)\n"
|
||||
" ansible.builtin.file:\n"
|
||||
" src: \"{{ gateway_openclaw_multi_session_plugin_dir | default('/tmp/openclaw-multi-session-plugins') }}\"\n"
|
||||
" dest: \"{{ gateway_openclaw_home }}/.openclaw/extensions/openclaw-multi-session-plugins\"\n"
|
||||
" state: link\n"
|
||||
" owner: \"{{ gateway_openclaw_service_user }}\"\n"
|
||||
" group: \"{{ gateway_openclaw_service_group }}\"\n"
|
||||
" become_user: \"{{ gateway_openclaw_service_user }}\"\n"
|
||||
" when: ansible_os_family == 'Darwin'\n"
|
||||
" notify: Restart openclaw\n"
|
||||
"\n"
|
||||
)
|
||||
if anchor in text and "Clone openclaw-multi-session-plugins repository (macOS)" not in text:
|
||||
text = text.replace(anchor, injected + anchor, 1)
|
||||
|
||||
path.write_text(text)
|
||||
PY
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -sfL -o "$patch_script" "$raw_url"
|
||||
else
|
||||
wget -qO "$patch_script" "$raw_url"
|
||||
fi
|
||||
|
||||
if [ -f "$patch_script" ]; then
|
||||
python3 "$patch_script"
|
||||
rm -f "$patch_script"
|
||||
else
|
||||
error "Failed to download macOS patch script from $raw_url"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_core_skills_source() {
|
||||
@ -2629,14 +1943,8 @@ else
|
||||
cd "$TARGET_DIR"
|
||||
fi
|
||||
|
||||
patch_playbook_user_systemd
|
||||
if [ "$(detect_os)" = "darwin" ]; then
|
||||
patch_playbook_vault_macos
|
||||
patch_playbook_common_macos
|
||||
patch_playbook_postgres_macos
|
||||
patch_playbook_litellm_macos
|
||||
patch_playbook_console_macos
|
||||
patch_playbook_openclaw_macos
|
||||
patch_playbooks_for_macos
|
||||
fi
|
||||
prefetch_independent_sources
|
||||
ensure_core_skills_source
|
||||
|
||||
Loading…
Reference in New Issue
Block a user