Initial commit — OpenClaw VM infrastructure
- ansible/: VM provisioning playbooks and roles - provision-vm.yml: create KVM VM from Ubuntu cloud image - install.yml: install OpenClaw on guest (upstream) - customize.yml: swappiness, virtiofs fstab, linger - roles/vm/: libvirt domain XML, cloud-init templates - inventory.yml + host_vars/zap.yml: zap instance config - backup-openclaw-vm.sh: daily rsync + MinIO upload - restore-openclaw-vm.sh: full redeploy from scratch - README.md: full operational documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
42
ansible/roles/openclaw/defaults/main.yml
Normal file
42
ansible/roles/openclaw/defaults/main.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
# OpenClaw default variables
|
||||
|
||||
# CI testing mode - skips tasks that require systemd, Docker-in-Docker, or kernel access
|
||||
ci_test: false
|
||||
|
||||
# Tailscale settings
|
||||
# WARNING: Tasks using tailscale_authkey MUST set no_log: true to prevent credential exposure
|
||||
tailscale_enabled: false # Set to true to install and configure Tailscale
|
||||
tailscale_authkey: "" # Optional: set to auto-connect during installation
|
||||
|
||||
# Node.js version
|
||||
nodejs_version: "22.x"
|
||||
|
||||
# OpenClaw settings
|
||||
openclaw_port: 3000
|
||||
|
||||
# OpenClaw config directory
|
||||
openclaw_config_dir: "{{ openclaw_home }}/.openclaw"
|
||||
|
||||
# User settings (will be created as system user)
|
||||
openclaw_user: openclaw
|
||||
openclaw_home: /home/openclaw
|
||||
|
||||
# Installation mode: 'release' or 'development'
|
||||
# release: Install via pnpm install -g openclaw@latest
|
||||
# development: Clone repo, build from source, link globally
|
||||
openclaw_install_mode: "release"
|
||||
|
||||
# Development mode settings (only used when openclaw_install_mode: development)
|
||||
openclaw_repo_url: "https://github.com/openclaw/openclaw.git"
|
||||
openclaw_repo_branch: "main"
|
||||
openclaw_code_dir: "{{ openclaw_home }}/code"
|
||||
openclaw_repo_dir: "{{ openclaw_code_dir }}/openclaw"
|
||||
|
||||
# SSH keys for openclaw user
|
||||
# Add your public SSH keys here to allow SSH access as openclaw user
|
||||
# Example:
|
||||
# openclaw_ssh_keys:
|
||||
# - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx user@host"
|
||||
# - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDxxxxxxxxxxxxxxxxxxxxxxx user@host"
|
||||
openclaw_ssh_keys: []
|
||||
106
ansible/roles/openclaw/files/openclaw-setup.sh
Normal file
106
ansible/roles/openclaw/files/openclaw-setup.sh
Normal file
@@ -0,0 +1,106 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Enable 256 colors
|
||||
export TERM=xterm-256color
|
||||
export COLORTERM=truecolor
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# OpenClaw ASCII Art Lobster
|
||||
cat << 'LOBSTER'
|
||||
[0;36m
|
||||
+====================================================+
|
||||
| |
|
||||
| [0;33mWelcome to OpenClaw! [0;31m🦞[0;36m |
|
||||
| |
|
||||
|[0;31m ,.---._ [0;36m|
|
||||
|[0;31m ,,,, / `, [0;36m|
|
||||
|[0;31m \\\\\\ / '\_ ; [0;36m|
|
||||
|[0;31m |||| /\/``-.__\;' [0;36m|
|
||||
|[0;31m ::::/\/_ [0;36m|
|
||||
|[0;31m {{`-.__.-'(`(^^(^^^(^ 9 `.=========' [0;36m|
|
||||
|[0;31m{{{{{{ { ( ( ( ( (-----:= [0;36m|
|
||||
|[0;31m {{.-'~~'-.(,(,,(,,,(__6_.'=========. [0;36m|
|
||||
|[0;31m ::::\/\ [0;36m|
|
||||
|[0;31m |||| \/\ ,-'/, [0;36m|
|
||||
|[0;31m //// \ `` _/ ; [0;36m|
|
||||
|[0;31m '''' \ ` .' [0;36m|
|
||||
|[0;31m `---' [0;36m|
|
||||
| |
|
||||
| [0;32m✅ Installation Successful![0;36m |
|
||||
| |
|
||||
+====================================================+[0m
|
||||
LOBSTER
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}🔒 Security Status:${NC}"
|
||||
echo " - UFW Firewall: ENABLED"
|
||||
echo " - Open Ports: SSH (22) + Tailscale (41641/udp)"
|
||||
echo " - Docker isolation: ACTIVE"
|
||||
echo ""
|
||||
echo -e "📚 Documentation: ${GREEN}https://docs.openclaw.ai${NC}"
|
||||
echo ""
|
||||
|
||||
# Switch to openclaw user for setup
|
||||
echo -e "${YELLOW}Switching to openclaw user for setup...${NC}"
|
||||
echo ""
|
||||
echo "DEBUG: About to create init script..."
|
||||
|
||||
# Create init script that will be sourced on login
|
||||
cat > /home/openclaw/.openclaw-init << 'INIT_EOF'
|
||||
# Display welcome message
|
||||
echo "============================================"
|
||||
echo "📋 OpenClaw Setup - Next Steps"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "You are now: $(whoami)@$(hostname)"
|
||||
echo "Home: $HOME"
|
||||
echo ""
|
||||
echo "🔧 Setup Commands:"
|
||||
echo ""
|
||||
echo "1. Configure OpenClaw:"
|
||||
echo " nano ~/.openclaw/config.yml"
|
||||
echo ""
|
||||
echo "2. Login to provider (WhatsApp/Telegram/Signal):"
|
||||
echo " openclaw login"
|
||||
echo ""
|
||||
echo "3. Test gateway:"
|
||||
echo " openclaw gateway"
|
||||
echo ""
|
||||
echo "4. Exit and manage as service:"
|
||||
echo " exit"
|
||||
echo " sudo systemctl status openclaw"
|
||||
echo " sudo journalctl -u openclaw -f"
|
||||
echo ""
|
||||
echo "5. Connect Tailscale (as root):"
|
||||
echo " exit"
|
||||
echo " sudo tailscale up"
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "Type 'exit' to return to previous user"
|
||||
echo ""
|
||||
|
||||
# Remove this init file after first login
|
||||
rm -f ~/.openclaw-init
|
||||
INIT_EOF
|
||||
|
||||
chown openclaw:openclaw /home/openclaw/.openclaw-init
|
||||
|
||||
# Add one-time sourcing to .bashrc if not already there
|
||||
grep -q '.openclaw-init' /home/openclaw/.bashrc 2>/dev/null || {
|
||||
echo '' >> /home/openclaw/.bashrc
|
||||
echo '# One-time setup message' >> /home/openclaw/.bashrc
|
||||
echo '[ -f ~/.openclaw-init ] && source ~/.openclaw-init' >> /home/openclaw/.bashrc
|
||||
}
|
||||
|
||||
# Switch to openclaw user with explicit interactive shell
|
||||
# Using setsid to create new session + force pseudo-terminal allocation
|
||||
exec sudo -i -u openclaw /bin/bash --login
|
||||
34
ansible/roles/openclaw/files/show-lobster.sh
Executable file
34
ansible/roles/openclaw/files/show-lobster.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
cat << 'LOBSTER'
|
||||
[0;36m
|
||||
+====================================================+
|
||||
| |
|
||||
| [0;33mWelcome to OpenClaw! [0;31m🦞[0;36m |
|
||||
| |
|
||||
|[0;31m ,.---._ [0;36m|
|
||||
|[0;31m ,,,, / `, [0;36m|
|
||||
|[0;31m \\\\\\ / '\_ ; [0;36m|
|
||||
|[0;31m |||| /\/``-.__\;' [0;36m|
|
||||
|[0;31m ::::/\/_ [0;36m|
|
||||
|[0;31m {{`-.__.-'(`(^^(^^^(^ 9 `.=========' [0;36m|
|
||||
|[0;31m{{{{{{ { ( ( ( ( (-----:= [0;36m|
|
||||
|[0;31m {{.-'~~'-.(,(,,(,,,(__6_.'=========. [0;36m|
|
||||
|[0;31m ::::\/\ [0;36m|
|
||||
|[0;31m |||| \/\ ,-'/, [0;36m|
|
||||
|[0;31m //// \ `` _/ ; [0;36m|
|
||||
|[0;31m '''' \ ` .' [0;36m|
|
||||
|[0;31m `---' [0;36m|
|
||||
| |
|
||||
| [0;32m✅ Installation Successful![0;36m |
|
||||
| |
|
||||
+====================================================+[0m
|
||||
LOBSTER
|
||||
|
||||
echo ""
|
||||
echo "🔒 Security Status:"
|
||||
echo " - UFW Firewall: ENABLED"
|
||||
echo " - Open Ports: SSH (22) + Tailscale (41641/udp)"
|
||||
echo " - Docker isolation: ACTIVE"
|
||||
echo ""
|
||||
echo "📚 Documentation: https://docs.openclaw.ai"
|
||||
echo ""
|
||||
10
ansible/roles/openclaw/handlers/main.yml
Normal file
10
ansible/roles/openclaw/handlers/main.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
- name: Restart docker
|
||||
ansible.builtin.systemd:
|
||||
name: docker
|
||||
state: restarted
|
||||
|
||||
- name: Restart fail2ban
|
||||
ansible.builtin.systemd:
|
||||
name: fail2ban
|
||||
state: restarted
|
||||
69
ansible/roles/openclaw/tasks/docker-linux.yml
Normal file
69
ansible/roles/openclaw/tasks/docker-linux.yml
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
# Linux-specific Docker installation (apt-based)
|
||||
|
||||
- name: Install required system packages for Docker
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- ca-certificates
|
||||
- curl
|
||||
- gnupg
|
||||
- lsb-release
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Create directory for Docker GPG key
|
||||
ansible.builtin.file:
|
||||
path: /etc/apt/keyrings
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Add Docker GPG key
|
||||
ansible.builtin.shell:
|
||||
cmd: |
|
||||
set -o pipefail
|
||||
curl -fsSL https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg | \
|
||||
gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
creates: /etc/apt/keyrings/docker.gpg
|
||||
executable: /bin/bash
|
||||
|
||||
- name: Add Docker repository
|
||||
ansible.builtin.shell:
|
||||
cmd: |
|
||||
set -o pipefail
|
||||
echo "deb [arch=$(dpkg --print-architecture) \
|
||||
signed-by=/etc/apt/keyrings/docker.gpg] \
|
||||
https://download.docker.com/linux/{{ ansible_distribution | lower }} \
|
||||
$(lsb_release -cs) stable" | \
|
||||
tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
creates: /etc/apt/sources.list.d/docker.list
|
||||
executable: /bin/bash
|
||||
|
||||
- name: Update apt cache after adding Docker repo
|
||||
ansible.builtin.apt:
|
||||
update_cache: true
|
||||
|
||||
- name: Install Docker CE
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- docker-ce
|
||||
- docker-ce-cli
|
||||
- containerd.io
|
||||
- docker-buildx-plugin
|
||||
- docker-compose-plugin
|
||||
state: present
|
||||
|
||||
- name: Ensure Docker service is started and enabled
|
||||
ansible.builtin.systemd:
|
||||
name: docker
|
||||
state: started
|
||||
enabled: true
|
||||
|
||||
- name: Add user to docker group
|
||||
ansible.builtin.user:
|
||||
name: "{{ openclaw_user }}"
|
||||
groups: docker
|
||||
append: true
|
||||
|
||||
- name: Reset SSH connection to apply docker group
|
||||
ansible.builtin.meta: reset_connection
|
||||
165
ansible/roles/openclaw/tasks/firewall-linux.yml
Normal file
165
ansible/roles/openclaw/tasks/firewall-linux.yml
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
# Linux-specific firewall configuration (UFW) with security hardening
|
||||
|
||||
# Install and configure fail2ban for SSH brute-force protection
|
||||
- name: Install fail2ban
|
||||
ansible.builtin.apt:
|
||||
name: fail2ban
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Configure fail2ban for SSH protection
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/fail2ban/jail.local
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
content: |
|
||||
# OpenClaw security hardening - SSH protection
|
||||
[DEFAULT]
|
||||
bantime = 3600
|
||||
findtime = 600
|
||||
maxretry = 5
|
||||
backend = systemd
|
||||
|
||||
[sshd]
|
||||
enabled = true
|
||||
port = ssh
|
||||
filter = sshd
|
||||
# logpath not needed - systemd backend reads from journal
|
||||
notify: Restart fail2ban
|
||||
|
||||
- name: Enable and start fail2ban
|
||||
ansible.builtin.systemd:
|
||||
name: fail2ban
|
||||
state: started
|
||||
enabled: true
|
||||
|
||||
# Install and configure unattended-upgrades for automatic security updates
|
||||
- name: Install unattended-upgrades
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- unattended-upgrades
|
||||
- apt-listchanges
|
||||
state: present
|
||||
|
||||
- name: Configure automatic security updates
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/apt/apt.conf.d/20auto-upgrades
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
content: |
|
||||
APT::Periodic::Update-Package-Lists "1";
|
||||
APT::Periodic::Unattended-Upgrade "1";
|
||||
APT::Periodic::AutocleanInterval "7";
|
||||
|
||||
- name: Configure unattended-upgrades to only install security updates
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/apt/apt.conf.d/50unattended-upgrades
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
content: |
|
||||
// OpenClaw security hardening - automatic security updates
|
||||
Unattended-Upgrade::Allowed-Origins {
|
||||
"${distro_id}:${distro_codename}-security";
|
||||
"${distro_id}ESMApps:${distro_codename}-apps-security";
|
||||
"${distro_id}ESM:${distro_codename}-infra-security";
|
||||
};
|
||||
Unattended-Upgrade::Package-Blacklist {
|
||||
};
|
||||
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
|
||||
Unattended-Upgrade::MinimalSteps "true";
|
||||
Unattended-Upgrade::Remove-Unused-Dependencies "true";
|
||||
Unattended-Upgrade::Automatic-Reboot "false";
|
||||
|
||||
# UFW Firewall configuration
|
||||
- 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 on port 22
|
||||
community.general.ufw:
|
||||
rule: allow
|
||||
port: '22'
|
||||
proto: tcp
|
||||
comment: 'SSH'
|
||||
|
||||
- name: Allow Tailscale UDP port 41641
|
||||
community.general.ufw:
|
||||
rule: allow
|
||||
port: '41641'
|
||||
proto: udp
|
||||
comment: 'Tailscale'
|
||||
when: tailscale_enabled | bool
|
||||
|
||||
- name: Get default network interface
|
||||
ansible.builtin.shell:
|
||||
cmd: |
|
||||
set -o pipefail
|
||||
ip route | grep default | awk '{print $5}' | head -n1
|
||||
executable: /bin/bash
|
||||
register: default_interface
|
||||
changed_when: false
|
||||
|
||||
- name: Validate default network interface was detected
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- default_interface.stdout is defined
|
||||
- default_interface.stdout | length > 0
|
||||
fail_msg: "Failed to detect default network interface. Cannot configure firewall rules safely."
|
||||
success_msg: "Default network interface detected: {{ default_interface.stdout }}"
|
||||
|
||||
- name: Create UFW after.rules for Docker isolation
|
||||
ansible.builtin.blockinfile:
|
||||
path: /etc/ufw/after.rules
|
||||
marker: "# {mark} ANSIBLE MANAGED BLOCK - Docker isolation"
|
||||
insertbefore: "^COMMIT$"
|
||||
block: |
|
||||
# Docker port isolation - block all forwarded traffic by default
|
||||
:DOCKER-USER - [0:0]
|
||||
|
||||
# Allow established connections
|
||||
-A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||
|
||||
# Allow localhost
|
||||
-A DOCKER-USER -i lo -j ACCEPT
|
||||
|
||||
# Block all other forwarded traffic to Docker containers from external interface
|
||||
-A DOCKER-USER -i {{ default_interface.stdout }} -j DROP
|
||||
create: false
|
||||
|
||||
- name: Create Docker daemon config directory
|
||||
ansible.builtin.file:
|
||||
path: /etc/docker
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Configure Docker daemon to work with UFW
|
||||
ansible.builtin.template:
|
||||
src: daemon.json.j2
|
||||
dest: /etc/docker/daemon.json
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
notify: Restart docker
|
||||
|
||||
- name: Enable UFW
|
||||
community.general.ufw:
|
||||
state: enabled
|
||||
|
||||
- name: Reload UFW
|
||||
community.general.ufw:
|
||||
state: reloaded
|
||||
24
ansible/roles/openclaw/tasks/main.yml
Normal file
24
ansible/roles/openclaw/tasks/main.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
- name: Include system tools installation tasks
|
||||
ansible.builtin.include_tasks: system-tools.yml
|
||||
|
||||
- name: Include Tailscale installation tasks
|
||||
ansible.builtin.include_tasks: tailscale-linux.yml
|
||||
when: tailscale_enabled | bool
|
||||
|
||||
- name: Include user creation tasks
|
||||
ansible.builtin.include_tasks: user.yml
|
||||
|
||||
- name: Include Docker installation tasks
|
||||
ansible.builtin.include_tasks: docker-linux.yml
|
||||
when: not ci_test
|
||||
|
||||
- name: Include firewall configuration tasks
|
||||
ansible.builtin.include_tasks: firewall-linux.yml
|
||||
when: not ci_test
|
||||
|
||||
- name: Include Node.js installation tasks
|
||||
ansible.builtin.include_tasks: nodejs.yml
|
||||
|
||||
- name: Include OpenClaw setup tasks
|
||||
ansible.builtin.include_tasks: openclaw.yml
|
||||
69
ansible/roles/openclaw/tasks/nodejs.yml
Normal file
69
ansible/roles/openclaw/tasks/nodejs.yml
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
- name: Install required packages for Node.js
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- ca-certificates
|
||||
- curl
|
||||
- gnupg
|
||||
state: present
|
||||
|
||||
- name: Create directory for NodeSource GPG key
|
||||
ansible.builtin.file:
|
||||
path: /etc/apt/keyrings
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Add NodeSource GPG key
|
||||
ansible.builtin.shell:
|
||||
cmd: |
|
||||
set -o pipefail
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | \
|
||||
gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
|
||||
chmod a+r /etc/apt/keyrings/nodesource.gpg
|
||||
creates: /etc/apt/keyrings/nodesource.gpg
|
||||
executable: /bin/bash
|
||||
|
||||
- name: Add NodeSource repository
|
||||
ansible.builtin.shell:
|
||||
cmd: |
|
||||
set -o pipefail
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] \
|
||||
https://deb.nodesource.com/node_{{ nodejs_version }} nodistro main" | \
|
||||
tee /etc/apt/sources.list.d/nodesource.list > /dev/null
|
||||
creates: /etc/apt/sources.list.d/nodesource.list
|
||||
executable: /bin/bash
|
||||
|
||||
- name: Update apt cache after adding NodeSource repo
|
||||
ansible.builtin.apt:
|
||||
update_cache: true
|
||||
|
||||
- name: Install Node.js
|
||||
ansible.builtin.apt:
|
||||
name: nodejs
|
||||
state: present
|
||||
|
||||
- name: Check if pnpm is already installed
|
||||
ansible.builtin.command: pnpm --version
|
||||
register: pnpm_check
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- name: Install pnpm globally
|
||||
ansible.builtin.command: npm install -g pnpm
|
||||
when: pnpm_check.rc != 0
|
||||
|
||||
- name: Verify Node.js installation
|
||||
ansible.builtin.command: node --version
|
||||
register: node_version
|
||||
changed_when: false
|
||||
|
||||
- name: Verify pnpm installation
|
||||
ansible.builtin.command: pnpm --version
|
||||
register: pnpm_version
|
||||
changed_when: false
|
||||
|
||||
- name: Display Node.js and pnpm versions
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Node.js version: {{ node_version.stdout }}"
|
||||
- "pnpm version: {{ pnpm_version.stdout }}"
|
||||
215
ansible/roles/openclaw/tasks/openclaw-development.yml
Normal file
215
ansible/roles/openclaw/tasks/openclaw-development.yml
Normal file
@@ -0,0 +1,215 @@
|
||||
---
|
||||
# Development mode installation - Clone repo, build from source, link globally
|
||||
|
||||
- name: Create code directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ openclaw_code_dir }}"
|
||||
state: directory
|
||||
owner: "{{ openclaw_user }}"
|
||||
group: "{{ openclaw_user }}"
|
||||
mode: '0755'
|
||||
|
||||
- name: Check if openclaw repository already exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ openclaw_repo_dir }}/.git"
|
||||
register: openclaw_repo_exists
|
||||
|
||||
- name: Clone openclaw repository
|
||||
ansible.builtin.git:
|
||||
repo: "{{ openclaw_repo_url }}"
|
||||
dest: "{{ openclaw_repo_dir }}"
|
||||
version: "{{ openclaw_repo_branch }}"
|
||||
update: true
|
||||
become: true
|
||||
become_user: "{{ openclaw_user }}"
|
||||
when: not openclaw_repo_exists.stat.exists
|
||||
|
||||
- name: Pull latest changes if repo exists
|
||||
ansible.builtin.git:
|
||||
repo: "{{ openclaw_repo_url }}"
|
||||
dest: "{{ openclaw_repo_dir }}"
|
||||
version: "{{ openclaw_repo_branch }}"
|
||||
update: true
|
||||
become: true
|
||||
become_user: "{{ openclaw_user }}"
|
||||
when: openclaw_repo_exists.stat.exists
|
||||
register: git_pull_result
|
||||
|
||||
- name: Display git pull status
|
||||
ansible.builtin.debug:
|
||||
msg: "Git repository updated: {{ git_pull_result.changed | default(false) }}"
|
||||
when: openclaw_repo_exists.stat.exists
|
||||
|
||||
- name: Install dependencies with pnpm
|
||||
ansible.builtin.shell:
|
||||
cmd: pnpm install
|
||||
chdir: "{{ openclaw_repo_dir }}"
|
||||
executable: /bin/bash
|
||||
become: true
|
||||
become_user: "{{ openclaw_user }}"
|
||||
environment:
|
||||
PNPM_HOME: "{{ openclaw_home }}/.local/share/pnpm"
|
||||
PATH: "{{ openclaw_home }}/.local/bin:{{ openclaw_home }}/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin"
|
||||
HOME: "{{ openclaw_home }}"
|
||||
register: pnpm_install_result
|
||||
changed_when: "'Already up to date' not in pnpm_install_result.stdout"
|
||||
|
||||
- name: Build openclaw from source
|
||||
ansible.builtin.shell:
|
||||
cmd: pnpm build
|
||||
chdir: "{{ openclaw_repo_dir }}"
|
||||
executable: /bin/bash
|
||||
become: true
|
||||
become_user: "{{ openclaw_user }}"
|
||||
environment:
|
||||
PNPM_HOME: "{{ openclaw_home }}/.local/share/pnpm"
|
||||
PATH: "{{ openclaw_home }}/.local/bin:{{ openclaw_home }}/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin"
|
||||
HOME: "{{ openclaw_home }}"
|
||||
register: pnpm_build_result
|
||||
changed_when: true # Build always changes dist/ directory
|
||||
|
||||
- name: Display build output
|
||||
ansible.builtin.debug:
|
||||
msg: "Build completed successfully"
|
||||
when: pnpm_build_result.rc == 0
|
||||
|
||||
- name: Check if dist directory exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ openclaw_repo_dir }}/dist"
|
||||
register: dist_dir
|
||||
|
||||
- name: Fail if build didn't create dist directory
|
||||
ansible.builtin.fail:
|
||||
msg: "Build failed - dist directory not found"
|
||||
when: not dist_dir.stat.exists
|
||||
|
||||
- name: Check for package metadata
|
||||
ansible.builtin.stat:
|
||||
path: "{{ openclaw_repo_dir }}/package.json"
|
||||
register: openclaw_package_json_stat
|
||||
|
||||
- name: Read package metadata
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ openclaw_repo_dir }}/package.json"
|
||||
register: openclaw_package_json_raw
|
||||
when: openclaw_package_json_stat.stat.exists
|
||||
|
||||
- name: Parse package metadata
|
||||
ansible.builtin.set_fact:
|
||||
openclaw_package_json: "{{ openclaw_package_json_raw.content | b64decode | from_json }}"
|
||||
when: openclaw_package_json_stat.stat.exists
|
||||
|
||||
- name: Resolve metadata-defined CLI entrypoint
|
||||
ansible.builtin.set_fact:
|
||||
openclaw_package_bin_entry: >-
|
||||
{{
|
||||
openclaw_package_json.bin
|
||||
if openclaw_package_json.bin is string else
|
||||
(
|
||||
openclaw_package_json.bin.openclaw
|
||||
if openclaw_package_json.bin is mapping and 'openclaw' in openclaw_package_json.bin else
|
||||
(
|
||||
(openclaw_package_json.bin | dict2items | map(attribute='value') | list | first)
|
||||
if openclaw_package_json.bin is mapping else
|
||||
''
|
||||
)
|
||||
)
|
||||
}}
|
||||
when: openclaw_package_json_stat.stat.exists
|
||||
|
||||
- name: Build CLI entrypoint candidate list
|
||||
ansible.builtin.set_fact:
|
||||
openclaw_cli_candidate_paths: >-
|
||||
{{
|
||||
[
|
||||
openclaw_package_bin_entry | default(''),
|
||||
'openclaw.mjs',
|
||||
'bin/openclaw.js',
|
||||
'dist/index.js'
|
||||
] | reject('equalto', '') | unique | list
|
||||
}}
|
||||
|
||||
- name: Check possible OpenClaw CLI entrypoints
|
||||
ansible.builtin.stat:
|
||||
path: "{{ openclaw_repo_dir }}/{{ item }}"
|
||||
loop: "{{ openclaw_cli_candidate_paths }}"
|
||||
register: openclaw_cli_candidates
|
||||
|
||||
- name: Resolve OpenClaw CLI entrypoint path
|
||||
ansible.builtin.set_fact:
|
||||
openclaw_cli_entry: >-
|
||||
{{
|
||||
(
|
||||
openclaw_cli_candidates.results
|
||||
| selectattr('stat.exists')
|
||||
| map(attribute='stat.path')
|
||||
| list
|
||||
| first
|
||||
) | default('')
|
||||
}}
|
||||
changed_when: false
|
||||
|
||||
- name: Fail if openclaw CLI entrypoint is missing
|
||||
ansible.builtin.fail:
|
||||
msg: >-
|
||||
Unable to locate OpenClaw CLI entrypoint in {{ openclaw_repo_dir }}.
|
||||
Expected one of: {{ openclaw_cli_candidate_paths | join(', ') }}
|
||||
when: openclaw_cli_entry == ""
|
||||
|
||||
- name: Remove existing global openclaw symlink (if any)
|
||||
ansible.builtin.file:
|
||||
path: "{{ openclaw_home }}/.local/bin/openclaw"
|
||||
state: absent
|
||||
|
||||
- name: Create symlink to openclaw binary
|
||||
ansible.builtin.file:
|
||||
src: "{{ openclaw_cli_entry }}"
|
||||
dest: "{{ openclaw_home }}/.local/bin/openclaw"
|
||||
state: link
|
||||
owner: "{{ openclaw_user }}"
|
||||
group: "{{ openclaw_user }}"
|
||||
force: true
|
||||
|
||||
- name: Make openclaw binary executable
|
||||
ansible.builtin.file:
|
||||
path: "{{ openclaw_cli_entry }}"
|
||||
mode: '0755'
|
||||
owner: "{{ openclaw_user }}"
|
||||
group: "{{ openclaw_user }}"
|
||||
|
||||
- name: Verify openclaw installation from development build
|
||||
ansible.builtin.shell:
|
||||
cmd: openclaw --version
|
||||
executable: /bin/bash
|
||||
become: true
|
||||
become_user: "{{ openclaw_user }}"
|
||||
environment:
|
||||
PNPM_HOME: "{{ openclaw_home }}/.local/share/pnpm"
|
||||
PATH: "{{ openclaw_home }}/.local/bin:{{ openclaw_home }}/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin"
|
||||
HOME: "{{ openclaw_home }}"
|
||||
register: openclaw_dev_version
|
||||
changed_when: false
|
||||
|
||||
- name: Display installed OpenClaw version (development build)
|
||||
ansible.builtin.debug:
|
||||
msg: |
|
||||
OpenClaw installed from source: {{ openclaw_dev_version.stdout }}
|
||||
Repository: {{ openclaw_repo_dir }}
|
||||
Binary: {{ openclaw_home }}/.local/bin/openclaw -> {{ openclaw_cli_entry }}
|
||||
|
||||
- name: Add development mode info to .bashrc
|
||||
ansible.builtin.blockinfile:
|
||||
path: "{{ openclaw_home }}/.bashrc"
|
||||
marker: "# {mark} ANSIBLE MANAGED BLOCK - OpenClaw development"
|
||||
block: |
|
||||
# OpenClaw development mode
|
||||
export OPENCLAW_DEV_DIR="{{ openclaw_repo_dir }}"
|
||||
|
||||
# Aliases for development
|
||||
alias openclaw-rebuild='cd {{ openclaw_repo_dir }} && pnpm build'
|
||||
alias openclaw-dev='cd {{ openclaw_repo_dir }}'
|
||||
alias openclaw-pull='cd {{ openclaw_repo_dir }} && git pull && pnpm install && pnpm build'
|
||||
create: true
|
||||
owner: "{{ openclaw_user }}"
|
||||
group: "{{ openclaw_user }}"
|
||||
mode: '0644'
|
||||
32
ansible/roles/openclaw/tasks/openclaw-release.yml
Normal file
32
ansible/roles/openclaw/tasks/openclaw-release.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
# Release mode installation - Install via pnpm from npm registry
|
||||
|
||||
- name: Install OpenClaw globally as openclaw user (using pnpm)
|
||||
ansible.builtin.shell:
|
||||
cmd: pnpm install -g openclaw@latest
|
||||
executable: /bin/bash
|
||||
become: true
|
||||
become_user: "{{ openclaw_user }}"
|
||||
environment:
|
||||
PNPM_HOME: "{{ openclaw_home }}/.local/share/pnpm"
|
||||
PATH: "{{ openclaw_home }}/.local/bin:{{ openclaw_home }}/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin"
|
||||
HOME: "{{ openclaw_home }}"
|
||||
register: openclaw_install
|
||||
changed_when: "'Already up to date' not in openclaw_install.stdout"
|
||||
|
||||
- name: Verify openclaw installation
|
||||
ansible.builtin.shell:
|
||||
cmd: openclaw --version
|
||||
executable: /bin/bash
|
||||
become: true
|
||||
become_user: "{{ openclaw_user }}"
|
||||
environment:
|
||||
PNPM_HOME: "{{ openclaw_home }}/.local/share/pnpm"
|
||||
PATH: "{{ openclaw_home }}/.local/bin:{{ openclaw_home }}/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin"
|
||||
HOME: "{{ openclaw_home }}"
|
||||
register: openclaw_version
|
||||
changed_when: false
|
||||
|
||||
- name: Display installed OpenClaw version (release)
|
||||
ansible.builtin.debug:
|
||||
msg: "OpenClaw installed from npm: {{ openclaw_version.stdout }}"
|
||||
121
ansible/roles/openclaw/tasks/openclaw.yml
Normal file
121
ansible/roles/openclaw/tasks/openclaw.yml
Normal file
@@ -0,0 +1,121 @@
|
||||
---
|
||||
- name: Validate openclaw_install_mode
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- openclaw_install_mode in ["release", "development"]
|
||||
fail_msg: "Invalid openclaw_install_mode: '{{ openclaw_install_mode }}'. Must be 'release' or 'development'."
|
||||
success_msg: "Valid install mode: {{ openclaw_install_mode }}"
|
||||
|
||||
- name: Ensure openclaw home directory exists with correct ownership
|
||||
ansible.builtin.file:
|
||||
path: "{{ openclaw_home }}"
|
||||
state: directory
|
||||
owner: "{{ openclaw_user }}"
|
||||
group: "{{ openclaw_user }}"
|
||||
mode: '0755'
|
||||
|
||||
- name: Create OpenClaw directories (structure only, no config files)
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.path }}"
|
||||
state: directory
|
||||
owner: "{{ openclaw_user }}"
|
||||
group: "{{ openclaw_user }}"
|
||||
mode: "{{ item.mode }}"
|
||||
loop:
|
||||
- { path: "{{ openclaw_config_dir }}", mode: '0755' }
|
||||
- { path: "{{ openclaw_config_dir }}/sessions", mode: '0755' }
|
||||
- { path: "{{ openclaw_config_dir }}/credentials", mode: '0700' }
|
||||
- { path: "{{ openclaw_config_dir }}/data", mode: '0755' }
|
||||
- { path: "{{ openclaw_config_dir }}/logs", mode: '0755' }
|
||||
- { path: "{{ openclaw_config_dir }}/agents", mode: '0755' }
|
||||
- { path: "{{ openclaw_config_dir }}/agents/main", mode: '0755' }
|
||||
- { path: "{{ openclaw_config_dir }}/agents/main/agent", mode: '0700' }
|
||||
- { path: "{{ openclaw_config_dir }}/workspace", mode: '0755' }
|
||||
|
||||
- name: Create pnpm directories
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ openclaw_user }}"
|
||||
group: "{{ openclaw_user }}"
|
||||
mode: '0755'
|
||||
loop:
|
||||
- "{{ openclaw_home }}/.local/share/pnpm"
|
||||
- "{{ openclaw_home }}/.local/share/pnpm/store"
|
||||
- "{{ openclaw_home }}/.local/bin"
|
||||
|
||||
- name: Ensure pnpm directories have correct ownership
|
||||
ansible.builtin.file:
|
||||
path: "{{ openclaw_home }}/.local/share/pnpm"
|
||||
state: directory
|
||||
owner: "{{ openclaw_user }}"
|
||||
group: "{{ openclaw_user }}"
|
||||
recurse: true
|
||||
mode: '0755'
|
||||
|
||||
- name: Configure pnpm for openclaw user
|
||||
ansible.builtin.shell:
|
||||
cmd: |
|
||||
CURRENT_GLOBAL_DIR=$(pnpm config get global-dir 2>/dev/null || echo "")
|
||||
CURRENT_BIN_DIR=$(pnpm config get global-bin-dir 2>/dev/null || echo "")
|
||||
CHANGED=0
|
||||
if [ "$CURRENT_GLOBAL_DIR" != "{{ openclaw_home }}/.local/share/pnpm" ]; then
|
||||
pnpm config set global-dir {{ openclaw_home }}/.local/share/pnpm
|
||||
CHANGED=1
|
||||
fi
|
||||
if [ "$CURRENT_BIN_DIR" != "{{ openclaw_home }}/.local/bin" ]; then
|
||||
pnpm config set global-bin-dir {{ openclaw_home }}/.local/bin
|
||||
CHANGED=1
|
||||
fi
|
||||
exit $CHANGED
|
||||
executable: /bin/bash
|
||||
become: true
|
||||
become_user: "{{ openclaw_user }}"
|
||||
register: pnpm_config_result
|
||||
changed_when: pnpm_config_result.rc == 1
|
||||
failed_when: pnpm_config_result.rc > 1
|
||||
|
||||
- name: Display installation mode
|
||||
ansible.builtin.debug:
|
||||
msg: "Installation mode: {{ openclaw_install_mode }}"
|
||||
|
||||
# Include appropriate installation method based on mode
|
||||
- name: Include release installation (pnpm install -g)
|
||||
ansible.builtin.include_tasks: openclaw-release.yml
|
||||
when: openclaw_install_mode == "release"
|
||||
|
||||
- name: Include development installation (git clone + build + link)
|
||||
ansible.builtin.include_tasks: openclaw-development.yml
|
||||
when: openclaw_install_mode == "development"
|
||||
|
||||
- name: Configure .bashrc for openclaw user (base config)
|
||||
ansible.builtin.blockinfile:
|
||||
path: "{{ openclaw_home }}/.bashrc"
|
||||
marker: "# {mark} ANSIBLE MANAGED BLOCK - OpenClaw pnpm"
|
||||
block: |
|
||||
# pnpm configuration
|
||||
export PNPM_HOME="{{ openclaw_home }}/.local/share/pnpm"
|
||||
export PATH="{{ openclaw_home }}/.local/bin:$PNPM_HOME:$PATH"
|
||||
create: true
|
||||
owner: "{{ openclaw_user }}"
|
||||
group: "{{ openclaw_user }}"
|
||||
mode: '0644'
|
||||
insertafter: EOF
|
||||
|
||||
# NOTE: We do NOT create config.yml here - openclaw onboard/configure will do that
|
||||
# We also do NOT install the systemd service - openclaw onboard --install-daemon will do that
|
||||
# The .openclaw directory structure is created above, but config and daemon are user-initiated
|
||||
|
||||
- name: Display configuration note
|
||||
ansible.builtin.debug:
|
||||
msg: |
|
||||
OpenClaw is installed but NOT configured yet.
|
||||
|
||||
Next steps (run as openclaw user):
|
||||
1. Switch user: sudo su - {{ openclaw_user }}
|
||||
2. Run onboarding: openclaw onboard --install-daemon
|
||||
|
||||
This will:
|
||||
- Create configuration files (~/.openclaw/openclaw.json)
|
||||
- Guide you through provider setup
|
||||
- Install and start the daemon service automatically
|
||||
53
ansible/roles/openclaw/tasks/system-tools-linux.yml
Normal file
53
ansible/roles/openclaw/tasks/system-tools-linux.yml
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
# Linux-specific system tools installation (apt-based)
|
||||
|
||||
- name: Install essential system tools (Linux - apt)
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
# Editors
|
||||
- vim
|
||||
- nano
|
||||
# Version control
|
||||
- git
|
||||
- git-lfs
|
||||
# Network tools
|
||||
- curl
|
||||
- wget
|
||||
- netcat-openbsd
|
||||
- net-tools
|
||||
- dnsutils
|
||||
- iputils-ping
|
||||
- traceroute
|
||||
- tcpdump
|
||||
- nmap
|
||||
- socat
|
||||
- telnet
|
||||
# Debugging tools
|
||||
- strace
|
||||
- lsof
|
||||
- gdb
|
||||
- htop
|
||||
- iotop
|
||||
- iftop
|
||||
- sysstat
|
||||
- procps
|
||||
# System utilities
|
||||
- tmux
|
||||
- tree
|
||||
- jq
|
||||
- unzip
|
||||
- rsync
|
||||
- less
|
||||
# Build essentials for development
|
||||
- build-essential
|
||||
- file
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Deploy global vim configuration (Linux)
|
||||
ansible.builtin.template:
|
||||
src: vimrc.j2
|
||||
dest: /etc/vim/vimrc.local
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
25
ansible/roles/openclaw/tasks/system-tools.yml
Normal file
25
ansible/roles/openclaw/tasks/system-tools.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
# Main system tools orchestration - Linux only
|
||||
|
||||
- name: Include Linux system tools installation
|
||||
ansible.builtin.include_tasks: system-tools-linux.yml
|
||||
|
||||
# Common tasks for all operating systems
|
||||
|
||||
- name: Configure git globally
|
||||
community.general.git_config:
|
||||
name: "{{ item.name }}"
|
||||
scope: global
|
||||
value: "{{ item.value }}"
|
||||
loop:
|
||||
- { name: 'init.defaultBranch', value: 'main' }
|
||||
- { name: 'pull.rebase', value: 'false' }
|
||||
- { name: 'core.editor', value: 'vim' }
|
||||
- { name: 'color.ui', value: 'auto' }
|
||||
- { name: 'alias.st', value: 'status' }
|
||||
- { name: 'alias.co', value: 'checkout' }
|
||||
- { name: 'alias.br', value: 'branch' }
|
||||
- { name: 'alias.ci', value: 'commit' }
|
||||
- { name: 'alias.unstage', value: 'reset HEAD --' }
|
||||
- { name: 'alias.last', value: 'log -1 HEAD' }
|
||||
- { name: 'alias.lg', value: 'log --oneline --graph --decorate --all' }
|
||||
61
ansible/roles/openclaw/tasks/tailscale-linux.yml
Normal file
61
ansible/roles/openclaw/tasks/tailscale-linux.yml
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
# Linux-specific Tailscale installation (Debian/Ubuntu)
|
||||
|
||||
- name: Add Tailscale GPG key
|
||||
ansible.builtin.shell:
|
||||
cmd: |
|
||||
set -o pipefail
|
||||
DIST="{{ ansible_distribution | lower }}"
|
||||
RELEASE="{{ ansible_distribution_release }}"
|
||||
curl -fsSL "https://pkgs.tailscale.com/stable/${DIST}/${RELEASE}.noarmor.gpg" | \
|
||||
tee /usr/share/keyrings/tailscale-archive-keyring.gpg > /dev/null
|
||||
creates: /usr/share/keyrings/tailscale-archive-keyring.gpg
|
||||
executable: /bin/bash
|
||||
|
||||
- name: Add Tailscale repository
|
||||
ansible.builtin.shell:
|
||||
cmd: |
|
||||
set -o pipefail
|
||||
DIST="{{ ansible_distribution | lower }}"
|
||||
RELEASE="{{ ansible_distribution_release }}"
|
||||
curl -fsSL "https://pkgs.tailscale.com/stable/${DIST}/${RELEASE}.tailscale-keyring.list" | \
|
||||
tee /etc/apt/sources.list.d/tailscale.list > /dev/null
|
||||
creates: /etc/apt/sources.list.d/tailscale.list
|
||||
executable: /bin/bash
|
||||
|
||||
- name: Update apt cache after adding Tailscale repo
|
||||
ansible.builtin.apt:
|
||||
update_cache: true
|
||||
|
||||
- name: Install Tailscale
|
||||
ansible.builtin.apt:
|
||||
name: tailscale
|
||||
state: present
|
||||
|
||||
- name: Enable Tailscale service (Linux)
|
||||
ansible.builtin.systemd:
|
||||
name: tailscaled
|
||||
enabled: true
|
||||
state: started
|
||||
|
||||
- name: Check if Tailscale is already connected (Linux)
|
||||
ansible.builtin.command: tailscale status --json
|
||||
register: tailscale_status_linux
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Display Tailscale auth URL if not connected (Linux)
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "============================================"
|
||||
- "Tailscale installed but not connected yet"
|
||||
- "============================================"
|
||||
- ""
|
||||
- "To connect this machine to your Tailnet:"
|
||||
- "Run: sudo tailscale up"
|
||||
- ""
|
||||
- "For unattended installation, use an auth key:"
|
||||
- "sudo tailscale up --authkey tskey-auth-xxxxx"
|
||||
- ""
|
||||
- "Get auth key from: https://login.tailscale.com/admin/settings/keys"
|
||||
when: tailscale_status_linux.rc != 0
|
||||
192
ansible/roles/openclaw/tasks/user.yml
Normal file
192
ansible/roles/openclaw/tasks/user.yml
Normal file
@@ -0,0 +1,192 @@
|
||||
---
|
||||
- name: Create openclaw system user
|
||||
ansible.builtin.user:
|
||||
name: "{{ openclaw_user }}"
|
||||
comment: "OpenClaw Service User"
|
||||
system: true
|
||||
shell: /bin/bash
|
||||
create_home: true
|
||||
home: "{{ openclaw_home }}"
|
||||
state: present
|
||||
|
||||
- name: Ensure openclaw home directory has correct ownership
|
||||
ansible.builtin.file:
|
||||
path: "{{ openclaw_home }}"
|
||||
owner: "{{ openclaw_user }}"
|
||||
group: "{{ openclaw_user }}"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Configure .bashrc for openclaw user
|
||||
ansible.builtin.blockinfile:
|
||||
path: "{{ openclaw_home }}/.bashrc"
|
||||
marker: "# {mark} ANSIBLE MANAGED BLOCK - OpenClaw config"
|
||||
block: |
|
||||
# Enable 256 colors
|
||||
export TERM=xterm-256color
|
||||
export COLORTERM=truecolor
|
||||
|
||||
# Add pnpm to PATH
|
||||
export PNPM_HOME="{{ openclaw_home }}/.local/share/pnpm"
|
||||
export PATH="{{ openclaw_home }}/.local/bin:$PNPM_HOME:$PATH"
|
||||
|
||||
# Color support for common tools
|
||||
export CLICOLOR=1
|
||||
export LS_COLORS='di=34:ln=35:so=32:pi=33:ex=31:bd=34;46:cd=34;43:su=30;41:sg=30;46:tw=30;42:ow=30;43'
|
||||
|
||||
# Aliases
|
||||
alias ls='ls --color=auto'
|
||||
alias grep='grep --color=auto'
|
||||
alias ll='ls -lah'
|
||||
create: true
|
||||
owner: "{{ openclaw_user }}"
|
||||
group: "{{ openclaw_user }}"
|
||||
mode: '0644'
|
||||
|
||||
- name: Add openclaw user to sudoers with scoped NOPASSWD
|
||||
ansible.builtin.copy:
|
||||
dest: "/etc/sudoers.d/{{ openclaw_user }}"
|
||||
mode: '0440'
|
||||
owner: root
|
||||
group: root
|
||||
content: |
|
||||
# OpenClaw sudo permissions (scoped for security)
|
||||
#
|
||||
# SECURITY NOTE: These permissions are intentionally limited.
|
||||
# If openclaw is compromised, attackers can only:
|
||||
# - Manage the openclaw service
|
||||
# - Run basic tailscale diagnostics
|
||||
# - View openclaw logs
|
||||
#
|
||||
# To grant full tailscale control (e.g., for self-healing VPN):
|
||||
# {{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale *
|
||||
#
|
||||
# To grant full sudo (NOT RECOMMENDED):
|
||||
# {{ openclaw_user }} ALL=(ALL) NOPASSWD: ALL
|
||||
|
||||
# Service control - openclaw service only
|
||||
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl start openclaw
|
||||
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop openclaw
|
||||
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart openclaw
|
||||
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl status openclaw
|
||||
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl enable openclaw
|
||||
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl disable openclaw
|
||||
# daemon-reload affects all units (required after service file changes)
|
||||
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl daemon-reload
|
||||
|
||||
# Tailscale - diagnostics + connect/disconnect
|
||||
# NOTE: 'up' allows flags like --advertise-exit-node. For tighter control,
|
||||
# remove 'up' and 'down' lines - operator must then manage VPN manually.
|
||||
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale status
|
||||
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale up *
|
||||
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale down
|
||||
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale ip *
|
||||
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale version
|
||||
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale ping *
|
||||
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/tailscale whois *
|
||||
|
||||
# Journal access - openclaw logs only
|
||||
{{ openclaw_user }} ALL=(ALL) NOPASSWD: /usr/bin/journalctl -u openclaw *
|
||||
validate: /usr/sbin/visudo -cf %s
|
||||
|
||||
- name: Set openclaw user as primary user for installation
|
||||
ansible.builtin.set_fact:
|
||||
openclaw_user: "{{ openclaw_user }}"
|
||||
openclaw_home: "{{ openclaw_home }}"
|
||||
|
||||
- name: Create .bash_profile to source .bashrc for login shells
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ openclaw_home }}/.bash_profile"
|
||||
owner: "{{ openclaw_user }}"
|
||||
group: "{{ openclaw_user }}"
|
||||
mode: '0644'
|
||||
content: |
|
||||
# .bash_profile - Executed for login shells
|
||||
# Source .bashrc to ensure environment is loaded for login shells
|
||||
if [ -f ~/.bashrc ]; then
|
||||
. ~/.bashrc
|
||||
fi
|
||||
|
||||
# Fix DBus issues for systemd user services
|
||||
- name: Get openclaw user ID
|
||||
ansible.builtin.command: "id -u {{ openclaw_user }}"
|
||||
register: openclaw_uid
|
||||
changed_when: false
|
||||
when: ansible_os_family == 'Debian' and not ci_test
|
||||
|
||||
- name: Display openclaw user ID
|
||||
ansible.builtin.debug:
|
||||
msg: "OpenClaw user ID: {{ openclaw_uid.stdout }}"
|
||||
when: ansible_os_family == 'Debian' and not ci_test
|
||||
|
||||
- name: Enable lingering for openclaw user (allows systemd user services without login)
|
||||
ansible.builtin.command: "loginctl enable-linger {{ openclaw_user }}"
|
||||
changed_when: false
|
||||
when: ansible_os_family == 'Debian' and not ci_test
|
||||
|
||||
- name: Create runtime directory for openclaw user
|
||||
ansible.builtin.file:
|
||||
path: "/run/user/{{ openclaw_uid.stdout }}"
|
||||
state: directory
|
||||
owner: "{{ openclaw_user }}"
|
||||
group: "{{ openclaw_user }}"
|
||||
mode: '0700'
|
||||
when: ansible_os_family == 'Debian' and not ci_test
|
||||
|
||||
- name: Store openclaw UID as fact for later use
|
||||
ansible.builtin.set_fact:
|
||||
openclaw_uid_value: "{{ openclaw_uid.stdout }}"
|
||||
when: ansible_os_family == 'Debian' and not ci_test
|
||||
|
||||
# SSH key configuration
|
||||
- name: Create .ssh directory for openclaw user
|
||||
ansible.builtin.file:
|
||||
path: "{{ openclaw_home }}/.ssh"
|
||||
state: directory
|
||||
owner: "{{ openclaw_user }}"
|
||||
group: "{{ openclaw_user }}"
|
||||
mode: '0700'
|
||||
|
||||
- name: Add SSH authorized keys for openclaw user
|
||||
ansible.posix.authorized_key:
|
||||
user: "{{ openclaw_user }}"
|
||||
state: present
|
||||
key: "{{ item }}"
|
||||
loop: "{{ openclaw_ssh_keys }}"
|
||||
when: openclaw_ssh_keys | length > 0
|
||||
|
||||
- name: Display SSH key configuration status
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ openclaw_ssh_keys | length }} SSH key(s) configured for openclaw user"
|
||||
when: openclaw_ssh_keys | length > 0
|
||||
|
||||
- name: Display SSH key warning if none configured
|
||||
ansible.builtin.debug:
|
||||
msg: "No SSH keys configured. Set 'openclaw_ssh_keys' variable to allow SSH access."
|
||||
when: openclaw_ssh_keys | length == 0
|
||||
|
||||
- name: Set XDG_RUNTIME_DIR in .bashrc for openclaw user
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ openclaw_home }}/.bashrc"
|
||||
line: 'export XDG_RUNTIME_DIR=/run/user/$(id -u)'
|
||||
state: present
|
||||
create: true
|
||||
owner: "{{ openclaw_user }}"
|
||||
group: "{{ openclaw_user }}"
|
||||
mode: '0644'
|
||||
when: ansible_os_family == 'Debian' and not ci_test
|
||||
|
||||
- name: Set DBUS_SESSION_BUS_ADDRESS in .bashrc for openclaw user
|
||||
ansible.builtin.blockinfile:
|
||||
path: "{{ openclaw_home }}/.bashrc"
|
||||
marker: "# {mark} ANSIBLE MANAGED BLOCK - DBus config"
|
||||
block: |
|
||||
# DBus session bus configuration
|
||||
if [ -z "$DBUS_SESSION_BUS_ADDRESS" ]; then
|
||||
export DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus"
|
||||
fi
|
||||
create: true
|
||||
owner: "{{ openclaw_user }}"
|
||||
group: "{{ openclaw_user }}"
|
||||
mode: '0644'
|
||||
when: ansible_os_family == 'Debian' and not ci_test
|
||||
18
ansible/roles/openclaw/templates/daemon.json.j2
Normal file
18
ansible/roles/openclaw/templates/daemon.json.j2
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"iptables": true,
|
||||
"ip-forward": true,
|
||||
"userland-proxy": false,
|
||||
"live-restore": true,
|
||||
"ip6tables": false,
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"default-address-pools": [
|
||||
{
|
||||
"base": "172.17.0.0/12",
|
||||
"size": 24
|
||||
}
|
||||
]
|
||||
}
|
||||
76
ansible/roles/openclaw/templates/openclaw-config.yml.j2
Normal file
76
ansible/roles/openclaw/templates/openclaw-config.yml.j2
Normal file
@@ -0,0 +1,76 @@
|
||||
# OpenClaw Configuration Template
|
||||
# Generated by Ansible on {{ ansible_date_time.iso8601 }}
|
||||
#
|
||||
# For full documentation, visit: https://docs.openclaw.ai/configuration
|
||||
|
||||
# Connection Provider
|
||||
# Options: whatsapp, telegram, signal
|
||||
provider: whatsapp
|
||||
|
||||
# WhatsApp Configuration (if using whatsapp provider)
|
||||
whatsapp:
|
||||
# Phone number in international format (e.g., +4366412345678)
|
||||
phone: ""
|
||||
|
||||
# Telegram Configuration (if using telegram provider)
|
||||
telegram:
|
||||
# Telegram bot token from @BotFather
|
||||
token: ""
|
||||
|
||||
# Signal Configuration (if using signal provider)
|
||||
signal:
|
||||
# Signal phone number
|
||||
phone: ""
|
||||
|
||||
# AI Model Configuration
|
||||
ai:
|
||||
# Model provider: anthropic, openai
|
||||
provider: anthropic
|
||||
|
||||
# API Keys (set as environment variables or here)
|
||||
# anthropic_api_key: ""
|
||||
# openai_api_key: ""
|
||||
|
||||
# Model selection
|
||||
model: claude-3-5-sonnet-20241022
|
||||
|
||||
# Max tokens per response
|
||||
max_tokens: 4096
|
||||
|
||||
# Gateway Settings
|
||||
gateway:
|
||||
# Port for web interface
|
||||
port: {{ openclaw_port }}
|
||||
|
||||
# Enable web UI
|
||||
web_ui: true
|
||||
|
||||
# Logging
|
||||
logging:
|
||||
# Log level: debug, info, warn, error
|
||||
level: info
|
||||
|
||||
# Log file location
|
||||
file: {{ openclaw_config_dir }}/openclaw.log
|
||||
|
||||
# Security
|
||||
security:
|
||||
# Allowed phone numbers (whitelist)
|
||||
# Leave empty to allow all
|
||||
allowed_numbers: []
|
||||
|
||||
# Rate limiting
|
||||
rate_limit:
|
||||
enabled: true
|
||||
max_requests_per_minute: 10
|
||||
|
||||
# Advanced Settings
|
||||
advanced:
|
||||
# Session timeout in minutes
|
||||
session_timeout: 60
|
||||
|
||||
# Auto-reconnect on disconnect
|
||||
auto_reconnect: true
|
||||
|
||||
# Keep-alive interval in seconds
|
||||
keep_alive_interval: 30
|
||||
42
ansible/roles/openclaw/templates/openclaw-host.service.j2
Normal file
42
ansible/roles/openclaw/templates/openclaw-host.service.j2
Normal file
@@ -0,0 +1,42 @@
|
||||
[Unit]
|
||||
Description=OpenClaw AI Gateway
|
||||
After=network.target docker.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User={{ openclaw_user }}
|
||||
Group={{ openclaw_user }}
|
||||
WorkingDirectory={{ openclaw_home }}
|
||||
|
||||
# Environment variables
|
||||
Environment="PNPM_HOME={{ openclaw_home }}/.local/share/pnpm"
|
||||
Environment="PATH={{ openclaw_home }}/.local/bin:{{ openclaw_home }}/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin"
|
||||
Environment="HOME={{ openclaw_home }}"
|
||||
Environment="XDG_RUNTIME_DIR=/run/user/{{ openclaw_uid_value }}"
|
||||
|
||||
# DBus session bus
|
||||
Environment="DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{{ openclaw_uid_value }}/bus"
|
||||
|
||||
# Start command
|
||||
ExecStart=openclaw gateway
|
||||
|
||||
# Restart policy
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
ReadWritePaths={{ openclaw_home }}/.openclaw
|
||||
ReadWritePaths={{ openclaw_home }}/.local
|
||||
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=openclaw
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
42
ansible/roles/openclaw/templates/show-lobster.sh.j2
Normal file
42
ansible/roles/openclaw/templates/show-lobster.sh.j2
Normal file
@@ -0,0 +1,42 @@
|
||||
#jinja2: lstrip_blocks: True
|
||||
{% raw %}#!/bin/bash
|
||||
cat << 'LOBSTER'
|
||||
[0;36m
|
||||
+====================================================+
|
||||
| |
|
||||
| [0;33mWelcome to OpenClaw! [0;31m🦞[0;36m |
|
||||
| |
|
||||
|[0;31m ,.---._ [0;36m|
|
||||
|[0;31m ,,,, / `, [0;36m|
|
||||
|[0;31m \\\ / '\_ ; [0;36m|
|
||||
|[0;31m |||| /\/``-.__\;' [0;36m|
|
||||
|[0;31m ::::/\/_ [0;36m|
|
||||
|[0;31m {{`-.__.-'(`(^^(^^^(^ 9 `.=========' [0;36m|
|
||||
|[0;31m{{{{{{ { ( ( ( ( (-----:= [0;36m|
|
||||
|[0;31m {{.-'~~'-.(,(,,(,,,(__6_.'=========. [0;36m|
|
||||
|[0;31m ::::\/\ [0;36m|
|
||||
|[0;31m |||| \/\ ,-'/, [0;36m|
|
||||
|[0;31m //// \ `` _/ ; [0;36m|
|
||||
|[0;31m '''' \ ` .' [0;36m|
|
||||
|[0;31m `---' [0;36m|
|
||||
| |
|
||||
| [0;32m✅ Installation Successful![0;36m |
|
||||
| |
|
||||
+====================================================+[0m
|
||||
LOBSTER
|
||||
|
||||
echo ""
|
||||
echo "🔒 Security Status:"
|
||||
echo " - UFW Firewall: ENABLED"
|
||||
{% endraw %}
|
||||
{% if tailscale_enabled | default(false) %}
|
||||
echo " - Open Ports: SSH (22) + Tailscale (41641/udp)"
|
||||
{% else %}
|
||||
echo " - Open Ports: SSH (22)"
|
||||
{% endif %}
|
||||
{% raw %}
|
||||
echo " - Docker isolation: ACTIVE"
|
||||
echo ""
|
||||
echo "📚 Documentation: https://github.com/openclaw/openclaw-ansible"
|
||||
echo ""
|
||||
{% endraw %}
|
||||
136
ansible/roles/openclaw/templates/vimrc.j2
Normal file
136
ansible/roles/openclaw/templates/vimrc.j2
Normal file
@@ -0,0 +1,136 @@
|
||||
" Vim Configuration - Generated by Ansible
|
||||
" Modern, practical vim setup for development and debugging
|
||||
|
||||
" Basic Settings
|
||||
set nocompatible " Disable vi compatibility
|
||||
filetype plugin indent on " Enable file type detection
|
||||
syntax on " Enable syntax highlighting
|
||||
|
||||
" UI Settings
|
||||
set number " Show line numbers
|
||||
set relativenumber " Show relative line numbers
|
||||
set ruler " Show cursor position
|
||||
set showcmd " Show command in bottom bar
|
||||
set wildmenu " Visual autocomplete for command menu
|
||||
set showmatch " Highlight matching brackets
|
||||
set cursorline " Highlight current line
|
||||
set laststatus=2 " Always show status line
|
||||
set colorcolumn=80,120 " Show column markers
|
||||
|
||||
" Search Settings
|
||||
set incsearch " Search as characters are entered
|
||||
set hlsearch " Highlight search results
|
||||
set ignorecase " Case insensitive search
|
||||
set smartcase " Case sensitive when uppercase present
|
||||
|
||||
" Indentation
|
||||
set autoindent " Auto-indent new lines
|
||||
set smartindent " Smart indent
|
||||
set expandtab " Use spaces instead of tabs
|
||||
set tabstop=2 " Number of visual spaces per TAB
|
||||
set shiftwidth=2 " Number of spaces for auto-indent
|
||||
set softtabstop=2 " Number of spaces in tab when editing
|
||||
|
||||
" Performance
|
||||
set lazyredraw " Don't redraw while executing macros
|
||||
set ttyfast " Fast terminal connection
|
||||
|
||||
" Backups and Undo
|
||||
set nobackup " No backup files
|
||||
set nowritebackup " No backup while editing
|
||||
set noswapfile " No swap files
|
||||
set undofile " Persistent undo
|
||||
set undodir=~/.vim/undo " Undo directory
|
||||
set undolevels=1000 " Maximum number of undos
|
||||
set undoreload=10000 " Maximum lines to save for undo
|
||||
|
||||
" File Handling
|
||||
set encoding=utf-8 " Use UTF-8 encoding
|
||||
set fileencoding=utf-8 " File encoding
|
||||
set autoread " Auto-reload changed files
|
||||
set hidden " Allow hidden buffers
|
||||
|
||||
" Navigation
|
||||
set scrolloff=8 " Keep 8 lines above/below cursor
|
||||
set sidescrolloff=8 " Keep 8 columns left/right of cursor
|
||||
set mouse=a " Enable mouse support
|
||||
|
||||
" Folding
|
||||
set foldmethod=indent " Fold based on indentation
|
||||
set foldlevel=99 " Open all folds by default
|
||||
|
||||
" Status Line
|
||||
set statusline=%F " Full file path
|
||||
set statusline+=%m " Modified flag
|
||||
set statusline+=%r " Read-only flag
|
||||
set statusline+=%h " Help buffer flag
|
||||
set statusline+=%w " Preview window flag
|
||||
set statusline+=%= " Right align
|
||||
set statusline+=%y " File type
|
||||
set statusline+=\ [%{&ff}] " File format
|
||||
set statusline+=\ [%{strlen(&fenc)?&fenc:'none'}] " File encoding
|
||||
set statusline+=\ %l:%c " Line:Column
|
||||
set statusline+=\ %p%% " Percentage through file
|
||||
|
||||
" Key Mappings
|
||||
let mapleader = "," " Set leader key to comma
|
||||
|
||||
" Quick save
|
||||
nnoremap <leader>w :w<CR>
|
||||
|
||||
" Quick quit
|
||||
nnoremap <leader>q :q<CR>
|
||||
|
||||
" Clear search highlighting
|
||||
nnoremap <leader><space> :nohlsearch<CR>
|
||||
|
||||
" Split navigation
|
||||
nnoremap <C-h> <C-w>h
|
||||
nnoremap <C-j> <C-w>j
|
||||
nnoremap <C-k> <C-w>k
|
||||
nnoremap <C-l> <C-w>l
|
||||
|
||||
" Tab navigation
|
||||
nnoremap <leader>tn :tabnew<CR>
|
||||
nnoremap <leader>tc :tabclose<CR>
|
||||
nnoremap <leader>1 1gt
|
||||
nnoremap <leader>2 2gt
|
||||
nnoremap <leader>3 3gt
|
||||
nnoremap <leader>4 4gt
|
||||
nnoremap <leader>5 5gt
|
||||
|
||||
" Buffer navigation
|
||||
nnoremap <leader>bn :bnext<CR>
|
||||
nnoremap <leader>bp :bprevious<CR>
|
||||
nnoremap <leader>bd :bdelete<CR>
|
||||
|
||||
" Paste toggle
|
||||
set pastetoggle=<F2>
|
||||
|
||||
" File Type Specific
|
||||
autocmd FileType python setlocal tabstop=4 shiftwidth=4 softtabstop=4
|
||||
autocmd FileType javascript,typescript,json setlocal tabstop=2 shiftwidth=2 softtabstop=2
|
||||
autocmd FileType yaml,yml setlocal tabstop=2 shiftwidth=2 softtabstop=2
|
||||
autocmd FileType go setlocal tabstop=4 shiftwidth=4 softtabstop=4 noexpandtab
|
||||
autocmd FileType markdown setlocal wrap linebreak nolist
|
||||
|
||||
" Auto-create undo directory
|
||||
if !isdirectory($HOME."/.vim/undo")
|
||||
call mkdir($HOME."/.vim/undo", "p", 0700)
|
||||
endif
|
||||
|
||||
" Colors
|
||||
set background=dark
|
||||
if &term =~ "xterm" || &term =~ "screen"
|
||||
set t_Co=256
|
||||
endif
|
||||
|
||||
" Highlight trailing whitespace
|
||||
highlight ExtraWhitespace ctermbg=red guibg=red
|
||||
match ExtraWhitespace /\s\+$/
|
||||
|
||||
" Remember cursor position
|
||||
autocmd BufReadPost *
|
||||
\ if line("'\"") > 1 && line("'\"") <= line("$") |
|
||||
\ exe "normal! g`\"" |
|
||||
\ endif
|
||||
Reference in New Issue
Block a user