Files
ansible_fencl/update_valheim.yml
T

294 lines
12 KiB
YAML

---
# update_valheim.yml
# Requires Docker Compose >= 2.18 and Python 3 on pve2_vm.
# Uses ansible.builtin only, like the command-based Minecraft update workflow.
# A run ALWAYS restarts Valheim, even if the image did not change.
# Do not run concurrently with another update or a Portainer redeployment.
- name: Deploy or update Valheim
hosts: pve2_vm
gather_facts: false
become: true
vars:
compose_local_dir: "{{ playbook_dir }}/docker-compose"
compose_remote_base: "/home/{{ ansible_user }}/.ansible-compose"
# NOT the shared /docker-compose directory removed by other playbooks.
compose_remote_dir: "{{ compose_remote_base }}/valheim"
valheim_compose_filename: "docker-compose-valheim.yml"
valheim_project: valheim
valheim_service: valheim
valheim_wait_timeout: 1800
valheim_preupdate_dir: "/backups/valheim/pre-update"
valheim_preupdate_keep_days: 14
# Read paths/UID/GID from the controller's compose file to avoid duplication.
valheim_compose: >-
{{ lookup('ansible.builtin.file', compose_local_dir ~ '/' ~ valheim_compose_filename) | from_yaml }}
valheim_spec: "{{ valheim_compose.services[valheim_service] }}"
valheim_container: "{{ valheim_spec.container_name }}"
valheim_uid: "{{ valheim_spec.environment.PUID }}"
valheim_gid: "{{ valheim_spec.environment.PGID }}"
valheim_password_file: >-
{{ (valheim_spec.volumes | selectattr('target', 'equalto', '/run/secrets/valheim_server_pass') | first).source }}
valheim_config_dir: >-
{{ (valheim_spec.volumes | selectattr('target', 'equalto', '/config') | first).source }}
valheim_compose_argv:
- docker
- compose
- --project-name
- "{{ valheim_project }}"
- --project-directory
- "{{ compose_remote_dir }}"
- --file
- "{{ compose_remote_dir }}/{{ valheim_compose_filename }}"
pre_tasks:
- name: Reject check mode before making changes
ansible.builtin.assert:
that:
- not ansible_check_mode
fail_msg: "This maintenance playbook does not support --check. Use --syntax-check instead."
- name: Check Docker Compose version
ansible.builtin.command:
argv: [docker, compose, version, --short]
register: valheim_compose_version
changed_when: false
- name: Require Compose with wait support
ansible.builtin.assert:
that:
- >-
(valheim_compose_version.stdout | regex_search('[0-9]+\.[0-9]+\.[0-9]+'))
is version('2.18.0', '>=')
fail_msg: "Install/update the Docker Compose plugin on pve2_vm (minimum 2.18.0)."
- name: Inspect the persistent password file without reading its contents
ansible.builtin.stat:
path: "{{ valheim_password_file }}"
get_checksum: false
get_mime: false
register: valheim_password_stat
- name: Require a real password file on the VM
ansible.builtin.assert:
that:
- valheim_password_stat.stat.isreg | default(false)
- not (valheim_password_stat.stat.islnk | default(false))
fail_msg: >-
Create {{ valheim_password_file }} on pve2_vm first.
It must contain ONLY the server password on one line, at least 5 characters,
not SERVER_PASS=... . See README-valheim.md. The playbook never generates or overwrites it.
- name: Validate the password without logging it
ansible.builtin.command:
argv:
- python3
- -c
- |
from pathlib import Path
import sys
try:
password = Path(sys.argv[1]).read_text(encoding="utf-8").rstrip("\r\n")
except (OSError, UnicodeError):
raise SystemExit("Cannot read the password file as UTF-8.")
if (len(password) < 5 or "\n" in password or "\r" in password
or "\x00" in password or password.startswith("SERVER_PASS=")):
raise SystemExit("Use a single plain password of at least 5 characters.")
- "{{ valheim_password_file }}"
changed_when: false
no_log: true
- name: Restrict password ownership and permissions to the configured game UID
ansible.builtin.file:
path: "{{ valheim_password_file }}"
owner: "{{ valheim_uid }}"
group: "{{ valheim_gid }}"
mode: "0600"
- name: Inspect an existing Valheim container
ansible.builtin.command:
argv: [docker, container, inspect, "{{ valheim_container }}"]
register: valheim_inspect
changed_when: false
failed_when: >-
valheim_inspect.rc != 0 and 'No such' not in valheim_inspect.stderr
no_log: true
- name: Save only the existing container metadata in Ansible memory
ansible.builtin.set_fact:
valheim_existing: "{{ (valheim_inspect.stdout | from_json | first) if valheim_inspect.rc == 0 else {} }}"
no_log: true
- name: Refuse to take over a container from a different project or service
ansible.builtin.assert:
that:
- >-
(valheim_existing.get('Config', {}).get('Labels') or {}).get('com.docker.compose.project')
== valheim_project
- >-
(valheim_existing.get('Config', {}).get('Labels') or {}).get('com.docker.compose.service')
== valheim_service
fail_msg: "Container name is occupied. The existing container must belong to stack/service valheim."
when: valheim_existing | length > 0
- name: Refuse a silent switch to different persistent data or password paths
ansible.builtin.assert:
that:
- >-
(valheim_existing.Mounts | selectattr('Destination', 'equalto', item.target)
| map(attribute='Source') | list) == [item.source]
fail_msg: >-
Existing mount {{ item.target }} does not match {{ item.source }}.
Migrate the data deliberately before changing the compose bind paths.
loop: "{{ valheim_spec.volumes }}"
loop_control:
label: "{{ item.target }}"
when: valheim_existing | length > 0
tasks:
- name: Ensure the dedicated remote compose directory exists
ansible.builtin.file:
path: "{{ compose_remote_dir }}"
state: directory
owner: "{{ ansible_user }}"
mode: "0755"
- name: Ensure the writable host bind directories exist
ansible.builtin.file:
path: "{{ item.source }}"
state: directory
owner: "{{ valheim_uid }}"
group: "{{ valheim_gid }}"
mode: "0750"
loop: "{{ valheim_spec.volumes }}"
when: not (item.read_only | default(false) | bool)
loop_control:
label: "{{ item.source }}"
- name: Ensure the offline pre-update backup directory exists
ansible.builtin.file:
path: "{{ valheim_preupdate_dir }}"
state: directory
mode: "0755"
- name: Upload only the Valheim compose file
ansible.builtin.copy:
src: "{{ compose_local_dir }}/{{ valheim_compose_filename }}"
dest: "{{ compose_remote_dir }}/{{ valheim_compose_filename }}"
owner: "{{ ansible_user }}"
mode: "0644"
- name: Validate the compose configuration before interrupting the game
ansible.builtin.command:
argv: "{{ valheim_compose_argv + ['config', '--quiet'] }}"
changed_when: false
- name: Pull the image while the old server is still running
ansible.builtin.command:
argv: "{{ valheim_compose_argv + ['pull', valheim_service] }}"
# Pull output is not a stable machine-readable change indicator.
changed_when: false
- name: Stop and make a consistent offline config/world backup
block:
- name: Gracefully stop only the existing Valheim container
ansible.builtin.command:
argv: [docker, stop, --time, "120", "{{ valheim_container }}"]
when: valheim_existing.get('State', {}).get('Running', false)
changed_when: true
- name: Allocate a unique pre-update archive filename
ansible.builtin.tempfile:
state: file
path: "{{ valheim_preupdate_dir }}"
prefix: "config-{{ now(utc=true, fmt='%Y%m%dT%H%M%SZ') }}-"
suffix: ".tar.gz"
register: valheim_backup
- name: Back up config and worlds, excluding the separate automatic backup mount
ansible.builtin.command:
argv:
- tar
- --create
- --gzip
- --file
- "{{ valheim_backup.path }}"
- --exclude=./backups
- --directory
- "{{ valheim_config_dir }}"
- .
changed_when: true
rescue:
- name: Abort rather than update without a completed backup
ansible.builtin.fail:
msg: >-
Stop/backup failed; the replacement container was not deployed.
The original container may now be stopped. It is NOT automatically restarted,
because starting it can itself trigger a Steam game update without a valid backup.
Check docker ps and disk space on pve2_vm; an incomplete archive may remain.
Fix the backup failure and run this playbook again.
- name: Recreate Valheim and verify local readiness and the startup Steam check
block:
- name: Recreate only Valheim and wait for its healthcheck
ansible.builtin.command:
argv: >-
{{ valheim_compose_argv + ['up', '--detach', '--force-recreate', '--no-deps',
'--pull', 'never', '--wait', '--wait-timeout', valheim_wait_timeout | string,
valheim_service] }}
changed_when: true
- name: Read new-container startup logs without exposing them in Semaphore
ansible.builtin.command:
argv: [docker, logs, "{{ valheim_container }}"]
register: valheim_start_logs
changed_when: false
no_log: true
- name: Confirm Steam update completed rather than falling back to old binaries
ansible.builtin.assert:
that:
- >-
'Valheim Server was updated - restarting' in (valheim_start_logs.stdout ~ valheim_start_logs.stderr)
or 'Valheim Server is already the latest version' in (valheim_start_logs.stdout ~ valheim_start_logs.stderr)
fail_msg: >-
The container is healthy, but no successful Steam update marker was found.
Inspect docker logs valheim on the VM: Steam may have failed and the image
may be using its previous game installation, or upstream log wording changed.
rescue:
- name: Report failure without destructive rollback or dumping potentially sensitive logs
ansible.builtin.fail:
msg: >-
Valheim readiness or Steam-update verification failed.
Inspect 'docker ps -a --filter name=valheim' and 'docker logs --tail 150 valheim'
on pve2_vm. The pre-update config/world archive is {{ valheim_backup.path }}.
No data directories were deleted and no automatic downgrade/restore was attempted.
- name: Find expired offline archives only after a successful deployment
ansible.builtin.find:
paths: "{{ valheim_preupdate_dir }}"
patterns: "config-*.tar.gz"
age: "{{ valheim_preupdate_keep_days }}d"
age_stamp: mtime
file_type: file
recurse: false
register: valheim_old_backups
- name: Remove only expired Valheim pre-update archives
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ valheim_old_backups.files }}"
loop_control:
label: "{{ item.path }}"
- name: Report completion
ansible.builtin.debug:
msg:
- "Valheim passed local health and startup Steam-update checks."
- "Offline config/world backup: {{ valheim_backup.path }}"
- "Join using the VM address and UDP port 2456. Router/firewall reachability is not tested."