248 lines
9.2 KiB
YAML
248 lines
9.2 KiB
YAML
---
|
|
# Deploy OpenClaw AI Gateway on a Proxmox VM
|
|
#
|
|
# OpenClaw: https://docs.openclaw.ai
|
|
# Ansible install docs: https://docs.openclaw.ai/install/ansible
|
|
# Signal channel docs: https://docs.openclaw.ai/channels/signal
|
|
#
|
|
# Prerequisites:
|
|
# Inventory host: openclaw.toal.ca (in group 'openclaw')
|
|
# host_vars required:
|
|
# openclaw_vm_ssh_public_key — SSH public key injected via cloud-init
|
|
# openclaw_vm_ip — static IP or 'dhcp'
|
|
# openclaw_vm_gateway — required for static IP
|
|
# openclaw_vm_vnet — Proxmox SDN VNet (e.g. lan)
|
|
#
|
|
# Vault secrets (1Password):
|
|
# vault_proxmox_token_secret — Proxmox API token
|
|
# vault_openclaw_api_key — Model provider API key (Anthropic, OpenAI, etc.)
|
|
# vault_openclaw_signal_phone — Signal account phone number (E.164, if Signal enabled)
|
|
#
|
|
# Security architecture:
|
|
# - OPNsense firewall provides perimeter security
|
|
# - UFW on VM: allow SSH (22) + gateway (18789); deny everything else inbound
|
|
# - Docker CE for agent sandbox isolation
|
|
# - Systemd hardening: NoNewPrivileges, PrivateTmp, ProtectSystem
|
|
#
|
|
# Signal channel MANUAL STEP required after deploy:
|
|
# sudo -i -u openclaw
|
|
# signal-cli link -n "OpenClaw" # scan QR with Signal app
|
|
# openclaw pairing approve signal
|
|
#
|
|
# Play order:
|
|
# Play 1: openclaw_create_vm — Create Ubuntu VM in Proxmox (cloud-init)
|
|
# Play 2: openclaw_wait — Wait for SSH to become available
|
|
# Play 3: openclaw_install — Install OpenClaw, security stack, Signal channel
|
|
#
|
|
# Usage:
|
|
# ansible-navigator run playbooks/deploy_openclaw.yml
|
|
# ansible-navigator run playbooks/deploy_openclaw.yml --tags openclaw_create_vm
|
|
# ansible-navigator run playbooks/deploy_openclaw.yml --tags openclaw_install
|
|
# ansible-navigator run playbooks/deploy_openclaw.yml --tags openclaw_install,openclaw_signal
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Play 1: Create Ubuntu VM in Proxmox using cloud-init
|
|
# ---------------------------------------------------------------------------
|
|
- name: Create OpenClaw VM in Proxmox
|
|
hosts: openclaw.toal.ca
|
|
gather_facts: false
|
|
connection: local
|
|
tags: openclaw_create_vm
|
|
|
|
vars:
|
|
# Proxmox connection — override in host_vars if needed
|
|
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
|
|
proxmox_iso_dir: /var/lib/vz/template/iso
|
|
# VM spec — override in host_vars for the openclaw inventory host
|
|
openclaw_vm_name: openclaw
|
|
openclaw_vm_id: 0
|
|
openclaw_vm_cpu: 2
|
|
openclaw_vm_memory_mb: 4096
|
|
openclaw_vm_disk_gb: 40
|
|
openclaw_vm_vnet: lan
|
|
openclaw_vm_user: ubuntu
|
|
openclaw_vm_ssh_public_key: "" # required — set in host_vars
|
|
openclaw_vm_ip: dhcp # set to x.x.x.x for static
|
|
openclaw_vm_prefix: 24
|
|
openclaw_vm_gateway: ""
|
|
openclaw_vm_nameserver: ""
|
|
openclaw_vm_cloud_image_url: "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img"
|
|
openclaw_vm_cloud_image_filename: noble-server-cloudimg-amd64.img
|
|
# Computed
|
|
__openclaw_proxmox_api_host: "{{ hostvars['proxmox_api']['ansible_host'] }}"
|
|
__openclaw_proxmox_api_port: "{{ hostvars['proxmox_api']['ansible_port'] }}"
|
|
|
|
tasks:
|
|
- name: Download Ubuntu 24.04 cloud image to Proxmox host
|
|
ansible.builtin.get_url:
|
|
url: "{{ openclaw_vm_cloud_image_url }}"
|
|
dest: "{{ proxmox_iso_dir }}/{{ openclaw_vm_cloud_image_filename }}"
|
|
mode: "0644"
|
|
delegate_to: proxmox_host
|
|
|
|
- name: Create VM definition
|
|
community.proxmox.proxmox_kvm:
|
|
api_host: "{{ __openclaw_proxmox_api_host }}"
|
|
api_user: "{{ proxmox_api_user }}"
|
|
api_port: "{{ __openclaw_proxmox_api_port }}"
|
|
api_token_id: "{{ proxmox_api_token_id }}"
|
|
api_token_secret: "{{ proxmox_api_token_secret }}"
|
|
validate_certs: "{{ proxmox_validate_certs }}"
|
|
node: "{{ proxmox_node }}"
|
|
vmid: "{{ openclaw_vm_id | default(omit, true) }}"
|
|
name: "{{ openclaw_vm_name }}"
|
|
cores: "{{ openclaw_vm_cpu }}"
|
|
memory: "{{ openclaw_vm_memory_mb }}"
|
|
cpu: host
|
|
machine: q35
|
|
bios: ovmf
|
|
efidisk0:
|
|
storage: "{{ proxmox_storage }}"
|
|
format: raw
|
|
efitype: 4m
|
|
pre_enrolled_keys: false
|
|
scsihw: virtio-scsi-single
|
|
net:
|
|
net0: "virtio,bridge={{ openclaw_vm_vnet }}"
|
|
boot: "order=scsi0"
|
|
onboot: true
|
|
state: present
|
|
|
|
- name: Retrieve VM info
|
|
community.proxmox.proxmox_vm_info:
|
|
api_host: "{{ __openclaw_proxmox_api_host }}"
|
|
api_user: "{{ proxmox_api_user }}"
|
|
api_port: "{{ __openclaw_proxmox_api_port }}"
|
|
api_token_id: "{{ proxmox_api_token_id }}"
|
|
api_token_secret: "{{ proxmox_api_token_secret }}"
|
|
validate_certs: "{{ proxmox_validate_certs }}"
|
|
node: "{{ proxmox_node }}"
|
|
name: "{{ openclaw_vm_name }}"
|
|
type: qemu
|
|
config: current
|
|
register: __openclaw_vm_info
|
|
retries: 5
|
|
|
|
- name: Set VM ID fact
|
|
ansible.builtin.set_fact:
|
|
openclaw_vm_id: "{{ __openclaw_vm_info.proxmox_vms[0].vmid }}"
|
|
cacheable: true
|
|
|
|
- name: Check if disk is already imported (scsi0 present in config)
|
|
ansible.builtin.set_fact:
|
|
__openclaw_disk_imported: "{{ __openclaw_vm_info.proxmox_vms[0].config.scsi0 is defined }}"
|
|
|
|
- name: Import cloud image as primary disk
|
|
ansible.builtin.command:
|
|
cmd: >-
|
|
qm importdisk {{ openclaw_vm_id }}
|
|
{{ proxmox_iso_dir }}/{{ openclaw_vm_cloud_image_filename }}
|
|
{{ proxmox_storage }} --format raw
|
|
delegate_to: proxmox_host
|
|
changed_when: true
|
|
when: not __openclaw_disk_imported | bool
|
|
|
|
- name: Attach imported disk as scsi0
|
|
ansible.builtin.command:
|
|
cmd: "qm set {{ openclaw_vm_id }} --scsi0 {{ proxmox_storage }}:vm-{{ openclaw_vm_id }}-disk-0,iothread=1,cache=writeback"
|
|
delegate_to: proxmox_host
|
|
changed_when: true
|
|
when: not __openclaw_disk_imported | bool
|
|
|
|
- name: Resize disk to configured size
|
|
ansible.builtin.command:
|
|
cmd: "qm disk resize {{ openclaw_vm_id }} scsi0 {{ openclaw_vm_disk_gb }}G"
|
|
delegate_to: proxmox_host
|
|
changed_when: true
|
|
when: not __openclaw_disk_imported | bool
|
|
|
|
- name: Add cloud-init drive
|
|
ansible.builtin.command:
|
|
cmd: "qm set {{ openclaw_vm_id }} --ide2 {{ proxmox_storage }}:cloudinit"
|
|
delegate_to: proxmox_host
|
|
changed_when: true
|
|
when: not __openclaw_disk_imported | bool
|
|
|
|
- name: Write SSH public key to temp file on Proxmox host
|
|
ansible.builtin.copy:
|
|
content: "{{ openclaw_vm_ssh_public_key }}"
|
|
dest: "/tmp/openclaw-sshkey-{{ openclaw_vm_id }}.pub"
|
|
mode: "0600"
|
|
delegate_to: proxmox_host
|
|
no_log: false
|
|
|
|
- name: Configure cloud-init user and SSH key
|
|
ansible.builtin.command:
|
|
cmd: >-
|
|
qm set {{ openclaw_vm_id }}
|
|
--ciuser {{ openclaw_vm_user }}
|
|
--sshkeys /tmp/openclaw-sshkey-{{ openclaw_vm_id }}.pub
|
|
delegate_to: proxmox_host
|
|
changed_when: true
|
|
|
|
- name: Configure cloud-init network (static)
|
|
ansible.builtin.command:
|
|
cmd: >-
|
|
qm set {{ openclaw_vm_id }}
|
|
--ipconfig0 ip={{ openclaw_vm_ip }}/{{ openclaw_vm_prefix }},gw={{ openclaw_vm_gateway }}
|
|
--nameserver {{ openclaw_vm_nameserver }}
|
|
delegate_to: proxmox_host
|
|
changed_when: true
|
|
when: openclaw_vm_ip != 'dhcp'
|
|
|
|
- name: Configure cloud-init network (DHCP)
|
|
ansible.builtin.command:
|
|
cmd: "qm set {{ openclaw_vm_id }} --ipconfig0 ip=dhcp"
|
|
delegate_to: proxmox_host
|
|
changed_when: true
|
|
when: openclaw_vm_ip == 'dhcp'
|
|
|
|
- name: Start VM
|
|
community.proxmox.proxmox_kvm:
|
|
api_host: "{{ __openclaw_proxmox_api_host }}"
|
|
api_user: "{{ proxmox_api_user }}"
|
|
api_port: "{{ __openclaw_proxmox_api_port }}"
|
|
api_token_id: "{{ proxmox_api_token_id }}"
|
|
api_token_secret: "{{ proxmox_api_token_secret }}"
|
|
validate_certs: "{{ proxmox_validate_certs }}"
|
|
node: "{{ proxmox_node }}"
|
|
name: "{{ openclaw_vm_name }}"
|
|
state: started
|
|
|
|
- name: Remove temporary SSH key file
|
|
ansible.builtin.file:
|
|
path: "/tmp/openclaw-sshkey-{{ openclaw_vm_id }}.pub"
|
|
state: absent
|
|
delegate_to: proxmox_host
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Play 2: Wait for VM to become reachable
|
|
# ---------------------------------------------------------------------------
|
|
- name: Wait for OpenClaw VM SSH
|
|
hosts: openclaw.toal.ca
|
|
gather_facts: false
|
|
tags: openclaw_create_vm
|
|
|
|
tasks:
|
|
- name: Wait for SSH port
|
|
ansible.builtin.wait_for_connection:
|
|
timeout: 300
|
|
sleep: 10
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Play 3: Install OpenClaw, security stack, and Signal channel
|
|
# ---------------------------------------------------------------------------
|
|
- name: Install and configure OpenClaw
|
|
hosts: openclaw.toal.ca
|
|
gather_facts: true
|
|
become: true
|
|
tags: openclaw_install
|
|
|
|
roles:
|
|
- role: openclaw
|