forked from jakub/ansible
feat: update Valheim deployment playbook to include WebMap installation and configuration
This commit is contained in:
@@ -1,43 +1,32 @@
|
|||||||
# Portainer stack name: valheim
|
# Portainer stack name: valheim
|
||||||
# Target: pve2_vm / vm-portainer-services
|
# Target: pve2_vm / vm-portainer-services
|
||||||
# Create the host directories and password file before the first deployment.
|
# The game service, data paths, secrets, backups and UDP ports are unchanged.
|
||||||
# Configuration lives here; the password is NOT stored in Git or Portainer ENV.
|
|
||||||
name: valheim
|
name: valheim
|
||||||
|
|
||||||
services:
|
services:
|
||||||
valheim:
|
valheim:
|
||||||
image: ghcr.io/community-valheim-tools/valheim-server:latest
|
image: ghcr.io/community-valheim-tools/valheim-server:latest
|
||||||
container_name: valheim
|
container_name: valheim
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
stop_grace_period: 2m
|
stop_grace_period: 2m
|
||||||
|
|
||||||
cap_add:
|
cap_add:
|
||||||
- SYS_NICE
|
- SYS_NICE
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "2456-2457:2456-2457/udp"
|
- "2456-2457:2456-2457/udp"
|
||||||
|
# Host TCP 8095 -> ValheimWebMap internal TCP 3696.
|
||||||
|
# This is an unauthenticated map; restrict ingress via firewall/proxy.
|
||||||
|
- "8095:3696/tcp"
|
||||||
environment:
|
environment:
|
||||||
TZ: "Europe/Prague"
|
TZ: "Europe/Prague"
|
||||||
# The game process uses these numeric IDs, not the bootstrap process.
|
|
||||||
# They need not be the host login user's IDs.
|
|
||||||
PUID: "1000"
|
PUID: "1000"
|
||||||
PGID: "1000"
|
PGID: "1000"
|
||||||
|
|
||||||
SERVER_NAME: "Fencl Valheim"
|
SERVER_NAME: "Fencl Valheim"
|
||||||
WORLD_NAME: "Fenheilm"
|
WORLD_NAME: "Fenheilm"
|
||||||
SERVER_PORT: "2456"
|
SERVER_PORT: "2456"
|
||||||
SERVER_PUBLIC: "false"
|
SERVER_PUBLIC: "false"
|
||||||
CROSSPLAY: "false"
|
CROSSPLAY: "false"
|
||||||
SERVER_PASS_FILE: "/run/secrets/valheim_server_pass"
|
SERVER_PASS_FILE: "/run/secrets/valheim_server_pass"
|
||||||
|
|
||||||
# No periodic game updates/restarts inside the container.
|
|
||||||
# A container start still checks/downloads the game through SteamCMD.
|
|
||||||
# Run update_valheim.yml in your chosen maintenance window.
|
|
||||||
UPDATE_CRON: ""
|
UPDATE_CRON: ""
|
||||||
RESTART_CRON: ""
|
RESTART_CRON: ""
|
||||||
|
|
||||||
# Built-in world backups, hourly at minute 05, retained for 7 days.
|
|
||||||
BACKUPS: "true"
|
BACKUPS: "true"
|
||||||
BACKUPS_CRON: "5 * * * *"
|
BACKUPS_CRON: "5 * * * *"
|
||||||
BACKUPS_DIRECTORY: "/config/backups"
|
BACKUPS_DIRECTORY: "/config/backups"
|
||||||
@@ -45,13 +34,10 @@ services:
|
|||||||
BACKUPS_MAX_COUNT: "168"
|
BACKUPS_MAX_COUNT: "168"
|
||||||
BACKUPS_IF_IDLE: "true"
|
BACKUPS_IF_IDLE: "true"
|
||||||
BACKUPS_ZIP: "true"
|
BACKUPS_ZIP: "true"
|
||||||
|
|
||||||
VALHEIM_PLUS: "false"
|
VALHEIM_PLUS: "false"
|
||||||
BEPINEX: "false"
|
BEPINEX: "true"
|
||||||
# Do not publish 9001: the Portainer agent already uses it on this VM.
|
|
||||||
SUPERVISOR_HTTP: "false"
|
SUPERVISOR_HTTP: "false"
|
||||||
STATUS_HTTP: "false"
|
STATUS_HTTP: "false"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- type: bind
|
- type: bind
|
||||||
source: /data/compose/valheim/config
|
source: /data/compose/valheim/config
|
||||||
@@ -74,10 +60,6 @@ services:
|
|||||||
read_only: true
|
read_only: true
|
||||||
bind:
|
bind:
|
||||||
create_host_path: false
|
create_host_path: false
|
||||||
|
|
||||||
# Local readiness: supervised game process AND an open game/query UDP socket.
|
|
||||||
# This does not test the router/firewall or an actual player connection.
|
|
||||||
# The helpers ship in this image and also support non-public servers.
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
- CMD
|
- CMD
|
||||||
@@ -91,15 +73,11 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 5
|
retries: 5
|
||||||
start_period: 30m
|
start_period: 30m
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
driver: json-file
|
driver: json-file
|
||||||
options:
|
options:
|
||||||
max-size: "10m"
|
max-size: "10m"
|
||||||
max-file: "3"
|
max-file: "3"
|
||||||
|
|
||||||
# Docker allocates this bridge network's subnet and the container address.
|
|
||||||
# Clients use the VM's LAN/public address and UDP port 2456, not this private IP.
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|||||||
+172
-85
@@ -1,10 +1,8 @@
|
|||||||
---
|
---
|
||||||
# update_valheim.yml
|
# Deploy or update existing Valheim plus a pinned server-side WebMap.
|
||||||
# Requires Docker Compose >= 2.18 and Python 3 on pve2_vm.
|
# WARNING: always restarts the game and may update Steam game binaries.
|
||||||
# Uses ansible.builtin only, like the command-based Minecraft update workflow.
|
# Never run concurrently with Portainer redeployments or other maintenance.
|
||||||
# A run ALWAYS restarts Valheim, even if the image did not change.
|
- name: Deploy or update Valheim with WebMap
|
||||||
# Do not run concurrently with another update or a Portainer redeployment.
|
|
||||||
- name: Deploy or update Valheim
|
|
||||||
hosts: pve2_vm
|
hosts: pve2_vm
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
become: true
|
become: true
|
||||||
@@ -12,16 +10,18 @@
|
|||||||
vars:
|
vars:
|
||||||
compose_local_dir: "{{ playbook_dir }}/docker-compose"
|
compose_local_dir: "{{ playbook_dir }}/docker-compose"
|
||||||
compose_remote_base: "/home/{{ ansible_user }}/.ansible-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"
|
compose_remote_dir: "{{ compose_remote_base }}/valheim"
|
||||||
valheim_compose_filename: "docker-compose-valheim.yml"
|
valheim_compose_filename: docker-compose-valheim.yml
|
||||||
valheim_project: valheim
|
valheim_project: valheim
|
||||||
valheim_service: valheim
|
valheim_service: valheim
|
||||||
valheim_wait_timeout: 1800
|
valheim_wait_timeout: 1800
|
||||||
valheim_preupdate_dir: "/backups/valheim/pre-update"
|
valheim_preupdate_dir: /backups/valheim/pre-update
|
||||||
valheim_preupdate_keep_days: 14
|
valheim_preupdate_keep_days: 14
|
||||||
|
webmap_version: "1.4.0"
|
||||||
|
webmap_url: "https://thunderstore.io/package/download/koenhendriks/ValheimWebMap/1.4.0/"
|
||||||
|
webmap_listen_port: 3696
|
||||||
|
webmap_host_port: 8095
|
||||||
|
|
||||||
# Read paths/UID/GID from the controller's compose file to avoid duplication.
|
|
||||||
valheim_compose: >-
|
valheim_compose: >-
|
||||||
{{ lookup('ansible.builtin.file', compose_local_dir ~ '/' ~ valheim_compose_filename) | from_yaml }}
|
{{ lookup('ansible.builtin.file', compose_local_dir ~ '/' ~ valheim_compose_filename) | from_yaml }}
|
||||||
valheim_spec: "{{ valheim_compose.services[valheim_service] }}"
|
valheim_spec: "{{ valheim_compose.services[valheim_service] }}"
|
||||||
@@ -32,6 +32,8 @@
|
|||||||
{{ (valheim_spec.volumes | selectattr('target', 'equalto', '/run/secrets/valheim_server_pass') | first).source }}
|
{{ (valheim_spec.volumes | selectattr('target', 'equalto', '/run/secrets/valheim_server_pass') | first).source }}
|
||||||
valheim_config_dir: >-
|
valheim_config_dir: >-
|
||||||
{{ (valheim_spec.volumes | selectattr('target', 'equalto', '/config') | first).source }}
|
{{ (valheim_spec.volumes | selectattr('target', 'equalto', '/config') | first).source }}
|
||||||
|
webmap_plugin_dir: "{{ valheim_config_dir }}/bepinex/plugins/ValheimWebMap"
|
||||||
|
webmap_config_file: "{{ valheim_config_dir }}/bepinex/config/com.valheimwebmap.cfg"
|
||||||
valheim_compose_argv:
|
valheim_compose_argv:
|
||||||
- docker
|
- docker
|
||||||
- compose
|
- compose
|
||||||
@@ -43,11 +45,19 @@
|
|||||||
- "{{ compose_remote_dir }}/{{ valheim_compose_filename }}"
|
- "{{ compose_remote_dir }}/{{ valheim_compose_filename }}"
|
||||||
|
|
||||||
pre_tasks:
|
pre_tasks:
|
||||||
- name: Reject check mode before making changes
|
- name: Reject check mode
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that: [not ansible_check_mode]
|
||||||
|
fail_msg: "Maintenance does not support --check; use --syntax-check."
|
||||||
|
|
||||||
|
- name: Validate desired service settings before touching the VM
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- not ansible_check_mode
|
- valheim_spec.environment.BEPINEX | bool
|
||||||
fail_msg: "This maintenance playbook does not support --check. Use --syntax-check instead."
|
- not (valheim_spec.environment.VALHEIM_PLUS | bool)
|
||||||
|
- valheim_spec.environment.WORLD_NAME == 'Fenheilm'
|
||||||
|
- "'8095:3696/tcp' in valheim_spec.ports"
|
||||||
|
fail_msg: "Expected unchanged world Fenheilm, BepInEx enabled and TCP 8095:3696."
|
||||||
|
|
||||||
- name: Check Docker Compose version
|
- name: Check Docker Compose version
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
@@ -55,32 +65,28 @@
|
|||||||
register: valheim_compose_version
|
register: valheim_compose_version
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
|
||||||
- name: Require Compose with wait support
|
- name: Require Docker Compose 2.18+
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- >-
|
- >-
|
||||||
(valheim_compose_version.stdout | regex_search('[0-9]+\.[0-9]+\.[0-9]+'))
|
(valheim_compose_version.stdout | regex_search('[0-9]+\.[0-9]+\.[0-9]+'))
|
||||||
is version('2.18.0', '>=')
|
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
|
- name: Inspect persistent password without reading it
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "{{ valheim_password_file }}"
|
path: "{{ valheim_password_file }}"
|
||||||
get_checksum: false
|
get_checksum: false
|
||||||
get_mime: false
|
get_mime: false
|
||||||
register: valheim_password_stat
|
register: valheim_password_stat
|
||||||
|
|
||||||
- name: Require a real password file on the VM
|
- name: Require existing regular password file
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- valheim_password_stat.stat.isreg | default(false)
|
- valheim_password_stat.stat.isreg | default(false)
|
||||||
- not (valheim_password_stat.stat.islnk | default(false))
|
- not (valheim_password_stat.stat.islnk | default(false))
|
||||||
fail_msg: >-
|
fail_msg: "Create {{ valheim_password_file }} first; it must contain the password on one line."
|
||||||
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
|
- name: Validate password without disclosing it
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
argv:
|
argv:
|
||||||
- python3
|
- python3
|
||||||
@@ -91,22 +97,15 @@
|
|||||||
try:
|
try:
|
||||||
password = Path(sys.argv[1]).read_text(encoding="utf-8").rstrip("\r\n")
|
password = Path(sys.argv[1]).read_text(encoding="utf-8").rstrip("\r\n")
|
||||||
except (OSError, UnicodeError):
|
except (OSError, UnicodeError):
|
||||||
raise SystemExit("Cannot read the password file as UTF-8.")
|
raise SystemExit("Invalid UTF-8 password file")
|
||||||
if (len(password) < 5 or "\n" in password or "\r" in password
|
if (len(password) < 5 or "\n" in password or "\r" in password
|
||||||
or "\x00" in password or password.startswith("SERVER_PASS=")):
|
or "\x00" in password or password.startswith("SERVER_PASS=")):
|
||||||
raise SystemExit("Use a single plain password of at least 5 characters.")
|
raise SystemExit("Invalid password file format")
|
||||||
- "{{ valheim_password_file }}"
|
- "{{ valheim_password_file }}"
|
||||||
changed_when: false
|
changed_when: false
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
- name: Restrict password ownership and permissions to the configured game UID
|
- name: Inspect existing container
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ valheim_password_file }}"
|
|
||||||
owner: "{{ valheim_uid }}"
|
|
||||||
group: "{{ valheim_gid }}"
|
|
||||||
mode: "0600"
|
|
||||||
|
|
||||||
- name: Inspect an existing Valheim container
|
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
argv: [docker, container, inspect, "{{ valheim_container }}"]
|
argv: [docker, container, inspect, "{{ valheim_container }}"]
|
||||||
register: valheim_inspect
|
register: valheim_inspect
|
||||||
@@ -115,71 +114,74 @@
|
|||||||
valheim_inspect.rc != 0 and 'No such' not in valheim_inspect.stderr
|
valheim_inspect.rc != 0 and 'No such' not in valheim_inspect.stderr
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
- name: Save only the existing container metadata in Ansible memory
|
- name: Keep existing metadata in memory
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
valheim_existing: "{{ (valheim_inspect.stdout | from_json | first) if valheim_inspect.rc == 0 else {} }}"
|
valheim_existing: "{{ (valheim_inspect.stdout | from_json | first) if valheim_inspect.rc == 0 else {} }}"
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
- name: Refuse to take over a container from a different project or service
|
- name: Do not take over a container belonging to another Compose service
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- >-
|
- >-
|
||||||
(valheim_existing.get('Config', {}).get('Labels') or {}).get('com.docker.compose.project')
|
(valheim_existing.get('Config', {}).get('Labels') or {}).get('com.docker.compose.project') == valheim_project
|
||||||
== valheim_project
|
|
||||||
- >-
|
- >-
|
||||||
(valheim_existing.get('Config', {}).get('Labels') or {}).get('com.docker.compose.service')
|
(valheim_existing.get('Config', {}).get('Labels') or {}).get('com.docker.compose.service') == valheim_service
|
||||||
== valheim_service
|
fail_msg: "Existing container belongs to a different project or service."
|
||||||
fail_msg: "Container name is occupied. The existing container must belong to stack/service valheim."
|
|
||||||
when: valheim_existing | length > 0
|
when: valheim_existing | length > 0
|
||||||
|
|
||||||
- name: Refuse a silent switch to different persistent data or password paths
|
- name: Refuse switching existing bind paths
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- >-
|
- >-
|
||||||
(valheim_existing.Mounts | selectattr('Destination', 'equalto', item.target)
|
(valheim_existing.Mounts | selectattr('Destination', 'equalto', item.target)
|
||||||
| map(attribute='Source') | list) == [item.source]
|
| map(attribute='Source') | list) == [item.source]
|
||||||
fail_msg: >-
|
fail_msg: "Existing mount {{ item.target }} does not match {{ item.source }}."
|
||||||
Existing mount {{ item.target }} does not match {{ item.source }}.
|
|
||||||
Migrate the data deliberately before changing the compose bind paths.
|
|
||||||
loop: "{{ valheim_spec.volumes }}"
|
loop: "{{ valheim_spec.volumes }}"
|
||||||
loop_control:
|
loop_control:
|
||||||
label: "{{ item.target }}"
|
label: "{{ item.target }}"
|
||||||
when: valheim_existing | length > 0
|
when: valheim_existing | length > 0
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Ensure the dedicated remote compose directory exists
|
- name: Restrict password ownership and permissions
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ valheim_password_file }}"
|
||||||
|
owner: "{{ valheim_uid }}"
|
||||||
|
group: "{{ valheim_gid }}"
|
||||||
|
mode: '0600'
|
||||||
|
|
||||||
|
- name: Ensure isolated remote Compose directory exists
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ compose_remote_dir }}"
|
path: "{{ compose_remote_dir }}"
|
||||||
state: directory
|
state: directory
|
||||||
owner: "{{ ansible_user }}"
|
owner: "{{ ansible_user }}"
|
||||||
mode: "0755"
|
mode: '0755'
|
||||||
|
|
||||||
- name: Ensure the writable host bind directories exist
|
- name: Ensure writable bind directories exist without replacing their contents
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ item.source }}"
|
path: "{{ item.source }}"
|
||||||
state: directory
|
state: directory
|
||||||
owner: "{{ valheim_uid }}"
|
owner: "{{ valheim_uid }}"
|
||||||
group: "{{ valheim_gid }}"
|
group: "{{ valheim_gid }}"
|
||||||
mode: "0750"
|
mode: '0750'
|
||||||
loop: "{{ valheim_spec.volumes }}"
|
loop: "{{ valheim_spec.volumes }}"
|
||||||
when: not (item.read_only | default(false) | bool)
|
when: not (item.read_only | default(false) | bool)
|
||||||
loop_control:
|
loop_control:
|
||||||
label: "{{ item.source }}"
|
label: "{{ item.source }}"
|
||||||
|
|
||||||
- name: Ensure the offline pre-update backup directory exists
|
- name: Ensure offline backup directory exists
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ valheim_preupdate_dir }}"
|
path: "{{ valheim_preupdate_dir }}"
|
||||||
state: directory
|
state: directory
|
||||||
mode: "0755"
|
mode: '0755'
|
||||||
|
|
||||||
- name: Upload only the Valheim compose file
|
- name: Upload only Valheim Compose file
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
src: "{{ compose_local_dir }}/{{ valheim_compose_filename }}"
|
src: "{{ compose_local_dir }}/{{ valheim_compose_filename }}"
|
||||||
dest: "{{ compose_remote_dir }}/{{ valheim_compose_filename }}"
|
dest: "{{ compose_remote_dir }}/{{ valheim_compose_filename }}"
|
||||||
owner: "{{ ansible_user }}"
|
owner: "{{ ansible_user }}"
|
||||||
mode: "0644"
|
mode: '0644'
|
||||||
|
|
||||||
- name: Validate the compose configuration before interrupting the game
|
- name: Validate Compose configuration before stopping the game
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
argv: "{{ valheim_compose_argv + ['config', '--quiet'] }}"
|
argv: "{{ valheim_compose_argv + ['config', '--quiet'] }}"
|
||||||
changed_when: false
|
changed_when: false
|
||||||
@@ -187,26 +189,54 @@
|
|||||||
- name: Pull the image while the old server is still running
|
- name: Pull the image while the old server is still running
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
argv: "{{ valheim_compose_argv + ['pull', valheim_service] }}"
|
argv: "{{ valheim_compose_argv + ['pull', valheim_service] }}"
|
||||||
# Pull output is not a stable machine-readable change indicator.
|
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
|
||||||
- name: Stop and make a consistent offline config/world backup
|
- name: Create staging directory outside existing game data
|
||||||
|
ansible.builtin.tempfile:
|
||||||
|
state: directory
|
||||||
|
prefix: valheim-webmap-
|
||||||
|
register: webmap_stage
|
||||||
|
|
||||||
|
- name: Download fixed version of the server-side WebMap package
|
||||||
|
ansible.builtin.get_url:
|
||||||
|
url: "{{ webmap_url }}"
|
||||||
|
dest: "{{ webmap_stage.path }}/ValheimWebMap-{{ webmap_version }}.zip"
|
||||||
|
mode: '0600'
|
||||||
|
timeout: 90
|
||||||
|
|
||||||
|
- name: Copy the safe extraction helper to staging
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: "{{ playbook_dir }}/scripts/stage_webmap.py"
|
||||||
|
dest: "{{ webmap_stage.path }}/stage_webmap.py"
|
||||||
|
mode: '0700'
|
||||||
|
|
||||||
|
- name: Validate archive contents and stage only the plugin DLL
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- python3
|
||||||
|
- "{{ webmap_stage.path }}/stage_webmap.py"
|
||||||
|
- "{{ webmap_stage.path }}/ValheimWebMap-{{ webmap_version }}.zip"
|
||||||
|
- "{{ webmap_stage.path }}/ValheimWebMap.dll"
|
||||||
|
register: webmap_dll_hash
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Stop and back up existing config/world before changing plugins
|
||||||
block:
|
block:
|
||||||
- name: Gracefully stop only the existing Valheim container
|
- name: Gracefully stop only existing Valheim container
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
argv: [docker, stop, --time, "120", "{{ valheim_container }}"]
|
argv: [docker, stop, --time, '120', "{{ valheim_container }}"]
|
||||||
when: valheim_existing.get('State', {}).get('Running', false)
|
when: valheim_existing.get('State', {}).get('Running', false)
|
||||||
changed_when: true
|
changed_when: true
|
||||||
|
|
||||||
- name: Allocate a unique pre-update archive filename
|
- name: Allocate a unique offline pre-update archive path
|
||||||
ansible.builtin.tempfile:
|
ansible.builtin.tempfile:
|
||||||
state: file
|
state: file
|
||||||
path: "{{ valheim_preupdate_dir }}"
|
path: "{{ valheim_preupdate_dir }}"
|
||||||
prefix: "config-{{ now(utc=true, fmt='%Y%m%dT%H%M%SZ') }}-"
|
prefix: "config-{{ now(utc=true, fmt='%Y%m%dT%H%M%SZ') }}-"
|
||||||
suffix: ".tar.gz"
|
suffix: .tar.gz
|
||||||
register: valheim_backup
|
register: valheim_backup
|
||||||
|
|
||||||
- name: Back up config and worlds, excluding the separate automatic backup mount
|
- name: Back up the config and world directory without automatic backup mount
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
argv:
|
argv:
|
||||||
- tar
|
- tar
|
||||||
@@ -219,20 +249,57 @@
|
|||||||
- "{{ valheim_config_dir }}"
|
- "{{ valheim_config_dir }}"
|
||||||
- .
|
- .
|
||||||
changed_when: true
|
changed_when: true
|
||||||
|
|
||||||
rescue:
|
rescue:
|
||||||
- name: Abort rather than update without a completed backup
|
- name: Fail without deploying a new container on backup failure
|
||||||
ansible.builtin.fail:
|
ansible.builtin.fail:
|
||||||
msg: >-
|
msg: >-
|
||||||
Stop/backup failed; the replacement container was not deployed.
|
Stop or backup failed; no replacement deployed. Existing container may be stopped.
|
||||||
The original container may now be stopped. It is NOT automatically restarted,
|
It is not restarted automatically because a start can trigger a Steam update.
|
||||||
because starting it can itself trigger a Steam game update without a valid backup.
|
Check disk space and repair the failure; an incomplete archive might remain.
|
||||||
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
|
- name: Install WebMap and recreate Valheim
|
||||||
block:
|
block:
|
||||||
- name: Recreate only Valheim and wait for its healthcheck
|
- name: Create persistent WebMap plugin directory
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ webmap_plugin_dir }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ valheim_uid }}"
|
||||||
|
group: "{{ valheim_gid }}"
|
||||||
|
mode: '0750'
|
||||||
|
|
||||||
|
- name: Install only the staged plugin DLL, not a full ZIP tree
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: "{{ webmap_stage.path }}/ValheimWebMap.dll"
|
||||||
|
dest: "{{ webmap_plugin_dir }}/ValheimWebMap.dll"
|
||||||
|
remote_src: true
|
||||||
|
owner: "{{ valheim_uid }}"
|
||||||
|
group: "{{ valheim_gid }}"
|
||||||
|
mode: '0644'
|
||||||
|
|
||||||
|
- name: Upload conservative WebMap config editor
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: "{{ playbook_dir }}/scripts/set_webmap_config.py"
|
||||||
|
dest: "{{ webmap_stage.path }}/set_webmap_config.py"
|
||||||
|
mode: '0700'
|
||||||
|
|
||||||
|
- name: Set WebMap's INTERNAL port to 3696 and disable player history
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- python3
|
||||||
|
- "{{ webmap_stage.path }}/set_webmap_config.py"
|
||||||
|
- "{{ webmap_config_file }}"
|
||||||
|
- "{{ webmap_listen_port | string }}"
|
||||||
|
register: webmap_cfg_result
|
||||||
|
changed_when: "'changed' in webmap_cfg_result.stdout"
|
||||||
|
|
||||||
|
- name: Ensure WebMap config belongs to the game user
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ webmap_config_file }}"
|
||||||
|
owner: "{{ valheim_uid }}"
|
||||||
|
group: "{{ valheim_gid }}"
|
||||||
|
mode: '0640'
|
||||||
|
|
||||||
|
- name: Recreate Valheim with BepInEx and wait for existing game healthcheck
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
argv: >-
|
argv: >-
|
||||||
{{ valheim_compose_argv + ['up', '--detach', '--force-recreate', '--no-deps',
|
{{ valheim_compose_argv + ['up', '--detach', '--force-recreate', '--no-deps',
|
||||||
@@ -240,44 +307,61 @@
|
|||||||
valheim_service] }}
|
valheim_service] }}
|
||||||
changed_when: true
|
changed_when: true
|
||||||
|
|
||||||
- name: Read new-container startup logs without exposing them in Semaphore
|
- name: Read startup logs privately
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
argv: [docker, logs, "{{ valheim_container }}"]
|
argv: [docker, logs, "{{ valheim_container }}"]
|
||||||
register: valheim_start_logs
|
register: valheim_start_logs
|
||||||
changed_when: false
|
changed_when: false
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
- name: Confirm Steam update completed rather than falling back to old binaries
|
- name: Confirm successful Steam check
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- >-
|
- >-
|
||||||
'Valheim Server was updated - restarting' in (valheim_start_logs.stdout ~ valheim_start_logs.stderr)
|
'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)
|
or 'Valheim Server is already the latest version' in (valheim_start_logs.stdout ~ valheim_start_logs.stderr)
|
||||||
fail_msg: >-
|
fail_msg: "No successful Steam check found; inspect docker logs privately on the VM."
|
||||||
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
|
- name: Wait for WebMap's actual info endpoint on the host port
|
||||||
may be using its previous game installation, or upstream log wording changed.
|
ansible.builtin.uri:
|
||||||
|
url: "http://127.0.0.1:{{ webmap_host_port }}/api/info"
|
||||||
|
method: GET
|
||||||
|
status_code: 200
|
||||||
|
timeout: 10
|
||||||
|
return_content: true
|
||||||
|
register: webmap_http
|
||||||
|
until: >-
|
||||||
|
webmap_http.status | default(0) == 200 and
|
||||||
|
'Fenheilm' in (webmap_http.content | default(''))
|
||||||
|
retries: 18
|
||||||
|
delay: 10
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
rescue:
|
rescue:
|
||||||
- name: Report failure without destructive rollback or dumping potentially sensitive logs
|
- name: Fail safely without attempting an automatic downgrade
|
||||||
ansible.builtin.fail:
|
ansible.builtin.fail:
|
||||||
msg: >-
|
msg: >-
|
||||||
Valheim readiness or Steam-update verification failed.
|
WebMap installation or readiness failed. No automatic restore attempted.
|
||||||
Inspect 'docker ps -a --filter name=valheim' and 'docker logs --tail 150 valheim'
|
The offline config/world archive is {{ valheim_backup.path }}.
|
||||||
on pve2_vm. The pre-update config/world archive is {{ valheim_backup.path }}.
|
Inspect 'docker ps -a --filter name=valheim', 'docker logs --tail 150 valheim'
|
||||||
No data directories were deleted and no automatic downgrade/restore was attempted.
|
and /config/bepinex logs. Existing world bind mounts were not deleted.
|
||||||
|
always:
|
||||||
|
- name: Remove staged download and scripts
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ webmap_stage.path }}"
|
||||||
|
state: absent
|
||||||
|
|
||||||
- name: Find expired offline archives only after a successful deployment
|
- name: Find expired pre-update archives after successful deployment only
|
||||||
ansible.builtin.find:
|
ansible.builtin.find:
|
||||||
paths: "{{ valheim_preupdate_dir }}"
|
paths: "{{ valheim_preupdate_dir }}"
|
||||||
patterns: "config-*.tar.gz"
|
patterns: config-*.tar.gz
|
||||||
age: "{{ valheim_preupdate_keep_days }}d"
|
age: "{{ valheim_preupdate_keep_days }}d"
|
||||||
age_stamp: mtime
|
age_stamp: mtime
|
||||||
file_type: file
|
file_type: file
|
||||||
recurse: false
|
recurse: false
|
||||||
register: valheim_old_backups
|
register: valheim_old_backups
|
||||||
|
|
||||||
- name: Remove only expired Valheim pre-update archives
|
- name: Remove expired Valheim pre-update archives
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ item.path }}"
|
path: "{{ item.path }}"
|
||||||
state: absent
|
state: absent
|
||||||
@@ -285,9 +369,12 @@
|
|||||||
loop_control:
|
loop_control:
|
||||||
label: "{{ item.path }}"
|
label: "{{ item.path }}"
|
||||||
|
|
||||||
- name: Report completion
|
- name: Report successful deployment
|
||||||
ansible.builtin.debug:
|
ansible.builtin.debug:
|
||||||
msg:
|
msg:
|
||||||
- "Valheim passed local health and startup Steam-update checks."
|
- "Valheim game health + Steam check passed; WebMap /api/info returned Fenheilm."
|
||||||
|
- "WebMap URL: http://<pve2_vm>:{{ webmap_host_port }}/"
|
||||||
|
- "Internal WebMap port: {{ webmap_listen_port }}"
|
||||||
- "Offline config/world backup: {{ valheim_backup.path }}"
|
- "Offline config/world backup: {{ valheim_backup.path }}"
|
||||||
- "Join using the VM address and UDP port 2456. Router/firewall reachability is not tested."
|
- "WebMap DLL SHA256: {{ webmap_dll_hash.stdout }}"
|
||||||
|
- "No external reachability test performed."
|
||||||
|
|||||||
Reference in New Issue
Block a user