deploy hermes

This commit is contained in:
2026-05-25 20:21:24 -05:00
parent e309acd67d
commit be8e50d590
40 changed files with 2420 additions and 1159 deletions

View File

@@ -5,7 +5,7 @@
common_timezone: America/Chicago
common_ntp_server: 10.1.71.33 # sundial
common_ntp_server: "sundial.local.mk-labs.cloud"
common_packages:
- curl
@@ -20,3 +20,15 @@ common_packages:
- lsb-release
- net-tools
- dnsutils
- lvm2
# ------------------------------------------------------------------------------
# LVM root volume expansion
# Override common_root_pv/vg/lv per host if the template uses different names.
# Ubuntu 24.04 cloud templates use ubuntu-vg/ubuntu-lv by default.
# ------------------------------------------------------------------------------
common_expand_root_lvm: true
common_root_pv: /dev/sda3 # Ubuntu cloud-init partition layout
common_root_vg: ubuntu-vg
common_root_lv: ubuntu-lv

View File

@@ -1,12 +1,9 @@
---
# ------------------------------------------------------------------------------
# FILE: roles/common/handlers/main.yml
# ------------------------------------------------------------------------------
- name: restart chrony
systemd:
name: chrony
state: restarted
- name: restart sshd
systemd:
name: "{{ 'ssh' if ansible_os_family == 'Debian' else 'sshd' }}"
state: restarted

View File

@@ -1,8 +1,9 @@
---
# ------------------------------------------------------------------------------
# FILE: roles/common/tasks/main.yml
# DESCRIPTION: Baseline configuration applied to all managed hosts.
# Handles hostname, timezone, core packages, NTP, and
# ansible user setup.
# DESCRIPTION: Baseline configuration applied to all managed Ubuntu hosts.
# Handles hostname, timezone, core packages, NTP, ansible user,
# and optional LVM root volume expansion.
# ------------------------------------------------------------------------------
- name: Set hostname
@@ -10,7 +11,7 @@
name: "{{ inventory_hostname | replace('_', '-') }}"
- name: Set timezone
community.general.timezone:
timezone:
name: "{{ common_timezone }}"
- name: Update apt cache
@@ -41,7 +42,7 @@
state: started
enabled: yes
- name: Ensure ansible user has sudo without password
- name: Ensure ansible user has passwordless sudo
lineinfile:
path: /etc/sudoers.d/{{ ansible_user }}
line: "{{ ansible_user }} ALL=(ALL) NOPASSWD:ALL"
@@ -49,33 +50,28 @@
mode: '0440'
validate: 'visudo -cf %s'
# --- Step-CA SSH certificate enrollment ---
- name: Enroll host in step-ca SSH CA
include_tasks: step_ca_client.yml
# ------------------------------------------------------------------------------
# LVM root volume expansion
# Extends the root PV to the full disk size and grows the LV + filesystem.
# This codifies what was previously done manually after provisioning.
# Runs only when common_expand_root_lvm is true (default: true).
# Safe to re-run — pvresize and lvextend are idempotent when already at max.
# ------------------------------------------------------------------------------
- name: Create authorized principals directory
ansible.builtin.file:
path: /etc/ssh/auth_principals
state: directory
owner: root
group: root
mode: '0755'
- name: Expand root PV to full disk size
command: pvresize {{ common_root_pv }}
register: pvresize_result
changed_when: "'changed' in pvresize_result.stdout or pvresize_result.rc == 0"
when: common_expand_root_lvm | bool
- name: Create authorized principals files
ansible.builtin.copy:
content: "{{ item.principals | join('\n') }}\n"
dest: "/etc/ssh/auth_principals/{{ item.local_user }}"
owner: root
group: root
mode: '0644'
loop: "{{ step_ca_principal_mappings }}"
notify: restart sshd
- name: Configure sshd AuthorizedPrincipalsFile
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
line: "AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u"
regexp: "^#?AuthorizedPrincipalsFile"
state: present
notify: restart sshd
- name: Extend root LV to 100% of free VG space
lvol:
vg: "{{ common_root_vg }}"
lv: "{{ common_root_lv }}"
size: +100%FREE
resizefs: yes
when:
- common_expand_root_lvm | bool
ignore_errors: yes
# ignore_errors because lvextend returns non-zero when already at max size.
# resizefs: yes handles the resize2fs call inline — no separate task needed.

View File

@@ -1,38 +0,0 @@
---
# ------------------------------------------------------------------------------
# FILE: roles/monitoring/defaults/main.yml
# ------------------------------------------------------------------------------
# Directory layout on cinderella-castle
monitoring_base_dir: /opt/docker/monitoring
# Data disk — LVM on /dev/sdb, mounted at this path for Prometheus TSDB
prometheus_data_dir: /var/lib/prometheus
prometheus_vg: prometheus-vg
prometheus_lv: prometheus-lv
# Prometheus retention
prometheus_retention_time: 15d
prometheus_retention_size: 58GB # 64GB disk, leaving headroom
# Scrape interval
prometheus_scrape_interval: 30s
# Grafana
grafana_port: 3000
grafana_admin_user: admin
# Service DNS (via Traefik)
grafana_hostname: grafana.local.mk-labs.cloud
# Trusted proxy (lightning-lane)
trusted_proxy_ip: 10.1.71.35
# Prometheus port (internal, not exposed via Traefik)
prometheus_port: 9090
# Alertmanager port (internal)
alertmanager_port: 9093
# Node exporter port
node_exporter_port: 9100

View File

@@ -1,10 +0,0 @@
---
# ------------------------------------------------------------------------------
# FILE: roles/monitoring/handlers/main.yml
# ------------------------------------------------------------------------------
- name: restart monitoring
community.docker.docker_compose_v2:
project_src: "{{ monitoring_base_dir }}"
state: present
recreate: always

View File

@@ -1,203 +0,0 @@
---
# ------------------------------------------------------------------------------
# FILE: roles/monitoring/tasks/main.yml
# ------------------------------------------------------------------------------
# ── Data disk (LVM) ───────────────────────────────────────────────────────────
- name: Install LVM2
ansible.builtin.apt:
name: lvm2
state: present
update_cache: true
- name: Check if PV already exists on /dev/sdb
ansible.builtin.command: pvs /dev/sdb
register: pvs_result
changed_when: false
failed_when: false
- name: Check if disk has a partition table signature
ansible.builtin.command: wipefs /dev/sdb
register: wipefs_check
changed_when: false
failed_when: false
when: pvs_result.rc != 0
- name: Wipe GPT protective partition signature from /dev/sdb
ansible.builtin.command: wipefs -a /dev/sdb
when:
- pvs_result.rc != 0
- wipefs_check.stdout | length > 0
- name: Create LVM volume group on /dev/sdb
community.general.lvg:
vg: prometheus-vg
pvs: /dev/sdb
state: present
when: pvs_result.rc != 0
- name: Check if logical volume already exists
ansible.builtin.command: lvs /dev/prometheus-vg/prometheus-lv
register: lvs_result
changed_when: false
failed_when: false
- name: Create logical volume for Prometheus TSDB
community.general.lvol:
vg: prometheus-vg
lv: prometheus-lv
size: 100%FREE
state: present
shrink: false
when: lvs_result.rc != 0
- name: Format logical volume as ext4
community.general.filesystem:
fstype: ext4
dev: /dev/prometheus-vg/prometheus-lv
- name: Create Prometheus data directory mount point
ansible.builtin.file:
path: "{{ prometheus_data_dir }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Mount Prometheus logical volume
ansible.posix.mount:
path: "{{ prometheus_data_dir }}"
src: /dev/prometheus-vg/prometheus-lv
fstype: ext4
opts: defaults
state: mounted
- name: Set ownership of Prometheus data directory
ansible.builtin.file:
path: "{{ prometheus_data_dir }}"
state: directory
owner: "65534" # nobody — Prometheus container runs as this UID
group: "65534"
mode: "0755"
# ── Directory layout ───────────────────────────────────────────────────────────
- name: Create monitoring directory structure
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ ansible_user }}"
group: docker
mode: "0775"
loop:
- "{{ monitoring_base_dir }}"
- "{{ monitoring_base_dir }}/prometheus"
- "{{ monitoring_base_dir }}/grafana"
- "{{ monitoring_base_dir }}/alertmanager"
- "{{ monitoring_base_dir }}/pve-exporter"
- "{{ monitoring_base_dir }}/snmp-exporter"
# ── Config files ───────────────────────────────────────────────────────────────
- name: Deploy Prometheus configuration
ansible.builtin.template:
src: prometheus.yaml.j2
dest: "{{ monitoring_base_dir }}/prometheus/prometheus.yaml"
owner: "{{ ansible_user }}"
group: docker
mode: "0644"
notify: restart monitoring
- name: Deploy Alertmanager configuration
ansible.builtin.template:
src: alertmanager.yml.j2
dest: "{{ monitoring_base_dir }}/alertmanager/alertmanager.yml"
owner: "{{ ansible_user }}"
group: docker
mode: "0644"
notify: restart monitoring
- name: Deploy Grafana provisioning datasource
ansible.builtin.template:
src: grafana-datasource.yml.j2
dest: "{{ monitoring_base_dir }}/grafana/datasource.yml"
owner: "{{ ansible_user }}"
group: docker
mode: "0644"
notify: restart monitoring
- name: Deploy PVE exporter credentials file
ansible.builtin.template:
src: pve.yml.j2
dest: "{{ monitoring_base_dir }}/pve-exporter/pve.yml"
owner: "{{ ansible_user }}"
group: docker
mode: "0644"
no_log: true
notify: restart monitoring
# ── SNMP exporter config ───────────────────────────────────────────────────────
# Extract the default snmp.yml from the image (contains full if_mib metrics
# mapping) then inject the mk-labs community auth into the auths section.
- name: Extract default snmp.yml from snmp-exporter image
ansible.builtin.shell:
cmd: >
docker run --rm --entrypoint cat prom/snmp-exporter:latest
/etc/snmp_exporter/snmp.yml
> {{ monitoring_base_dir }}/snmp-exporter/snmp.yml
changed_when: true
- name: Inject mk_labs_v2c auth into snmp.yml
ansible.builtin.shell:
cmd: |
python3 - << 'EOF'
path = '{{ monitoring_base_dir }}/snmp-exporter/snmp.yml'
with open(path, 'r') as f:
content = f.read()
auth = (
' mk_labs_v2c:\n'
' community: {{ vault_snmp_community }}\n'
' security_level: noAuthNoPriv\n'
' auth_protocol: MD5\n'
' priv_protocol: DES\n'
' version: 2\n'
)
if 'mk_labs_v2c' not in content:
content = content.replace('\nmodules:\n', '\n' + auth + 'modules:\n', 1)
with open(path, 'w') as f:
f.write(content)
print('injected')
else:
print('already present')
EOF
register: snmp_inject
changed_when: "'injected' in snmp_inject.stdout"
no_log: true
notify: restart monitoring
# ── Compose stack ──────────────────────────────────────────────────────────────
- name: Deploy monitoring compose file from boilerplate
ansible.builtin.copy:
src: "{{ playbook_dir }}/../../boilerplates/monitoring/compose.yaml"
dest: "{{ monitoring_base_dir }}/compose.yaml"
owner: "{{ ansible_user }}"
group: docker
mode: "0644"
notify: restart monitoring
- name: Deploy .env with secrets from vault
ansible.builtin.template:
src: env.j2
dest: "{{ monitoring_base_dir }}/.env"
owner: "{{ ansible_user }}"
group: docker
mode: "0600"
notify: restart monitoring
- name: Start monitoring stack
community.docker.docker_compose_v2:
project_src: "{{ monitoring_base_dir }}"
state: present

View File

@@ -1,13 +0,0 @@
# ------------------------------------------------------------------------------
# FILE: roles/monitoring/templates/alertmanager.yml.j2
# DESCRIPTION: Alertmanager configuration — routing deferred (P.XX)
# ------------------------------------------------------------------------------
route:
receiver: blackhole
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receivers:
- name: blackhole

View File

@@ -1,11 +0,0 @@
# ------------------------------------------------------------------------------
# FILE: roles/monitoring/templates/env.j2
# DESCRIPTION: Environment file for monitoring compose stack
# Deployed to {{ monitoring_base_dir }}/.env
# ------------------------------------------------------------------------------
GRAFANA_ADMIN_USER={{ grafana_admin_user }}
GRAFANA_ADMIN_PASSWORD={{ grafana_admin_password }}
PROMETHEUS_RETENTION_TIME={{ prometheus_retention_time }}
PROMETHEUS_RETENTION_SIZE={{ prometheus_retention_size }}
TRUSTED_PROXIES={{ trusted_proxy_ip }}

View File

@@ -1,14 +0,0 @@
# ------------------------------------------------------------------------------
# FILE: roles/monitoring/templates/grafana-datasource.yml.j2
# DESCRIPTION: Auto-provisions Prometheus as Grafana default datasource
# ------------------------------------------------------------------------------
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false

View File

@@ -1,205 +0,0 @@
# ------------------------------------------------------------------------------
# FILE: ansible/playbooks/roles/monitoring/templates/prometheus.yaml.j2
# DESCRIPTION: Prometheus scrape configuration
# ------------------------------------------------------------------------------
global:
scrape_interval: 30s
evaluation_interval: 30s
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
rule_files: []
scrape_configs:
# Prometheus self-monitoring
- job_name: prometheus
static_configs:
- targets:
- localhost:9090
# cinderella-castle node exporter (this host)
- job_name: node-cinderella-castle
static_configs:
- targets:
- 10.1.71.31:9100
labels:
hostname: cinderella-castle
# Proxmox nodes (node exporter on each hypervisor)
- job_name: proxmox-nodes
static_configs:
- targets:
- 10.1.71.11:9100
- 10.1.71.12:9100
- 10.1.71.13:9100
labels:
job: proxmox
# Traefik metrics
- job_name: traefik
static_configs:
- targets:
- 10.1.71.35:8080
labels:
hostname: lightning-lane
# --------------------------------------------------------------------------
# Proxmox VE Exporter — cluster-level metrics (VM states, storage, CPU)
# Uses the multi-target pattern: Prometheus passes the cluster address as a
# query parameter to the exporter, which proxies to the Proxmox API.
# One scrape job covers all three nodes via the cluster API endpoint.
# --------------------------------------------------------------------------
- job_name: proxmox-cluster
metrics_path: /pve
params:
module: [default]
cluster: ["1"]
node: [""]
static_configs:
- targets:
- 10.1.71.11 # main-street-usa — cluster API endpoint
labels:
cluster: magic-kingdom
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: pve-exporter:9221
# --------------------------------------------------------------------------
# Pure Storage FlashArray -- utilidor (DEFERRED until Purity 6.7+)
#
# When utilidor is upgraded to Purity 6.7+, uncomment this job and remove
# the legacy exporter container. No container needed -- Prometheus scrapes
# the native OpenMetrics endpoint on the array directly over HTTPS.
#
# Also add vault_pure_utilidor_api_token to group_vars/all/vault and create
# a read-only API token on utilidor under System -> Users -> API Tokens.
# --------------------------------------------------------------------------
# - job_name: pure-flasharray-utilidor
# metrics_path: /metrics/array
# scheme: https
# tls_config:
# insecure_skip_verify: true
# authorization:
# credentials: {{ vault_pure_utilidor_api_token }}
# params:
# namespace: ["purefa"]
# static_configs:
# - targets:
# - utilidor.local.mk-labs.cloud
# labels:
# array: utilidor
# array_type: physical
# --------------------------------------------------------------------------
# UniFi Network — SNMP scrape jobs (one per device for clean Grafana filtering)
# All targets use the if_mib module for interface throughput, errors, and
# up/down state. Auth uses the mk_labs_v2c community string from vault.
# --------------------------------------------------------------------------
- job_name: snmp-udm-pro
metrics_path: /snmp
params:
auth: [mk_labs_v2c]
module: [if_mib]
static_configs:
- targets:
- 192.168.1.1
labels:
device: world-drive
model: UDM-Pro
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: 10.1.71.31:9116
- job_name: snmp-usw-pro-aggregation
metrics_path: /snmp
params:
auth: [mk_labs_v2c]
module: [if_mib]
static_configs:
- targets:
- 192.168.1.123
labels:
device: usw-pro-aggregation
model: USW-Pro-Aggregation
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: 10.1.71.31:9116
- job_name: snmp-usw-pro-max-24
metrics_path: /snmp
scrape_interval: 60s
scrape_timeout: 55s
params:
auth: [mk_labs_v2c]
module: [if_mib]
static_configs:
- targets:
- 192.168.1.226
labels:
device: usw-pro-max-24
model: USW-Pro-Max-24
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: 10.1.71.31:9116
- job_name: snmp-usw-lite-16-poe
metrics_path: /snmp
params:
auth: [mk_labs_v2c]
module: [if_mib]
static_configs:
- targets:
- 192.168.1.136
labels:
device: usw-lite-16-poe
model: USW-Lite-16-PoE
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: 10.1.71.31:9116
- job_name: snmp-ups-tower
metrics_path: /snmp
params:
auth: [mk_labs_v2c]
module: [if_mib]
static_configs:
- targets:
- 192.168.1.233
labels:
device: ups-tower
model: UPS-Tower
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: 10.1.71.31:9116

View File

@@ -1,11 +0,0 @@
# ------------------------------------------------------------------------------
# FILE: ansible/playbooks/roles/monitoring/templates/pve.yml.j2
# DESCRIPTION: Proxmox VE Exporter credentials file.
# Mounted read-only into the pve-exporter container.
# ------------------------------------------------------------------------------
default:
user: prometheus@pve
token_name: prometheus-token
token_value: {{ vault_proxmox_prometheus_token }}
verify_ssl: false

View File

@@ -0,0 +1,28 @@
---
# ------------------------------------------------------------------------------
# FILE: roles/ollama/defaults/main.yml
# ------------------------------------------------------------------------------
# ROCm
ollama_rocm_version: "6.2" # ROCm release stream — update when upgrading
ollama_rocm_packages:
- hip-runtime-amd # Pulls in full ROCm stack
- rocminfo # GPU visibility verification tool
- rocm-smi-lib # ROCm system management interface
# Ollama
ollama_version: "latest"
ollama_port: 11434
ollama_host: "0.0.0.0" # Listen on all interfaces for cluster access
ollama_default_model: "qwen3:8b"
# HSA override — required for RX 5000-series (gfx1010 / RDNA 1)
# Tells ROCm to treat the GPU as gfx1010 which has supported LLVM targets.
ollama_hsa_override_gfx_version: "10.1.0"
# Data disk
ollama_setup_data_disk: true
ollama_data_disk: /dev/sdb
ollama_data_dir: /var/lib/ollama # Ollama default models path
ollama_data_vg: ollama-vg
ollama_data_lv: ollama-lv

View File

@@ -0,0 +1,20 @@
---
# ------------------------------------------------------------------------------
# FILE: roles/ollama/handlers/main.yml
# ------------------------------------------------------------------------------
- name: update apt cache
apt:
update_cache: yes
- name: update initramfs
command: update-initramfs -u
- name: reload systemd
systemd:
daemon_reload: yes
- name: restart ollama
systemd:
name: ollama
state: restarted

View File

@@ -0,0 +1,229 @@
---
# ------------------------------------------------------------------------------
# FILE: roles/ollama/tasks/main.yml
# DESCRIPTION: Deploys Ollama with AMD ROCm GPU acceleration on Ubuntu 24.04.
# Handles:
# - ROCm repository and driver stack installation
# - Data disk preparation (LVM, ext4, persistent mount)
# - Ollama installation and systemd service configuration
# - HSA override for RX 5000-series (gfx1010) compatibility
# - Initial model pull
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# ROCm prerequisites
# ------------------------------------------------------------------------------
- name: Install ROCm prerequisite packages
apt:
name:
- wget
- gnupg
- ca-certificates
- lvm2
state: present
update_cache: yes
- name: Add ROCm apt repository signing key
apt_key:
url: https://repo.radeon.com/rocm/rocm.gpg.key
state: present
- name: Add ROCm apt repository
apt_repository:
repo: "deb [arch=amd64] https://repo.radeon.com/rocm/apt/{{ ollama_rocm_version }} {{ ansible_distribution_release }} main"
state: present
filename: rocm
notify: update apt cache
- name: Flush handlers to update apt cache before ROCm install
meta: flush_handlers
# ------------------------------------------------------------------------------
# ROCm driver stack
# hip-runtime-amd pulls in the full ROCm stack as a dependency.
# rocminfo is used to verify GPU visibility after install.
# ------------------------------------------------------------------------------
- name: Install ROCm packages
apt:
name: "{{ ollama_rocm_packages }}"
state: present
notify: update initramfs
- name: Create /opt/rocm symlink to versioned directory
file:
src: "/opt/rocm-{{ ollama_rocm_version }}.0"
dest: /opt/rocm
state: link
force: false
- name: Add ROCm binaries to system PATH
copy:
dest: /etc/profile.d/rocm.sh
content: |
export PATH=$PATH:/opt/rocm/bin
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/rocm/lib
mode: '0644'
- name: Add wed user to render and video groups for GPU access
user:
name: "{{ ansible_user }}"
groups:
- render
- video
append: yes
- name: Add ollama user to render and video groups (created by Ollama installer)
user:
name: ollama
groups:
- render
- video
append: yes
ignore_errors: yes
# ollama user does not exist yet at this point — Ollama installer creates it.
# This task re-runs after install via the post-install handler.
# ------------------------------------------------------------------------------
# Data disk — LVM setup on /dev/sdb
# Follows the same pattern as the monitoring role (cinderella-castle).
# Wipes any stale GPT/partition signatures before creating the PV to avoid
# the "GPT protective partition" error encountered during monitoring deploy.
# ------------------------------------------------------------------------------
- name: Wipe any existing partition signatures from data disk
command: wipefs -a {{ ollama_data_disk }}
args:
creates: /dev/{{ ollama_data_vg }}
when: ollama_setup_data_disk | bool
- name: Create LVM physical volume on data disk
command: pvcreate {{ ollama_data_disk }}
args:
creates: /dev/{{ ollama_data_vg }}
when: ollama_setup_data_disk | bool
- name: Create LVM volume group
lvg:
vg: "{{ ollama_data_vg }}"
pvs: "{{ ollama_data_disk }}"
when: ollama_setup_data_disk | bool
- name: Create LVM logical volume using 100% of VG
lvol:
vg: "{{ ollama_data_vg }}"
lv: "{{ ollama_data_lv }}"
size: 100%FREE
shrink: false
when: ollama_setup_data_disk | bool
- name: Format logical volume as ext4
filesystem:
fstype: ext4
dev: "/dev/{{ ollama_data_vg }}/{{ ollama_data_lv }}"
opts: "-L ollama-data"
when: ollama_setup_data_disk | bool
- name: Create Ollama model directory mount point
file:
path: "{{ ollama_data_dir }}"
state: directory
mode: '0755'
- name: Mount Ollama data volume and add fstab entry
mount:
path: "{{ ollama_data_dir }}"
src: "/dev/{{ ollama_data_vg }}/{{ ollama_data_lv }}"
fstype: ext4
opts: defaults,noatime
state: mounted
when: ollama_setup_data_disk | bool
# noatime: eliminates inode access time writes on every model file read.
# Meaningful win for large sequential reads like model loading.
# ------------------------------------------------------------------------------
# Ollama installation
# Uses the official install script which creates the ollama user and
# systemd service unit. We override the service with our own config.
# ------------------------------------------------------------------------------
- name: Download Ollama install script
get_url:
url: https://ollama.ai/install.sh
dest: /tmp/ollama_install.sh
mode: '0755'
- name: Run Ollama install script
command: /tmp/ollama_install.sh
environment: "{{ {} if ollama_version == 'latest' else {'OLLAMA_VERSION': ollama_version} }}"
args:
creates: /usr/local/bin/ollama
- name: Add ollama user to render and video groups (post-install)
user:
name: ollama
groups:
- render
- video
append: yes
- name: Set ownership of Ollama data directory to ollama user
file:
path: "{{ ollama_data_dir }}"
owner: ollama
group: ollama
recurse: yes
# ------------------------------------------------------------------------------
# Systemd service override
# Injects HSA_OVERRIDE_GFX_VERSION for RX 5000-series compatibility and
# sets OLLAMA_MODELS to the data disk mount point.
# ------------------------------------------------------------------------------
- name: Create systemd override directory for ollama service
file:
path: /etc/systemd/system/ollama.service.d
state: directory
mode: '0755'
- name: Deploy ollama systemd service override
template:
src: ollama-override.conf.j2
dest: /etc/systemd/system/ollama.service.d/override.conf
mode: '0644'
notify:
- reload systemd
- restart ollama
- name: Flush handlers to apply service config before model pull
meta: flush_handlers
- name: Enable and start Ollama service
systemd:
name: ollama
state: started
enabled: yes
daemon_reload: yes
- name: Wait for Ollama API to become available
uri:
url: "http://localhost:{{ ollama_port }}/api/tags"
status_code: 200
register: ollama_health
retries: 12
delay: 5
until: ollama_health.status == 200
# ------------------------------------------------------------------------------
# Initial model pull
# ------------------------------------------------------------------------------
- name: Pull initial model
command: ollama pull {{ ollama_default_model }}
environment:
OLLAMA_HOST: "http://localhost:{{ ollama_port }}"
register: model_pull
changed_when: "'pulled' in model_pull.stdout or model_pull.rc == 0"
timeout: 600
# Qwen3 8B Q4_K_M is ~5GB — allow 10 minutes on first pull.

View File

@@ -0,0 +1,13 @@
# ------------------------------------------------------------------------------
# FILE: roles/ollama/templates/ollama-override.conf.j2
# DESCRIPTION: Systemd drop-in for the ollama service.
# Sets environment variables required for AMD GPU operation
# and configures the model storage path.
# Managed by Ansible — do not edit manually.
# ------------------------------------------------------------------------------
[Service]
Environment="OLLAMA_HOST={{ ollama_host }}:{{ ollama_port }}"
Environment="OLLAMA_MODELS={{ ollama_data_dir }}"
Environment="HSA_OVERRIDE_GFX_VERSION={{ ollama_hsa_override_gfx_version }}"
Environment="ROCR_VISIBLE_DEVICES=0"