From 7450892a68de9eac0730fa236ce1883dded1f215 Mon Sep 17 00:00:00 2001 From: Ryan Blundon Date: Wed, 21 Oct 2026 11:17:14 -0500 Subject: [PATCH] added claude role --- ansible/inventory.yml | 8 +++ ansible/playbooks/install-claude.yml | 11 +++ ansible/roles/claude-code/README.md | 35 ++++++++++ ansible/roles/claude-code/defaults/main.yml | 39 +++++++++++ ansible/roles/claude-code/meta/main.yml | 15 ++++ ansible/roles/claude-code/tasks/apt.yml | 70 +++++++++++++++++++ ansible/roles/claude-code/tasks/main.yml | 44 ++++++++++++ ansible/roles/claude-code/tasks/native.yml | 20 ++++++ ansible/roles/claude-code/tasks/settings.yml | 22 ++++++ ansible/roles/claude-code/tasks/verify.yml | 17 +++++ .../roles/paperclip-docker/defaults/main.yml | 4 ++ ansible/roles/paperclip-docker/tasks/main.yml | 20 +++--- .../templates/docker-compose.yml.j2 | 2 +- 13 files changed, 296 insertions(+), 11 deletions(-) create mode 100644 ansible/playbooks/install-claude.yml create mode 100644 ansible/roles/claude-code/README.md create mode 100644 ansible/roles/claude-code/defaults/main.yml create mode 100644 ansible/roles/claude-code/meta/main.yml create mode 100644 ansible/roles/claude-code/tasks/apt.yml create mode 100644 ansible/roles/claude-code/tasks/main.yml create mode 100644 ansible/roles/claude-code/tasks/native.yml create mode 100644 ansible/roles/claude-code/tasks/settings.yml create mode 100644 ansible/roles/claude-code/tasks/verify.yml diff --git a/ansible/inventory.yml b/ansible/inventory.yml index 7d726a4..a43d970 100755 --- a/ansible/inventory.yml +++ b/ansible/inventory.yml @@ -63,6 +63,14 @@ astro_orbiter: hosts: astro-orbiter: +claude_servers: + hosts: + haunted-mansion: + ansible_host: 10.1.71.137 + ansible_user: wed + ansible_become: true + ansible_ssh_private_key_file: ~/.ssh/ansible + hermes_server: hosts: carousel-of-progress: diff --git a/ansible/playbooks/install-claude.yml b/ansible/playbooks/install-claude.yml new file mode 100644 index 0000000..496bced --- /dev/null +++ b/ansible/playbooks/install-claude.yml @@ -0,0 +1,11 @@ +# install-claude.yml +--- +# Usage: ansible-playbook -i inventory.ini install-claude.yml +- name: Install Claude Code on Ubuntu servers + hosts: claude_servers + become: false # the role escalates only where it needs to + roles: + - role: claude_code + vars: + claude_code_install_method: apt # or: native + claude_code_apt_channel: stable # or: latest diff --git a/ansible/roles/claude-code/README.md b/ansible/roles/claude-code/README.md new file mode 100644 index 0000000..12cbac1 --- /dev/null +++ b/ansible/roles/claude-code/README.md @@ -0,0 +1,35 @@ +# claude_code role + +Installs [Claude Code](https://code.claude.com/docs/en/setup) on Ubuntu 20.04+ / Debian 10+ (x64 or ARM64, 4 GB+ RAM). + +## Methods + +| `claude_code_install_method` | What it does | Updates | +|---|---|---| +| `apt` (default) | Adds Anthropic's signed apt repo (stable or latest channel) and installs the `claude-code` package system-wide. Verifies the signing-key fingerprint before trusting it. | `apt upgrade` / set `claude_code_package_state: latest` | +| `native` | Runs Anthropic's installer as `claude_code_user`; binary lands in `~/.local/bin/claude`. | Self-updates in the background | + +## Key variables (see `defaults/main.yml`) + +- `claude_code_apt_channel`: `stable` (about a week behind, skips bad releases) or `latest` +- `claude_code_package_state`: `present` or `latest` +- `claude_code_native_version`: `latest`, `stable`, or a pinned version like `2.1.89` +- `claude_code_settings`: optional dict written to `~/.claude/settings.json` (overwrites the file) + +## Use + +```bash +cp inventory.ini.example inventory.ini # edit hosts +ansible-playbook -i inventory.ini install-claude.yml --check # dry run +ansible-playbook -i inventory.ini install-claude.yml +``` + +The remote user needs sudo for the `apt` method. + +## After install: authenticate (not automated) + +Run `claude` once on the server and follow the login prompt, or export `ANTHROPIC_API_KEY` +for the user (store it in Ansible Vault, not in the repo). A Pro, Max, Team, Enterprise or +Console account is required; the free claude.ai plan does not include Claude Code. + +Verify with `claude --version` and `claude doctor`. diff --git a/ansible/roles/claude-code/defaults/main.yml b/ansible/roles/claude-code/defaults/main.yml new file mode 100644 index 0000000..03f8411 --- /dev/null +++ b/ansible/roles/claude-code/defaults/main.yml @@ -0,0 +1,39 @@ +# roles/claude_code/defaults/main.yml +--- +# How to install Claude Code: +# apt - signed Anthropic apt repo, system-wide, upgraded via apt (recommended for servers) +# native - Anthropic's installer script, per-user in ~/.local/bin, self-updating +claude_code_install_method: apt + +# ---- apt method ------------------------------------------------------------- +# Release channel: "stable" (~1 week behind, skips bad releases) or "latest" +claude_code_apt_channel: stable +# "present" installs once; "latest" upgrades on every run +claude_code_package_state: present + +claude_code_apt_key_url: https://downloads.claude.ai/keys/claude-code.asc +claude_code_apt_key_path: /etc/apt/keyrings/claude-code.asc +# Fingerprint published in Anthropic's docs; the role refuses to continue if it differs +claude_code_apt_key_fingerprint: "31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE" +claude_code_apt_repo_base: https://downloads.claude.ai/claude-code/apt + +# ---- native method ---------------------------------------------------------- +# User the native installer runs as +claude_code_user: "{{ ansible_user_id }}" +# "latest", "stable", or a specific version such as "2.1.89" +claude_code_native_version: latest + +# ---- common ----------------------------------------------------------------- +claude_code_prereq_packages: + - curl + - ca-certificates + - gnupg + +# Optional: content for ~/.claude/settings.json of claude_code_settings_user. +# Empty dict = don't touch the file. NOTE: when set, the file is overwritten. +# Example: { "autoUpdatesChannel": "stable", "env": { "DISABLE_AUTOUPDATER": "1" } } +claude_code_settings: {} +claude_code_settings_user: "{{ claude_code_user }}" + +# Run `claude --version` at the end and fail if it doesn't work +claude_code_verify: true diff --git a/ansible/roles/claude-code/meta/main.yml b/ansible/roles/claude-code/meta/main.yml new file mode 100644 index 0000000..9ab9f44 --- /dev/null +++ b/ansible/roles/claude-code/meta/main.yml @@ -0,0 +1,15 @@ +# roles/claude_code/meta/main.yml +--- +galaxy_info: + role_name: claude_code + author: ryan + description: Install Claude Code on Ubuntu/Debian servers (signed apt repo or native installer) + license: MIT + min_ansible_version: "2.14" + platforms: + - name: Ubuntu + versions: [focal, jammy, noble] + - name: Debian + versions: [buster, bullseye, bookworm] + galaxy_tags: [claude, ai, cli] +dependencies: [] diff --git a/ansible/roles/claude-code/tasks/apt.yml b/ansible/roles/claude-code/tasks/apt.yml new file mode 100644 index 0000000..1611c42 --- /dev/null +++ b/ansible/roles/claude-code/tasks/apt.yml @@ -0,0 +1,70 @@ +# roles/claude_code/tasks/apt.yml +--- +- name: Create apt keyrings directory + ansible.builtin.file: + path: /etc/apt/keyrings + state: directory + mode: "0755" + owner: root + group: root + become: true + +- name: Download Anthropic release signing key + ansible.builtin.get_url: + url: "{{ claude_code_apt_key_url }}" + dest: "{{ claude_code_apt_key_path }}" + mode: "0644" + owner: root + group: root + become: true + +- name: Read key fingerprint + ansible.builtin.shell: + cmd: | + set -o pipefail + gpg --show-keys --with-colons {{ claude_code_apt_key_path | quote }} | awk -F':' '/^fpr/ {print $10; exit}' + executable: /bin/bash + register: claude_code_key_fpr + changed_when: false + check_mode: false + become: true + +- name: Refuse to continue if the key fingerprint does not match + ansible.builtin.assert: + that: + - claude_code_key_fpr.stdout | trim | upper == claude_code_apt_key_fingerprint | upper + fail_msg: >- + Signing key fingerprint mismatch: got '{{ claude_code_key_fpr.stdout | trim }}', + expected '{{ claude_code_apt_key_fingerprint }}'. Removing the key file. + quiet: true + register: claude_code_key_check + ignore_errors: true + +- name: Remove untrusted key file + ansible.builtin.file: + path: "{{ claude_code_apt_key_path }}" + state: absent + become: true + when: claude_code_key_check is failed + +- name: Stop if key check failed + ansible.builtin.fail: + msg: "{{ claude_code_key_check.msg }}" + when: claude_code_key_check is failed + +- name: Configure Claude Code apt repository + ansible.builtin.apt_repository: + repo: >- + deb [signed-by={{ claude_code_apt_key_path }}] + {{ claude_code_apt_repo_base }}/{{ claude_code_apt_channel }} + {{ claude_code_apt_channel }} main + filename: claude-code + state: present + update_cache: true + become: true + +- name: Install claude-code package + ansible.builtin.apt: + name: claude-code + state: "{{ claude_code_package_state }}" + become: true diff --git a/ansible/roles/claude-code/tasks/main.yml b/ansible/roles/claude-code/tasks/main.yml new file mode 100644 index 0000000..9354f01 --- /dev/null +++ b/ansible/roles/claude-code/tasks/main.yml @@ -0,0 +1,44 @@ +# roles/claude_code/tasks/main.yml +--- +- name: Assert a supported install method + ansible.builtin.assert: + that: + - claude_code_install_method in ['apt', 'native'] + - claude_code_apt_channel in ['stable', 'latest'] + fail_msg: "claude_code_install_method must be apt|native and claude_code_apt_channel stable|latest" + quiet: true + +- name: Assert a supported OS (Ubuntu 20.04+ / Debian 10+) + ansible.builtin.assert: + that: + - ansible_facts['os_family'] == 'Debian' + - >- + (ansible_facts['distribution'] == 'Ubuntu' and ansible_facts['distribution_version'] is version('20.04', '>=')) + or (ansible_facts['distribution'] == 'Debian' and ansible_facts['distribution_version'] is version('10', '>=')) + - ansible_facts['architecture'] in ['x86_64', 'aarch64'] + fail_msg: "Claude Code needs Ubuntu 20.04+/Debian 10+ on x64 or ARM64" + quiet: true + +- name: Install prerequisite packages + ansible.builtin.apt: + name: "{{ claude_code_prereq_packages }}" + state: present + update_cache: true + cache_valid_time: 3600 + become: true + +- name: Install via apt repository + ansible.builtin.include_tasks: apt.yml + when: claude_code_install_method == 'apt' + +- name: Install via native installer + ansible.builtin.include_tasks: native.yml + when: claude_code_install_method == 'native' + +- name: Write Claude Code settings + ansible.builtin.include_tasks: settings.yml + when: claude_code_settings | length > 0 + +- name: Verify installation + ansible.builtin.include_tasks: verify.yml + when: claude_code_verify | bool diff --git a/ansible/roles/claude-code/tasks/native.yml b/ansible/roles/claude-code/tasks/native.yml new file mode 100644 index 0000000..0cfdafe --- /dev/null +++ b/ansible/roles/claude-code/tasks/native.yml @@ -0,0 +1,20 @@ +# roles/claude_code/tasks/native.yml +--- +- name: Install Claude Code with the native installer + ansible.builtin.shell: + cmd: set -o pipefail; curl -fsSL https://claude.ai/install.sh | bash -s {{ claude_code_native_version | quote }} + executable: /bin/bash + creates: "~{{ claude_code_user }}/.local/bin/claude" + become: true + become_user: "{{ claude_code_user }}" + +- name: Put ~/.local/bin on PATH for login shells + ansible.builtin.copy: + dest: /etc/profile.d/claude-code.sh + content: | + # Managed by Ansible (claude_code role) + case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) export PATH="$HOME/.local/bin:$PATH" ;; esac + mode: "0644" + owner: root + group: root + become: true diff --git a/ansible/roles/claude-code/tasks/settings.yml b/ansible/roles/claude-code/tasks/settings.yml new file mode 100644 index 0000000..2143cf6 --- /dev/null +++ b/ansible/roles/claude-code/tasks/settings.yml @@ -0,0 +1,22 @@ +# roles/claude_code/tasks/settings.yml +--- +- name: Look up settings user + ansible.builtin.getent: + database: passwd + key: "{{ claude_code_settings_user }}" + +- name: Create ~/.claude directory + ansible.builtin.file: + path: "{{ ansible_facts['getent_passwd'][claude_code_settings_user][4] }}/.claude" + state: directory + mode: "0700" + owner: "{{ claude_code_settings_user }}" + become: true + +- name: Write ~/.claude/settings.json + ansible.builtin.copy: + dest: "{{ ansible_facts['getent_passwd'][claude_code_settings_user][4] }}/.claude/settings.json" + content: "{{ claude_code_settings | to_nice_json }}\n" + mode: "0600" + owner: "{{ claude_code_settings_user }}" + become: true diff --git a/ansible/roles/claude-code/tasks/verify.yml b/ansible/roles/claude-code/tasks/verify.yml new file mode 100644 index 0000000..e746fd0 --- /dev/null +++ b/ansible/roles/claude-code/tasks/verify.yml @@ -0,0 +1,17 @@ +# roles/claude_code/tasks/verify.yml +--- +- name: Check claude --version + ansible.builtin.shell: + cmd: | + if command -v claude >/dev/null 2>&1; then claude --version + else "$HOME/.local/bin/claude" --version; fi + executable: /bin/bash + become: "{{ claude_code_install_method == 'native' }}" + become_user: "{{ claude_code_user if claude_code_install_method == 'native' else omit }}" + register: claude_code_version_out + changed_when: false + check_mode: false + +- name: Show installed version + ansible.builtin.debug: + msg: "{{ claude_code_version_out.stdout }}" diff --git a/ansible/roles/paperclip-docker/defaults/main.yml b/ansible/roles/paperclip-docker/defaults/main.yml index 5097bd7..2c5ea73 100644 --- a/ansible/roles/paperclip-docker/defaults/main.yml +++ b/ansible/roles/paperclip-docker/defaults/main.yml @@ -1,4 +1,8 @@ --- +# Paperclip Official image +paperclip_image: ghcr.io/paperclipai/paperclip +paperclip_version: "2026.1001.0" # no "v" prefix; also accepts stable, beta, nightly, canary, sha- + # Paperclip deployment paths and configuration paperclip_base_path: /opt/paperclip paperclip_data_dir: "{{ paperclip_base_path }}/data" diff --git a/ansible/roles/paperclip-docker/tasks/main.yml b/ansible/roles/paperclip-docker/tasks/main.yml index d2e3a93..463c8a7 100644 --- a/ansible/roles/paperclip-docker/tasks/main.yml +++ b/ansible/roles/paperclip-docker/tasks/main.yml @@ -77,16 +77,16 @@ become: true tags: [paperclip, files] -- name: Template OpenCode config - ansible.builtin.template: - src: opencode.jsonc.j2 - dest: "{{ paperclip_base_path }}/.config/opencode/opencode.jsonc" - mode: '0644' - owner: 1000 - group: 1000 - become: true - notify: Restart paperclip stack - tags: [paperclip, files] +# - name: Template OpenCode config +# ansible.builtin.template: +# src: opencode.jsonc.j2 +# dest: "{{ paperclip_base_path }}/.config/opencode/opencode.jsonc" +# mode: '0644' +# owner: 1000 +# group: 1000 +# become: true +# notify: Restart paperclip stack +# tags: [paperclip, files] # Idempotent: only changed services are recreated. remove_orphans also cleans # up the old paperclip-nginx / paperclip-certbot containers if they still exist. diff --git a/ansible/roles/paperclip-docker/templates/docker-compose.yml.j2 b/ansible/roles/paperclip-docker/templates/docker-compose.yml.j2 index ab05ae5..11670d7 100644 --- a/ansible/roles/paperclip-docker/templates/docker-compose.yml.j2 +++ b/ansible/roles/paperclip-docker/templates/docker-compose.yml.j2 @@ -19,7 +19,7 @@ services: retries: 5 paperclip: - image: paperclip-local:latest + image: {{ paperclip_image }}:{{ paperclip_version }} container_name: paperclip network_mode: host env_file: