Remove unused indri tags and ansible roles (#41)

## Summary
- Remove ansible roles for services migrated to k8s: devpi, kiwix, transmission
- Also remove unused node_exporter and podman ansible roles
- Remove service tags from indri for k8s-hosted services (grafana, kiwix, devpi, pg, feed)
- Update indri description to reflect current architecture

## Changes
**Ansible roles removed** (34 files, ~1000 lines):
- devpi, devpi_metrics
- kiwix
- transmission, transmission_metrics
- node_exporter
- podman

**Pulumi indri tags removed**:
- tag:grafana, tag:kiwix, tag:devpi, tag:pg, tag:feed

These services now run in k8s with their own Tailscale devices via tailscale-operator.

## Deployment and Testing
- [x] Verified remaining ansible roles match indri.yml
- [x] Verified no playbooks or role dependencies reference removed roles
- [ ] Run `pulumi preview` to verify tag changes
- [ ] Run `pulumi up` to apply tag changes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: https://forge.tail8d86e.ts.net/eblume/blumeops/pulls/41
This commit is contained in:
Erich Blume 2026-01-21 20:18:53 -08:00
commit 5a829e0afd
36 changed files with 10 additions and 1079 deletions

View file

@ -1,7 +0,0 @@
---
devpi_port: 3141
devpi_serverdir: /Users/erichblume/devpi
devpi_log_dir: /Users/erichblume/Library/Logs
devpi_host: 0.0.0.0 # Listen on all interfaces for Tailscale
devpi_outside_url: https://pypi.tail8d86e.ts.net # URL for Tailscale proxy
devpi_secretfile: /Users/erichblume/devpi/.secret # Persistent auth secret

View file

@ -1,6 +0,0 @@
---
- name: Reload devpi
ansible.builtin.shell: |
launchctl unload ~/Library/LaunchAgents/mcquack.eblume.devpi.plist 2>/dev/null || true
launchctl load ~/Library/LaunchAgents/mcquack.eblume.devpi.plist
changed_when: true

View file

@ -1,55 +0,0 @@
---
# Note: devpi is installed via mise (pipx/uvx), not managed here.
#
# ONE-TIME SETUP (before running ansible):
#
# 1. Add to ~/.config/mise/config.toml on indri:
#
# [tools]
# "pipx:devpi-server" = { version = "latest", uvx = "true", uvx_args = "--with devpi-web" }
# "pipx:devpi-client" = { version = "latest", uvx = "true" }
#
# 2. Install: mise install
#
# 3. Initialize with root password (generate password in 1password):
# mise x -- devpi-init --serverdir {{ devpi_serverdir }} --root-passwd YOUR_PASSWORD
#
# 4. Run ansible to deploy LaunchAgent
#
# 5. Set up Tailscale service (see management log)
- name: Ensure devpi data directory exists
ansible.builtin.file:
path: "{{ devpi_serverdir }}"
state: directory
mode: '0755'
- name: Generate devpi secret file if not exists
ansible.builtin.shell: |
openssl rand -hex 32 > "{{ devpi_secretfile }}"
args:
creates: "{{ devpi_secretfile }}"
- name: Ensure devpi secret file has secure permissions
ansible.builtin.file:
path: "{{ devpi_secretfile }}"
mode: '0600'
- name: Deploy devpi LaunchAgent plist
ansible.builtin.template:
src: devpi.plist.j2
dest: ~/Library/LaunchAgents/mcquack.eblume.devpi.plist
mode: '0644'
notify: Reload devpi
- name: Check if devpi LaunchAgent is loaded
ansible.builtin.command: launchctl list mcquack.eblume.devpi
register: devpi_launchctl_check
changed_when: false
failed_when: false
- name: Load devpi LaunchAgent if not loaded
ansible.builtin.command: launchctl load ~/Library/LaunchAgents/mcquack.eblume.devpi.plist
when: devpi_launchctl_check.rc != 0
changed_when: true
failed_when: false

View file

@ -1,39 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- {{ ansible_managed }} -->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>KeepAlive</key>
<true/>
<key>Label</key>
<string>mcquack.eblume.devpi</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/bin:/bin</string>
</dict>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/opt/mise/bin/mise</string>
<string>x</string>
<string>--</string>
<string>devpi-server</string>
<string>--serverdir</string>
<string>{{ devpi_serverdir }}</string>
<string>--host</string>
<string>{{ devpi_host }}</string>
<string>--port</string>
<string>{{ devpi_port }}</string>
<string>--outside-url</string>
<string>{{ devpi_outside_url }}</string>
<string>--secretfile</string>
<string>{{ devpi_secretfile }}</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>{{ devpi_log_dir }}/mcquack.devpi.err.log</string>
<key>StandardOutPath</key>
<string>{{ devpi_log_dir }}/mcquack.devpi.out.log</string>
</dict>
</plist>

View file

@ -1,6 +0,0 @@
---
devpi_metrics_url: http://localhost:3141/+status
devpi_metrics_dir: /opt/homebrew/var/node_exporter/textfile
devpi_metrics_script: /Users/erichblume/bin/devpi-metrics
devpi_metrics_interval: 60 # seconds between metric collection
devpi_metrics_log_dir: /opt/homebrew/var/log

View file

@ -1,6 +0,0 @@
---
- name: Reload devpi-metrics
ansible.builtin.shell: |
launchctl unload ~/Library/LaunchAgents/mcquack.eblume.devpi-metrics.plist 2>/dev/null || true
launchctl load ~/Library/LaunchAgents/mcquack.eblume.devpi-metrics.plist
changed_when: true

View file

@ -1,4 +0,0 @@
---
# Role ordering is controlled by indri.yml playbook - do not add dependencies here
# (Ansible's tag accumulation prevents proper deduplication when using meta dependencies)
dependencies: []

View file

@ -1,37 +0,0 @@
---
- name: Ensure metrics directory exists
ansible.builtin.file:
path: "{{ devpi_metrics_dir }}"
state: directory
mode: '0755'
- name: Ensure log directory exists
ansible.builtin.file:
path: "{{ devpi_metrics_log_dir }}"
state: directory
mode: '0755'
- name: Deploy devpi-metrics script
ansible.builtin.template:
src: devpi-metrics.sh.j2
dest: "{{ devpi_metrics_script }}"
mode: '0755'
- name: Deploy devpi-metrics LaunchAgent plist
ansible.builtin.template:
src: devpi-metrics.plist.j2
dest: ~/Library/LaunchAgents/mcquack.eblume.devpi-metrics.plist
mode: '0644'
notify: Reload devpi-metrics
- name: Check if devpi-metrics LaunchAgent is loaded
ansible.builtin.command: launchctl list mcquack.eblume.devpi-metrics
register: devpi_metrics_launchctl_check
changed_when: false
failed_when: false
- name: Load devpi-metrics LaunchAgent if not loaded
ansible.builtin.command: launchctl load ~/Library/LaunchAgents/mcquack.eblume.devpi-metrics.plist
when: devpi_metrics_launchctl_check.rc != 0
changed_when: true
failed_when: false

View file

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- {{ ansible_managed }} -->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>mcquack.eblume.devpi-metrics</string>
<key>ProgramArguments</key>
<array>
<string>{{ devpi_metrics_script }}</string>
</array>
<key>StartInterval</key>
<integer>{{ devpi_metrics_interval }}</integer>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>{{ devpi_metrics_log_dir }}/mcquack.devpi-metrics.err.log</string>
<key>StandardOutPath</key>
<string>{{ devpi_metrics_log_dir }}/mcquack.devpi-metrics.out.log</string>
</dict>
</plist>

View file

@ -1,54 +0,0 @@
#!/bin/bash
# {{ ansible_managed }}
# Collects devpi-server metrics for node_exporter textfile collector
set -euo pipefail
STATUS_URL="{{ devpi_metrics_url }}"
OUTPUT_FILE="{{ devpi_metrics_dir }}/devpi.prom"
TEMP_FILE="${OUTPUT_FILE}.tmp"
# Fetch status JSON
status_json=$(curl -s -H "Accept: application/json" "$STATUS_URL" 2>/dev/null)
if [ -z "$status_json" ] || ! echo "$status_json" | jq -e '.result' >/dev/null 2>&1; then
echo "Failed to fetch devpi status" >&2
exit 1
fi
# Start output file
cat > "$TEMP_FILE" << 'HEADER'
# HELP devpi_up devpi-server is up and responding
# TYPE devpi_up gauge
devpi_up 1
HEADER
# Extract serial number using jq
serial=$(echo "$status_json" | jq -r '.result.serial // empty')
if [ -n "$serial" ]; then
cat >> "$TEMP_FILE" << EOF
# HELP devpi_serial Current changelog serial number
# TYPE devpi_serial gauge
devpi_serial $serial
EOF
fi
# Parse metrics array using jq - format is ["name", "type", value]
echo "$status_json" | jq -r '.result.metrics[]? | @json' | while read -r metric_json; do
name=$(echo "$metric_json" | jq -r '.[0]')
type=$(echo "$metric_json" | jq -r '.[1]')
value=$(echo "$metric_json" | jq -r '.[2]')
# Write metric in Prometheus format
cat >> "$TEMP_FILE" << EOF
# HELP $name devpi metric
# TYPE $name $type
$name $value
EOF
done
# Atomic move
mv "$TEMP_FILE" "$OUTPUT_FILE"

View file

@ -1,131 +0,0 @@
---
kiwix_serve_bin: /Users/erichblume/code/3rd/kiwix-tools/kiwix-serve
kiwix_zim_dir: /Users/erichblume/code/3rd/kiwix-tools
kiwix_bin_dir: /Users/erichblume/.local/bin
kiwix_port: 5501
kiwix_log_dir: /Users/erichblume/Library/Logs
# Transmission integration
# When enabled, ZIM archives are downloaded via BitTorrent instead of direct HTTP
kiwix_use_transmission: true
kiwix_torrent_base_url: "https://download.kiwix.org/zim"
# ZIM archives to download and serve
# Each item needs: category, filename
# Torrent URL: {{ kiwix_torrent_base_url }}/{{ category }}/{{ filename }}.torrent
kiwix_zim_archives:
# Wikipedia - Top 1M articles with images (43G)
- category: wikipedia
filename: wikipedia_en_top1m_maxi_2025-09.zim
## Other Wikipedia options:
# - category: wikipedia
# filename: wikipedia_en_all_maxi_2025-08.zim # 111G - Full English Wikipedia
# - category: wikipedia
# filename: wikipedia_en_top_maxi_2025-12.zim # 7.6G - Top 100K articles
# Project Gutenberg - Public domain books (72G)
- category: gutenberg
filename: gutenberg_en_all_2023-08.zim
## Newer Gutenberg (much larger, unclear why):
# - category: gutenberg
# filename: gutenberg_en_all_2025-11.zim # 206G - Full collection (2025)
# iFixit - Repair guides (3.3G)
- category: ifixit
filename: ifixit_en_all_2025-12.zim
# Stack Exchange
- category: stack_exchange
filename: superuser.com_en_all_2025-12.zim # 3.7G
# - category: stack_exchange
# filename: serverfault.com_en_all_2025-12.zim # 1.5G
# - category: stack_exchange
# filename: askubuntu.com_en_all_2025-12.zim # 2.6G
# - category: stack_exchange
# filename: unix.stackexchange.com_en_all_2025-12.zim # 1.2G
- category: stack_exchange
filename: math.stackexchange.com_en_all_2025-12.zim # 6.9G
# - category: stack_exchange
# filename: stackoverflow.com_en_all_2023-11.zim # 75G - Full StackOverflow
# LibreTexts - Open educational resources
- category: libretexts
filename: libretexts.org_en_bio_2025-01.zim # 2.1G
- category: libretexts
filename: libretexts.org_en_chem_2025-01.zim # 2.0G
- category: libretexts
filename: libretexts.org_en_eng_2025-01.zim # 647M
- category: libretexts
filename: libretexts.org_en_math_2025-01.zim # 744M
- category: libretexts
filename: libretexts.org_en_phys_2025-01.zim # 464M
- category: libretexts
filename: libretexts.org_en_human_2025-01.zim # 3.5G
# DevDocs - Programming documentation
- category: devdocs
filename: devdocs_en_bash_2026-01.zim
- category: devdocs
filename: devdocs_en_c_2026-01.zim
- category: devdocs
filename: devdocs_en_click_2026-01.zim
- category: devdocs
filename: devdocs_en_cmake_2026-01.zim
- category: devdocs
filename: devdocs_en_cpp_2026-01.zim
- category: devdocs
filename: devdocs_en_css_2026-01.zim
- category: devdocs
filename: devdocs_en_django-rest-framework_2026-01.zim
- category: devdocs
filename: devdocs_en_django_2026-01.zim
- category: devdocs
filename: devdocs_en_docker_2026-01.zim
- category: devdocs
filename: devdocs_en_duckdb_2026-01.zim
- category: devdocs
filename: devdocs_en_fish_2026-01.zim
- category: devdocs
filename: devdocs_en_gcc_2026-01.zim
- category: devdocs
filename: devdocs_en_git_2026-01.zim
- category: devdocs
filename: devdocs_en_go_2026-01.zim
- category: devdocs
filename: devdocs_en_godot_2026-01.zim
- category: devdocs
filename: devdocs_en_hammerspoon_2026-01.zim
- category: devdocs
filename: devdocs_en_homebrew_2026-01.zim
- category: devdocs
filename: devdocs_en_javascript_2026-01.zim
- category: devdocs
filename: devdocs_en_kubectl_2026-01.zim
- category: devdocs
filename: devdocs_en_kubernetes_2026-01.zim
- category: devdocs
filename: devdocs_en_latex_2026-01.zim
- category: devdocs
filename: devdocs_en_lua_2026-01.zim
- category: devdocs
filename: devdocs_en_markdown_2026-01.zim
- category: devdocs
filename: devdocs_en_nginx_2026-01.zim
- category: devdocs
filename: devdocs_en_nix_2026-01.zim
- category: devdocs
filename: devdocs_en_postgresql_2026-01.zim
- category: devdocs
filename: devdocs_en_python_2026-01.zim
- category: devdocs
filename: devdocs_en_redis_2026-01.zim
- category: devdocs
filename: devdocs_en_sqlite_2026-01.zim
- category: devdocs
filename: devdocs_en_typescript_2026-01.zim
- category: devdocs
filename: devdocs_en_werkzeug_2026-01.zim
- category: devdocs
filename: devdocs_en_zig_2026-01.zim

View file

@ -1,6 +0,0 @@
---
- name: Restart kiwix-serve
ansible.builtin.shell: |
launchctl unload ~/Library/LaunchAgents/mcquack.eblume.kiwix-serve.plist 2>/dev/null || true
launchctl load ~/Library/LaunchAgents/mcquack.eblume.kiwix-serve.plist
changed_when: true

View file

@ -1,4 +0,0 @@
---
# Role ordering is controlled by indri.yml playbook - do not add dependencies here
# (Ansible's tag accumulation prevents proper deduplication when using meta dependencies)
dependencies: []

View file

@ -1,119 +0,0 @@
---
- name: Ensure kiwix ZIM directory exists
ansible.builtin.file:
path: "{{ kiwix_zim_dir }}"
state: directory
mode: '0755'
- name: Ensure kiwix bin directory exists
ansible.builtin.file:
path: "{{ kiwix_bin_dir }}"
state: directory
mode: '0755'
# --- Deploy management scripts ---
- name: Deploy kiwix torrent sync script
ansible.builtin.template:
src: kiwix-sync-torrents.sh.j2
dest: "{{ kiwix_bin_dir }}/kiwix-sync-torrents.sh"
mode: '0755'
when: kiwix_use_transmission
- name: Deploy kiwix symlink script
ansible.builtin.template:
src: kiwix-symlink-zims.sh.j2
dest: "{{ kiwix_bin_dir }}/kiwix-symlink-zims.sh"
mode: '0755'
when: kiwix_use_transmission
- name: Deploy kiwix torrent list
ansible.builtin.template:
src: kiwix-torrents.txt.j2
dest: "{{ kiwix_bin_dir }}/kiwix-torrents.txt"
mode: '0644'
when: kiwix_use_transmission
# --- Transmission-based torrent management ---
- name: Check transmission daemon is responding
ansible.builtin.command: transmission-remote -l
register: kiwix_transmission_check
changed_when: false
failed_when: false
when: kiwix_use_transmission
- name: Fail if transmission is not running
ansible.builtin.fail:
msg: "Transmission daemon is not responding. Ensure transmission role ran successfully."
when: kiwix_use_transmission and kiwix_transmission_check.rc != 0
- name: Sync ZIM torrents to transmission
ansible.builtin.command: "{{ kiwix_bin_dir }}/kiwix-sync-torrents.sh {{ kiwix_bin_dir }}/kiwix-torrents.txt"
register: kiwix_torrent_sync
changed_when: "'Added:' in kiwix_torrent_sync.stdout"
when: kiwix_use_transmission
# --- Symlink completed ZIM files ---
- name: Symlink completed ZIM files to kiwix directory
ansible.builtin.command: "{{ kiwix_bin_dir }}/kiwix-symlink-zims.sh {{ transmission_download_dir }} {{ kiwix_zim_dir }}"
register: kiwix_symlink_result
changed_when: "'Linked:' in kiwix_symlink_result.stdout"
when: kiwix_use_transmission
notify: Restart kiwix-serve
# --- Fallback: Direct HTTP download (original behavior) ---
- name: Check which ZIM archives exist (direct download mode)
ansible.builtin.stat:
path: "{{ kiwix_zim_dir }}/{{ item.filename }}"
get_checksum: false
loop: "{{ kiwix_zim_archives }}"
loop_control:
label: "{{ item.filename }}"
register: kiwix_zim_stat
when: not kiwix_use_transmission
- name: Download missing ZIM archives (direct download mode)
ansible.builtin.get_url:
url: "https://download.kiwix.org/zim/{{ item.item.category }}/{{ item.item.filename }}"
dest: "{{ kiwix_zim_dir }}/{{ item.item.filename }}"
mode: '0644'
timeout: 3600
loop: "{{ kiwix_zim_stat.results | default([]) }}"
loop_control:
label: "{{ item.item.filename | default('unknown') }}"
when:
- not kiwix_use_transmission
- item.stat is defined
- not item.stat.exists
notify: Restart kiwix-serve
# --- Determine which archives are available ---
- name: Find available ZIM archives in kiwix directory
ansible.builtin.find:
paths: "{{ kiwix_zim_dir }}"
patterns: "*.zim"
file_type: any # includes symlinks
register: kiwix_available_zim_files
- name: Build list of available archive filenames
ansible.builtin.set_fact:
kiwix_available_archives: "{{ kiwix_available_zim_files.files | map(attribute='path') | map('basename') | list }}"
# --- LaunchAgent deployment ---
- name: Deploy kiwix-serve LaunchAgent plist
ansible.builtin.template:
src: kiwix-serve.plist.j2
dest: ~/Library/LaunchAgents/mcquack.eblume.kiwix-serve.plist
mode: '0644'
notify: Restart kiwix-serve
- name: Check if kiwix-serve LaunchAgent is loaded
ansible.builtin.command: launchctl list mcquack.eblume.kiwix-serve
register: kiwix_launchctl_check
changed_when: false
failed_when: false
- name: Load kiwix-serve LaunchAgent if not loaded
ansible.builtin.command: launchctl load ~/Library/LaunchAgents/mcquack.eblume.kiwix-serve.plist
when: kiwix_launchctl_check.rc != 0
changed_when: true
failed_when: false

View file

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- {{ ansible_managed }} -->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>KeepAlive</key>
<true/>
<key>Label</key>
<string>mcquack.eblume.kiwix-serve</string>
<key>ProgramArguments</key>
<array>
<string>{{ kiwix_serve_bin }}</string>
<string>--port={{ kiwix_port }}</string>
{% for filename in kiwix_available_archives %}
<string>{{ kiwix_zim_dir }}/{{ filename }}</string>
{% endfor %}
</array>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>{{ kiwix_log_dir }}/mcquack.kiwix-serve.err.log</string>
<key>StandardOutPath</key>
<string>{{ kiwix_log_dir }}/mcquack.kiwix-serve.out.log</string>
</dict>
</plist>

View file

@ -1,50 +0,0 @@
#!/bin/bash
# Symlink completed ZIM files from download directory to kiwix directory
set -euo pipefail
SOURCE_DIR="${1:-}"
TARGET_DIR="${2:-}"
if [[ -z "$SOURCE_DIR" || -z "$TARGET_DIR" ]]; then
echo "Usage: $0 <source-dir> <target-dir>" >&2
exit 1
fi
if [[ ! -d "$SOURCE_DIR" ]]; then
echo "Error: Source directory not found: $SOURCE_DIR" >&2
exit 1
fi
if [[ ! -d "$TARGET_DIR" ]]; then
echo "Error: Target directory not found: $TARGET_DIR" >&2
exit 1
fi
created=0
skipped=0
# Find all .zim files in source directory
for zim_file in "$SOURCE_DIR"/*.zim; do
# Handle case where no .zim files exist
[[ -e "$zim_file" ]] || continue
filename=$(basename "$zim_file")
target_path="$TARGET_DIR/$filename"
if [[ -e "$target_path" || -L "$target_path" ]]; then
((skipped++)) || true
else
ln -s "$zim_file" "$target_path"
echo "Linked: $filename"
((created++)) || true
fi
done
echo "Symlink complete: $created created, $skipped already present"
# Exit with special code if new symlinks were created (for ansible changed detection)
if [[ $created -gt 0 ]]; then
exit 0
else
exit 0
fi

View file

@ -1,47 +0,0 @@
#!/bin/bash
# Sync ZIM archive torrents to transmission
# Reads torrent URLs from stdin or file, adds any missing to transmission
set -euo pipefail
TORRENT_LIST="${1:-}"
if [[ -z "$TORRENT_LIST" ]]; then
echo "Usage: $0 <torrent-list-file>" >&2
exit 1
fi
if [[ ! -f "$TORRENT_LIST" ]]; then
echo "Error: Torrent list file not found: $TORRENT_LIST" >&2
exit 1
fi
# Get current torrents from transmission (extract names, skip header/footer)
# Note: Use sed '$d' instead of head -n -1 for macOS compatibility
current_torrents=$(transmission-remote -l 2>/dev/null | tail -n +2 | sed '$d' | awk '{print $NF}' || true)
added=0
skipped=0
while IFS= read -r torrent_url || [[ -n "$torrent_url" ]]; do
# Skip empty lines and comments
[[ -z "$torrent_url" || "$torrent_url" =~ ^# ]] && continue
# Extract base name from URL (remove .torrent extension and path)
base_name=$(basename "$torrent_url" .torrent)
# Also try without .zim in case transmission reports it differently
base_without_zim="${base_name%.zim}"
# Check if already in transmission
if echo "$current_torrents" | grep -qF "$base_without_zim"; then
((skipped++)) || true
else
if transmission-remote -a "$torrent_url" 2>/dev/null; then
echo "Added: $base_name"
((added++)) || true
else
echo "Warning: Failed to add $torrent_url" >&2
fi
fi
done < "$TORRENT_LIST"
echo "Sync complete: $added added, $skipped already present"

View file

@ -1,5 +0,0 @@
# ZIM archive torrent URLs for kiwix
# Generated by ansible - do not edit manually
{% for archive in kiwix_zim_archives %}
{{ kiwix_torrent_base_url }}/{{ archive.category }}/{{ archive.filename }}.torrent
{% endfor %}

View file

@ -1,2 +0,0 @@
---
node_exporter_textfile_dir: /opt/homebrew/var/node_exporter/textfile

View file

@ -1,5 +0,0 @@
---
- name: Restart node_exporter
ansible.builtin.command: brew services restart node_exporter
listen: Restart node_exporter
changed_when: true

View file

@ -1,22 +0,0 @@
---
# Note: node_exporter is installed via homebrew manually.
# This role manages the args file to enable textfile collector.
- name: Create textfile collector directory
ansible.builtin.file:
path: "{{ node_exporter_textfile_dir }}"
state: directory
mode: '0755'
- name: Configure node_exporter args
ansible.builtin.template:
src: node_exporter.args.j2
dest: /opt/homebrew/etc/node_exporter.args
mode: '0644'
notify: Restart node_exporter
- name: Ensure node_exporter service is started
ansible.builtin.command: brew services start node_exporter
register: node_exporter_brew_start
changed_when: "'Successfully started' in node_exporter_brew_start.stdout"
failed_when: false

View file

@ -1 +0,0 @@
--collector.textfile.directory={{ node_exporter_textfile_dir }}

View file

@ -1,3 +0,0 @@
---
# No handlers currently - podman machine start is unreliable via Ansible
# See known issue in tasks/main.yml

View file

@ -1,58 +0,0 @@
---
# Podman installation and machine setup for indri
# Used as container runtime for minikube
#
# KNOWN ISSUE: podman machine init/start has reliability issues when run via
# Ansible/SSH. The machine sometimes gets stuck in "Starting" state due to a
# race condition (see https://github.com/containers/podman/issues/16945).
# Additionally, Apple Hypervisor may require GUI session context.
#
# WORKAROUND: If the machine fails to start via Ansible, manually run on indri:
# podman machine rm -f podman-machine-default
# podman machine init --cpus 4 --memory 8192 --disk-size 220
# podman machine start
#
# TODO: Investigate proper LaunchAgent or other solution for reliable automation.
- name: Install podman via homebrew
community.general.homebrew:
name: podman
state: present
- name: Check if podman machine exists
ansible.builtin.command:
cmd: podman machine list --format json
register: podman_machine_list
changed_when: false
check_mode: false # Safe to run in check mode - read-only
- name: Initialize podman machine (if not exists)
ansible.builtin.command:
cmd: podman machine init --cpus 4 --memory 8192 --disk-size 220
register: podman_init
changed_when: podman_init.rc == 0
failed_when: podman_init.rc not in [0, 125] # 125 = already exists
when: podman_machine_list.stdout == '[]'
- name: Check if podman machine is running
ansible.builtin.command:
cmd: podman machine list --format "{{ '{{' }}.Running{{ '}}' }}"
register: podman_running
changed_when: false
check_mode: false # Safe to run in check mode - read-only
- name: Start podman machine (if stopped)
ansible.builtin.command:
cmd: podman machine start
register: podman_start
changed_when: "'started successfully' in podman_start.stdout"
failed_when: false # Don't fail - see known issue above
when: "'true' not in podman_running.stdout"
- name: Warn if podman machine failed to start
ansible.builtin.debug:
msg: "WARNING: podman machine may not have started. Run 'podman machine start' manually on indri if needed."
when:
- "'true' not in podman_running.stdout"
- podman_start is defined
- podman_start.rc != 0 or "'started successfully' not in podman_start.stdout"

View file

@ -1,24 +0,0 @@
---
# Homebrew's transmission-cli service uses this config directory
transmission_config_dir: /opt/homebrew/var/transmission
# Download directories
transmission_download_dir: /Users/erichblume/transmission
transmission_incomplete_dir: /Users/erichblume/transmission/.incomplete
# RPC settings (local only - no authentication needed)
transmission_rpc_enabled: true
transmission_rpc_port: 9091
transmission_rpc_bind_address: "127.0.0.1"
transmission_rpc_authentication_required: false
transmission_rpc_whitelist_enabled: true
transmission_rpc_whitelist: "127.0.0.1"
# Speed limits (KB/s, 0 = unlimited)
transmission_speed_limit_down: 0
transmission_speed_limit_up: 100
# P2P settings
transmission_dht_enabled: true
transmission_pex_enabled: true
transmission_encryption: 1 # 0=prefer unencrypted, 1=prefer encrypted, 2=require encrypted

View file

@ -1,4 +0,0 @@
---
- name: Restart transmission
ansible.builtin.command: brew services restart transmission-cli
changed_when: true

View file

@ -1,52 +0,0 @@
---
- name: Install transmission-cli via homebrew
community.general.homebrew:
name: transmission-cli
state: present
- name: Ensure transmission download directory exists
ansible.builtin.file:
path: "{{ transmission_download_dir }}"
state: directory
mode: '0755'
- name: Ensure transmission incomplete directory exists
ansible.builtin.file:
path: "{{ transmission_incomplete_dir }}"
state: directory
mode: '0755'
- name: Remove old config directory (was deployed to wrong location)
ansible.builtin.file:
path: ~/.config/transmission-daemon
state: absent
# Note: transmission must be stopped before modifying settings.json
# otherwise it may overwrite our changes on shutdown
- name: Check if settings.json needs updating
ansible.builtin.template:
src: settings.json.j2
dest: "{{ transmission_config_dir }}/settings.json"
mode: '0600'
check_mode: true
register: transmission_settings_check
- name: Stop transmission before config changes
ansible.builtin.command: brew services stop transmission-cli
when: transmission_settings_check.changed
register: transmission_brew_stop
changed_when: false
failed_when: false
- name: Deploy transmission settings.json
ansible.builtin.template:
src: settings.json.j2
dest: "{{ transmission_config_dir }}/settings.json"
mode: '0600'
notify: Restart transmission
- name: Ensure transmission service is started
ansible.builtin.command: brew services start transmission-cli
register: transmission_brew_start
changed_when: "'Successfully started' in transmission_brew_start.stdout"
failed_when: false

View file

@ -1,89 +0,0 @@
{#
RPC is required for transmission-remote CLI to manage torrents.
Config is secure: bound to localhost only, no auth needed.
rpc-password uses a static hash starting with '{' so transmission
recognizes it as pre-hashed and won't regenerate it on restart.
Without this, transmission writes a new hash each startup causing
perpetual ansible diffs.
#}
{
"_comment": "{{ ansible_managed }}",
"alt-speed-down": 50,
"alt-speed-enabled": false,
"alt-speed-time-begin": 540,
"alt-speed-time-day": 127,
"alt-speed-time-enabled": false,
"alt-speed-time-end": 1020,
"alt-speed-up": 50,
"announce-ip": "",
"announce-ip-enabled": false,
"anti-brute-force-enabled": false,
"anti-brute-force-threshold": 100,
"bind-address-ipv4": "0.0.0.0",
"bind-address-ipv6": "::",
"blocklist-enabled": false,
"blocklist-url": "http://www.example.com/blocklist",
"cache-size-mb": 4,
"default-trackers": "",
"dht-enabled": {{ transmission_dht_enabled | lower }},
"download-dir": "{{ transmission_download_dir }}",
"download-queue-enabled": true,
"download-queue-size": 5,
"encryption": {{ transmission_encryption }},
"idle-seeding-limit": 30,
"idle-seeding-limit-enabled": false,
"incomplete-dir": "{{ transmission_incomplete_dir }}",
"incomplete-dir-enabled": true,
"lpd-enabled": true,
"message-level": 4,
"peer-congestion-algorithm": "",
"peer-limit-global": 200,
"peer-limit-per-torrent": 50,
"peer-port": 51413,
"peer-port-random-high": 65535,
"peer-port-random-low": 49152,
"peer-port-random-on-start": false,
"peer-socket-tos": "le",
"pex-enabled": {{ transmission_pex_enabled | lower }},
"port-forwarding-enabled": true,
"preallocation": 1,
"prefetch-enabled": true,
"queue-stalled-enabled": true,
"queue-stalled-minutes": 30,
"ratio-limit": 2,
"ratio-limit-enabled": false,
"rename-partial-files": false,
"rpc-authentication-required": {{ transmission_rpc_authentication_required | lower }},
"rpc-bind-address": "{{ transmission_rpc_bind_address }}",
"rpc-enabled": {{ transmission_rpc_enabled | lower }},
"rpc-host-whitelist": "",
"rpc-host-whitelist-enabled": true,
"rpc-password": "{00000000000000000000000000000000000000000000000e",
"rpc-port": {{ transmission_rpc_port }},
"rpc-socket-mode": "0750",
"rpc-url": "/transmission/",
"rpc-username": "",
"rpc-whitelist": "{{ transmission_rpc_whitelist }}",
"rpc-whitelist-enabled": {{ transmission_rpc_whitelist_enabled | lower }},
"scrape-paused-torrents-enabled": true,
"script-torrent-added-enabled": false,
"script-torrent-added-filename": "",
"script-torrent-done-enabled": false,
"script-torrent-done-filename": "",
"script-torrent-done-seeding-enabled": false,
"script-torrent-done-seeding-filename": "",
"seed-queue-enabled": false,
"seed-queue-size": 10,
"speed-limit-down": {{ transmission_speed_limit_down }},
"speed-limit-down-enabled": {{ (transmission_speed_limit_down > 0) | lower }},
"speed-limit-up": {{ transmission_speed_limit_up }},
"speed-limit-up-enabled": {{ (transmission_speed_limit_up > 0) | lower }},
"start-added-torrents": true,
"tcp-enabled": true,
"torrent-added-verify-mode": "fast",
"trash-original-torrent-files": false,
"umask": "022",
"upload-slots-per-torrent": 8,
"utp-enabled": true
}

View file

@ -1,7 +0,0 @@
---
transmission_metrics_rpc_host: 127.0.0.1
transmission_metrics_rpc_port: 9091
transmission_metrics_dir: /opt/homebrew/var/node_exporter/textfile
transmission_metrics_script: /Users/erichblume/bin/transmission-metrics
transmission_metrics_interval: 60 # seconds between metric collection
transmission_metrics_log_dir: /opt/homebrew/var/log

View file

@ -1,6 +0,0 @@
---
- name: Reload transmission-metrics
ansible.builtin.shell: |
launchctl unload ~/Library/LaunchAgents/mcquack.eblume.transmission-metrics.plist 2>/dev/null || true
launchctl load ~/Library/LaunchAgents/mcquack.eblume.transmission-metrics.plist
changed_when: true

View file

@ -1,4 +0,0 @@
---
# Role ordering is controlled by indri.yml playbook - do not add dependencies here
# (Ansible's tag accumulation prevents proper deduplication when using meta dependencies)
dependencies: []

View file

@ -1,26 +0,0 @@
---
- name: Deploy transmission metrics collection script
ansible.builtin.template:
src: transmission-metrics.sh.j2
dest: "{{ transmission_metrics_script }}"
mode: '0755'
notify: Reload transmission-metrics
- name: Deploy transmission-metrics LaunchAgent plist
ansible.builtin.template:
src: transmission-metrics.plist.j2
dest: ~/Library/LaunchAgents/mcquack.eblume.transmission-metrics.plist
mode: '0644'
notify: Reload transmission-metrics
- name: Check if transmission-metrics LaunchAgent is loaded
ansible.builtin.command: launchctl list mcquack.eblume.transmission-metrics
register: transmission_metrics_launchctl_check
changed_when: false
failed_when: false
- name: Load transmission-metrics LaunchAgent if not loaded
ansible.builtin.command: launchctl load ~/Library/LaunchAgents/mcquack.eblume.transmission-metrics.plist
when: transmission_metrics_launchctl_check.rc != 0
changed_when: true
failed_when: false

View file

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- {{ ansible_managed }} -->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>mcquack.eblume.transmission-metrics</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/bin:/bin</string>
</dict>
<key>ProgramArguments</key>
<array>
<string>{{ transmission_metrics_script }}</string>
</array>
<key>StartInterval</key>
<integer>{{ transmission_metrics_interval }}</integer>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>{{ transmission_metrics_log_dir }}/transmission-metrics.err.log</string>
<key>StandardOutPath</key>
<string>{{ transmission_metrics_log_dir }}/transmission-metrics.out.log</string>
</dict>
</plist>

View file

@ -1,120 +0,0 @@
#!/bin/bash
# {{ ansible_managed }}
# Collects transmission-daemon metrics for node_exporter textfile collector
set -euo pipefail
RPC_URL="http://{{ transmission_metrics_rpc_host }}:{{ transmission_metrics_rpc_port }}/transmission/rpc"
OUTPUT_FILE="{{ transmission_metrics_dir }}/transmission.prom"
TEMP_FILE="${OUTPUT_FILE}.tmp"
# Get session ID (required for transmission RPC)
# Note: transmission doesn't support HEAD requests, so we make a request and parse
# the 409 response headers. We use sed to stop at the blank line (which has \r) before body.
get_session_id() {
curl -s -i "$RPC_URL" 2>/dev/null | sed '/^\r$/q' | grep -i '^X-Transmission-Session-Id:' | awk '{print $2}' | tr -d '\r'
}
# Make RPC request
rpc_request() {
local method="$1"
local args="${2:-}"
local session_id
session_id=$(get_session_id)
if [ -z "$session_id" ]; then
echo "Failed to get session ID" >&2
return 1
fi
local payload
if [ -n "$args" ]; then
payload="{\"method\": \"$method\", \"arguments\": $args}"
else
payload="{\"method\": \"$method\"}"
fi
curl -s "$RPC_URL" \
-H "X-Transmission-Session-Id: $session_id" \
-H "Content-Type: application/json" \
-d "$payload"
}
# Get session stats
session_stats=$(rpc_request "session-stats")
if [ -z "$session_stats" ] || ! echo "$session_stats" | grep -q '"result":"success"'; then
echo "Failed to get session stats" >&2
exit 1
fi
# Extract values using grep/sed (avoiding jq dependency)
extract_json_int() {
echo "$1" | grep -o "\"$2\":[0-9]*" | head -1 | sed "s/\"$2\"://"
}
# Session stats
download_speed=$(extract_json_int "$session_stats" "downloadSpeed")
upload_speed=$(extract_json_int "$session_stats" "uploadSpeed")
torrents_active=$(extract_json_int "$session_stats" "activeTorrentCount")
torrents_paused=$(extract_json_int "$session_stats" "pausedTorrentCount")
torrents_total=$(extract_json_int "$session_stats" "torrentCount")
# Cumulative stats
downloaded_bytes=$(echo "$session_stats" | grep -o '"cumulative-stats":{[^}]*}' | grep -o '"downloadedBytes":[0-9]*' | sed 's/"downloadedBytes"://')
uploaded_bytes=$(echo "$session_stats" | grep -o '"cumulative-stats":{[^}]*}' | grep -o '"uploadedBytes":[0-9]*' | sed 's/"uploadedBytes"://')
seconds_active=$(echo "$session_stats" | grep -o '"cumulative-stats":{[^}]*}' | grep -o '"secondsActive":[0-9]*' | sed 's/"secondsActive"://')
# Get total size of all torrents
torrent_info=$(rpc_request "torrent-get" '{"fields": ["totalSize"]}')
total_size_bytes=0
if echo "$torrent_info" | grep -q '"result":"success"'; then
# Sum all totalSize values
total_size_bytes=$(echo "$torrent_info" | grep -o '"totalSize":[0-9]*' | sed 's/"totalSize"://' | awk '{sum += $1} END {print sum}')
fi
# Write metrics
cat > "$TEMP_FILE" << EOF
# HELP transmission_download_speed_bytes Current download speed in bytes per second
# TYPE transmission_download_speed_bytes gauge
transmission_download_speed_bytes ${download_speed:-0}
# HELP transmission_upload_speed_bytes Current upload speed in bytes per second
# TYPE transmission_upload_speed_bytes gauge
transmission_upload_speed_bytes ${upload_speed:-0}
# HELP transmission_torrents_active Number of active torrents
# TYPE transmission_torrents_active gauge
transmission_torrents_active ${torrents_active:-0}
# HELP transmission_torrents_paused Number of paused torrents
# TYPE transmission_torrents_paused gauge
transmission_torrents_paused ${torrents_paused:-0}
# HELP transmission_torrents_total Total number of torrents
# TYPE transmission_torrents_total gauge
transmission_torrents_total ${torrents_total:-0}
# HELP transmission_torrents_size_bytes Total size of all torrents in bytes
# TYPE transmission_torrents_size_bytes gauge
transmission_torrents_size_bytes ${total_size_bytes:-0}
# HELP transmission_downloaded_bytes_total Total bytes downloaded (cumulative)
# TYPE transmission_downloaded_bytes_total counter
transmission_downloaded_bytes_total ${downloaded_bytes:-0}
# HELP transmission_uploaded_bytes_total Total bytes uploaded (cumulative)
# TYPE transmission_uploaded_bytes_total counter
transmission_uploaded_bytes_total ${uploaded_bytes:-0}
# HELP transmission_seconds_active_total Total seconds transmission has been active
# TYPE transmission_seconds_active_total counter
transmission_seconds_active_total ${seconds_active:-0}
# HELP transmission_up Transmission daemon is up and responding
# TYPE transmission_up gauge
transmission_up 1
EOF
# Atomic move
mv "$TEMP_FILE" "$OUTPUT_FILE"

View file

@ -16,3 +16,9 @@ spec:
syncPolicy: syncPolicy:
syncOptions: syncOptions:
- CreateNamespace=true - CreateNamespace=true
# Ignore zim-hash annotation - updated dynamically by zim-watcher CronJob
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /metadata/annotations/kiwix.blumeops~1zim-hash

View file

@ -36,7 +36,8 @@ acl = tailscale.Acl(
# Tags control access via the ACL policy in policy.hujson. # Tags control access via the ACL policy in policy.hujson.
# indri - Mac Mini M1, primary homelab server # indri - Mac Mini M1, primary homelab server
# Hosts all user-facing services (grafana, forge, kiwix, etc.) # Hosts forge, loki, zot registry, and the k8s control plane.
# Other services (grafana, kiwix, devpi, etc.) run in k8s with their own Tailscale devices.
indri = tailscale.get_device(name="indri.tail8d86e.ts.net") indri = tailscale.get_device(name="indri.tail8d86e.ts.net")
indri_tags = tailscale.DeviceTags( indri_tags = tailscale.DeviceTags(
"indri-tags", "indri-tags",
@ -44,16 +45,11 @@ indri_tags = tailscale.DeviceTags(
tags=[ tags=[
"tag:homelab", # Server role - allows SSH from workstations "tag:homelab", # Server role - allows SSH from workstations
"tag:blumeops", # Managed by this IaC "tag:blumeops", # Managed by this IaC
# Service tags - enable fine-grained access control per service # Service tags for services still hosted directly on indri
"tag:grafana",
"tag:forge", "tag:forge",
"tag:kiwix",
"tag:devpi",
"tag:loki", "tag:loki",
"tag:pg",
"tag:feed",
"tag:registry", # Zot container registry "tag:registry", # Zot container registry
"tag:k8s-api", # Kubernetes API server "tag:k8s-api", # Kubernetes API server (minikube)
], ],
) )