docs: update claude setup

refactor: Move some things to roles
refactor: fix some linting
This commit is contained in:
2026-04-12 14:02:12 -04:00
parent 1862f20074
commit df1dd39197
27 changed files with 859 additions and 320 deletions

View File

@@ -0,0 +1,23 @@
---
# OpenClaw service user
openclaw_user: openclaw
openclaw_group: openclaw
openclaw_home: /opt/openclaw
openclaw_state_dir: /opt/openclaw/.openclaw
openclaw_node_version: "24"
# Model provider
openclaw_model_provider: anthropic
openclaw_api_key: "{{ vault_openclaw_api_key }}"
# Signal channel
openclaw_signal_enabled: false
openclaw_signal_account: "{{ vault_openclaw_signal_phone | default('') }}"
openclaw_signal_cli_version: "0.13.15"
openclaw_signal_cli_path: /usr/local/bin/signal-cli
openclaw_signal_dm_policy: pairing
openclaw_signal_allow_from: [] # list of E.164 numbers permitted to DM
# Firewall
openclaw_ssh_port: 22
openclaw_gateway_port: 18789

View File

@@ -0,0 +1,10 @@
---
- name: Reload systemd
ansible.builtin.systemd:
daemon_reload: true
- name: Restart openclaw
ansible.builtin.systemd:
name: openclaw
state: restarted
listen: Restart openclaw

View File

@@ -0,0 +1,16 @@
---
galaxy_info:
author: ptoal
description: Install and configure OpenClaw AI gateway on Ubuntu
license: MIT
min_ansible_version: "2.16"
platforms:
- name: Ubuntu
versions:
- noble
galaxy_tags:
- openclaw
- ai
- signal
dependencies: []

View File

@@ -0,0 +1,122 @@
---
# ---------------------------------------------------------------------------
# System user and directories
# ---------------------------------------------------------------------------
- name: Create openclaw group
ansible.builtin.group:
name: "{{ openclaw_group }}"
system: false
state: present
- name: Create openclaw user
ansible.builtin.user:
name: "{{ openclaw_user }}"
group: "{{ openclaw_group }}"
home: "{{ openclaw_home }}"
shell: /sbin/nologin
system: false # must be non-system: subuid/subgid entries required for rootless Podman
create_home: true
state: present
- name: Get openclaw user UID
ansible.builtin.command:
cmd: "id -u {{ openclaw_user }}"
register: __openclaw_uid_result
changed_when: false
- name: Set openclaw UID fact
ansible.builtin.set_fact:
__openclaw_uid: "{{ __openclaw_uid_result.stdout }}"
- name: Enable lingering for openclaw user
ansible.builtin.command:
cmd: "loginctl enable-linger {{ openclaw_user }}"
register: __openclaw_linger
changed_when: __openclaw_linger.rc == 0
- name: Enable rootless Podman socket for openclaw user
ansible.builtin.systemd:
name: podman.socket
enabled: true
state: started
scope: user
become: true
become_user: "{{ openclaw_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ __openclaw_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ __openclaw_uid }}/bus"
- name: Create OpenClaw state directory
ansible.builtin.file:
path: "{{ openclaw_state_dir }}"
state: directory
owner: "{{ openclaw_user }}"
group: "{{ openclaw_group }}"
mode: "0750"
# ---------------------------------------------------------------------------
# Node.js
# ---------------------------------------------------------------------------
- name: Add NodeSource apt signing key
ansible.builtin.apt_key:
url: "https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key"
state: present
- name: Add NodeSource apt repository
ansible.builtin.apt_repository:
repo: "deb https://deb.nodesource.com/node_{{ openclaw_node_version }}.x nodistro main"
state: present
filename: nodesource
- name: Install Node.js
ansible.builtin.apt:
name: nodejs
state: present
update_cache: true
- name: Install pnpm globally
community.general.npm:
name: pnpm
global: true
state: present
# ---------------------------------------------------------------------------
# OpenClaw binary
# ---------------------------------------------------------------------------
- name: Install OpenClaw via npm
community.general.npm:
name: openclaw
global: true
state: "{{ 'latest' if openclaw_version == 'latest' else 'present' }}"
notify: Restart openclaw
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
- name: Template OpenClaw config
ansible.builtin.template:
src: openclaw-config.yaml.j2
dest: "{{ openclaw_state_dir }}/config.yaml"
owner: "{{ openclaw_user }}"
group: "{{ openclaw_group }}"
mode: "0640"
notify: Restart openclaw
# ---------------------------------------------------------------------------
# Systemd service with hardening
# ---------------------------------------------------------------------------
- name: Template openclaw systemd service
ansible.builtin.template:
src: openclaw.service.j2
dest: /etc/systemd/system/openclaw.service
mode: "0644"
notify:
- Reload systemd
- Restart openclaw
- name: Enable and start openclaw service
ansible.builtin.systemd:
name: openclaw
enabled: true
state: started
daemon_reload: true

View File

@@ -0,0 +1,10 @@
---
- name: Configure security (UFW, Tailscale, Docker)
ansible.builtin.include_tasks: security.yml
- name: Install OpenClaw
ansible.builtin.include_tasks: install.yml
- name: Configure Signal channel
ansible.builtin.include_tasks: signal.yml
when: openclaw_signal_enabled | bool

View File

@@ -0,0 +1,49 @@
---
# ---------------------------------------------------------------------------
# UFW firewall — defense-in-depth behind OPNsense perimeter
# Allows SSH and the OpenClaw gateway port; blocks everything else inbound
# ---------------------------------------------------------------------------
- name: Install UFW
ansible.builtin.apt:
name: ufw
state: present
update_cache: true
- name: Set UFW default policies
community.general.ufw:
direction: "{{ item.direction }}"
policy: "{{ item.policy }}"
loop:
- { direction: incoming, policy: deny }
- { direction: outgoing, policy: allow }
- { direction: routed, policy: deny }
- name: Allow SSH
community.general.ufw:
rule: allow
port: "{{ openclaw_ssh_port | string }}"
proto: tcp
- name: Allow OpenClaw gateway port
community.general.ufw:
rule: allow
port: "{{ openclaw_gateway_port | string }}"
proto: tcp
- name: Enable UFW
community.general.ufw:
state: enabled
# ---------------------------------------------------------------------------
# Rootless Podman — used exclusively for agent sandbox isolation
# Runs as the openclaw user; no root daemon, no exposed sockets
# podman-docker provides a docker-compatible CLI shim for OpenClaw tooling
# ---------------------------------------------------------------------------
- name: Install Podman and dependencies
ansible.builtin.apt:
name:
- podman
- podman-docker
- uidmap
state: present
update_cache: true

View File

@@ -0,0 +1,72 @@
---
# ---------------------------------------------------------------------------
# signal-cli — Java-based CLI bridge required by OpenClaw's Signal channel.
# Docs: https://docs.openclaw.ai/channels/signal
#
# MANUAL STEP REQUIRED after first deploy:
# Option A (link existing account):
# sudo -i -u openclaw
# signal-cli link -n "OpenClaw" # scan QR code with Signal app
#
# Option B (register dedicated number):
# sudo -i -u openclaw
# signal-cli -a {{ openclaw_signal_account }} register --captcha <token>
# signal-cli -a {{ openclaw_signal_account }} verify <sms-code>
#
# Then approve DM access:
# openclaw pairing approve signal
# ---------------------------------------------------------------------------
- name: Install Java runtime (required by signal-cli)
ansible.builtin.apt:
name: default-jre-headless
state: present
update_cache: true
- name: Create signal-cli install directory
ansible.builtin.file:
path: /opt/signal-cli
state: directory
mode: "0755"
- name: Download signal-cli archive
ansible.builtin.get_url:
url: "https://github.com/AsamK/signal-cli/releases/download/v{{ openclaw_signal_cli_version }}/signal-cli-{{ openclaw_signal_cli_version }}-Linux.tar.gz"
dest: "/opt/signal-cli/signal-cli-{{ openclaw_signal_cli_version }}.tar.gz"
mode: "0644"
register: __openclaw_signal_cli_download
- name: Extract signal-cli
ansible.builtin.unarchive:
src: "/opt/signal-cli/signal-cli-{{ openclaw_signal_cli_version }}.tar.gz"
dest: /opt/signal-cli
remote_src: true
creates: "/opt/signal-cli/signal-cli-{{ openclaw_signal_cli_version }}/bin/signal-cli"
- name: Symlink signal-cli to PATH
ansible.builtin.file:
src: "/opt/signal-cli/signal-cli-{{ openclaw_signal_cli_version }}/bin/signal-cli"
dest: "{{ openclaw_signal_cli_path }}"
state: link
- name: Set ownership of signal-cli data directory
ansible.builtin.file:
path: "{{ openclaw_home }}/.local/share/signal-cli"
state: directory
owner: "{{ openclaw_user }}"
group: "{{ openclaw_group }}"
mode: "0700"
- name: Display Signal registration reminder
ansible.builtin.debug:
msg:
- "*** MANUAL STEP REQUIRED: Signal account not yet registered ***"
- "Switch to the openclaw user and register signal-cli:"
- " sudo -i -u {{ openclaw_user }}"
- " # Option A — link existing account (recommended):"
- " signal-cli link -n 'OpenClaw' # scan QR with Signal app"
- " # Option B — register a dedicated number:"
- " signal-cli -a {{ openclaw_signal_account }} register --captcha <token>"
- " signal-cli -a {{ openclaw_signal_account }} verify <sms-code>"
- "After registration, approve pairing:"
- " openclaw pairing approve signal"

View File

@@ -0,0 +1,24 @@
# OpenClaw configuration — managed by Ansible, do not edit manually
# Ref: https://docs.openclaw.ai
gateway:
port: 18789
# Gateway binds localhost only; Tailscale is the remote access path
providers:
- type: {{ openclaw_model_provider }}
apiKey: "{{ openclaw_api_key }}"
{% if openclaw_signal_enabled | bool %}
channels:
signal:
account: "{{ openclaw_signal_account }}"
cliPath: "{{ openclaw_signal_cli_path }}"
dmPolicy: {{ openclaw_signal_dm_policy }}
{% if openclaw_signal_allow_from | length > 0 %}
allowFrom:
{% for number in openclaw_signal_allow_from %}
- "{{ number }}"
{% endfor %}
{% endif %}
{% endif %}

View File

@@ -0,0 +1,29 @@
[Unit]
Description=OpenClaw AI Gateway
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User={{ openclaw_user }}
Group={{ openclaw_group }}
WorkingDirectory={{ openclaw_home }}
Environment=OPENCLAW_STATE_DIR={{ openclaw_state_dir }}
Environment=OPENCLAW_CONFIG_PATH={{ openclaw_state_dir }}/config.yaml
Environment=DOCKER_HOST=unix:/run/user/{{ __openclaw_uid }}/podman/podman.sock
Environment=XDG_RUNTIME_DIR=/run/user/{{ __openclaw_uid }}
ExecStart=/usr/bin/openclaw gateway run
Restart=on-failure
RestartSec=5
# Hardening
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ReadWritePaths={{ openclaw_state_dir }} {{ openclaw_home }}
ProtectHome=read-only
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,21 @@
---
# Proxmox connection
# api_host / api_port are derived from the 'proxmox_api' inventory host.
proxmox_node: pve1
proxmox_api_user: ansible@pam
proxmox_api_token_id: ansible
proxmox_api_token_secret: "{{ vault_proxmox_token_secret }}"
proxmox_validate_certs: false
proxmox_storage: local-lvm
# VM spec
sno_vm_name: "sno-{{ ocp_cluster_name }}"
sno_vm_id: 0
sno_cpu: 8
sno_memory_mb: 32768
sno_disk_gb: 120
sno_pvc_disk_gb: 100
sno_vnet: ocp
sno_mac: ""
sno_storage_vnet: storage
sno_storage_mac: ""

View File

@@ -0,0 +1,15 @@
---
galaxy_info:
author: ptoal
description: Create a Proxmox VM (q35/UEFI) for SNO deployments
license: MIT
min_ansible_version: "2.16"
platforms:
- name: GenericLinux
versions:
- all
galaxy_tags:
- proxmox
- vm
dependencies: []

View File

@@ -1,5 +1,5 @@
---
# Create a Proxmox VM for Single Node OpenShift.
# Create a Proxmox VM.
# Uses q35 machine type with UEFI (required for SNO / RHCOS).
# An empty ide2 CD-ROM slot is created for the agent installer ISO.
@@ -51,7 +51,7 @@
boot: "order=scsi0;ide2"
onboot: true
state: present
register: __sno_deploy_vm_result
register: __proxmox_vm_result
- name: Retrieve VM info
community.proxmox.proxmox_vm_info:
@@ -65,18 +65,18 @@
name: "{{ sno_vm_name }}"
type: qemu
config: current
register: __sno_deploy_vm_info
register: __proxmox_vm_info
retries: 5
- name: Set VM ID fact for subsequent plays
ansible.builtin.set_fact:
sno_vm_id: "{{ __sno_deploy_vm_info.proxmox_vms[0].vmid }}"
sno_vm_id: "{{ __proxmox_vm_info.proxmox_vms[0].vmid }}"
cacheable: true
- name: Extract MAC address from VM config
ansible.builtin.set_fact:
sno_mac: >-
{{ __sno_deploy_vm_info.proxmox_vms[0].config.net0
{{ __proxmox_vm_info.proxmox_vms[0].config.net0
| regex_search('([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})', '\1')
| first }}
cacheable: true
@@ -85,7 +85,7 @@
- name: Extract storage MAC address from VM config
ansible.builtin.set_fact:
sno_storage_mac: >-
{{ __sno_deploy_vm_info.proxmox_vms[0].config.net1
{{ __proxmox_vm_info.proxmox_vms[0].config.net1
| regex_search('([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})', '\1')
| first }}
cacheable: true

View File

@@ -19,7 +19,6 @@ sno_vm_name: "sno-{{ ocp_cluster_name }}"
sno_cpu: 8
sno_memory_mb: 32768
sno_disk_gb: 120
sno_pvc_disk_gb: 100
sno_vnet: ocp
sno_mac: "" # populated after VM creation; set here to pin MAC
sno_vm_id: 0

View File

@@ -50,26 +50,10 @@ argument_specs:
description: Name of the VM in Proxmox.
type: str
default: "sno-{{ ocp_cluster_name }}"
sno_cpu:
description: Number of CPU cores for the VM.
type: int
default: 8
sno_memory_mb:
description: Memory in megabytes for the VM.
type: int
default: 32768
sno_disk_gb:
description: Primary disk size in gigabytes.
type: int
default: 120
sno_vnet:
description: Proxmox SDN VNet name for the primary (OCP) NIC.
type: str
default: ocp
sno_mac:
description: >-
MAC address for the primary NIC. Leave empty for auto-assignment by Proxmox.
Set here to pin the MAC across VM recreations.
MAC address for the primary NIC. Populated as a cacheable fact by the
proxmox_vm role; set explicitly to pin the MAC across VM recreations.
type: str
default: ""
sno_storage_ip: