Add Synology DSM support (#5315)

Adds optional support for running the playbook on Synology DSM 7+, detected
automatically via /etc/synoinfo.conf so that non-Synology hosts are unaffected.

Includes DSM-native user/group management (synouser/synogroup), a requests
version constraint for Docker SDK compatibility, and a boot-fix service that
re-shares the volume mount and starts matrix services skipped by DSM's boot
ordering. The shared-mount volume path is configurable via
matrix_base_synology_volume_path, and the make-shared step only runs when the
volume is not already shared.

Co-authored-by: CKSit <sitchiuki@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
cksit
2026-06-30 00:45:01 +08:00
committed by GitHub
parent 4f9346e182
commit ee1cd217a8
13 changed files with 490 additions and 23 deletions
@@ -204,6 +204,26 @@ matrix_group_system: true
matrix_user_uid: ~
matrix_user_gid: ~
# Controls Synology DSM-specific handling. `null` means autodetect (via /etc/synoinfo.conf).
# Set to `true`/`false` to force.
matrix_base_host_is_synology: ~
# Password for the Matrix service account on Synology DSM.
# Must be set to a non-empty value in your vars.yml when running on Synology.
# The account is created as expired so the password cannot be used to log in.
matrix_synology_user_password: ""
# Version constraint for the requests Python package installed on Synology hosts.
# requests >= 2.32 dropped the http+docker URL scheme used by the Docker SDK,
# causing "Not supported URL scheme http+docker" errors. Installed into the
# system Python interpreter (ansible_python_interpreter) on the remote host.
matrix_base_synology_requests_version_constraint: "requests<2.32"
# Synology volume that needs shared mount propagation so that Docker
# bind-propagation=slave mounts (used by matrix-synapse for its media store)
# work correctly. Defaults to /volume1 (DSM's default Docker storage volume).
matrix_base_synology_volume_path: "/volume1"
matrix_base_data_path: "/matrix"
matrix_base_data_path_mode: "750"
@@ -0,0 +1,16 @@
# SPDX-FileCopyrightText: 2026 Chiu Ki Sit
#
# SPDX-License-Identifier: AGPL-3.0-or-later
---
- name: Detect Synology DSM
ansible.builtin.stat:
path: /etc/synoinfo.conf
register: matrix_base_synoinfo_conf_stat
when: matrix_base_host_is_synology is none
- name: Set matrix_base_host_is_synology from detection
ansible.builtin.set_fact:
matrix_base_host_is_synology: "{{ matrix_base_synoinfo_conf_stat.stat.exists }}"
when: matrix_base_host_is_synology is none
+13
View File
@@ -4,6 +4,7 @@
# SPDX-FileCopyrightText: 2020 Marcel Partap
# SPDX-FileCopyrightText: 2022 Marko Weltzer
# SPDX-FileCopyrightText: 2022 Warren Bailey
# SPDX-FileCopyrightText: 2026 Chiu Ki Sit
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@@ -15,6 +16,11 @@
block:
- ansible.builtin.include_tasks: "{{ role_path }}/tasks/validate_config.yml"
- tags:
- always
block:
- ansible.builtin.include_tasks: "{{ role_path }}/tasks/detect_platform.yml"
# This needs to always run, because it populates `matrix_user_uid` and `matrix_user_gid`,
# which are required by many other roles.
- tags:
@@ -24,6 +30,13 @@
block:
- ansible.builtin.include_tasks: "{{ role_path }}/tasks/setup_matrix_user.yml"
- tags:
- setup-all
- install-all
block:
- ansible.builtin.include_tasks: "{{ role_path }}/tasks/setup_synology_prerequisites.yml"
when: matrix_base_host_is_synology
- tags:
- setup-all
- install-all
@@ -7,11 +7,20 @@
# SPDX-FileCopyrightText: 2022 Sebastian Gumprich
# SPDX-FileCopyrightText: 2024 - 2025 Suguru Hirahara
# SPDX-FileCopyrightText: 2024 László Várady
# SPDX-FileCopyrightText: 2026 Chiu Ki Sit
#
# SPDX-License-Identifier: AGPL-3.0-or-later
---
# Snapshot ownership before any changes so we can decide whether a recursive
# chown is needed (only when uid/gid actually differs from expected).
- name: Check current ownership of Matrix base path (Synology)
ansible.builtin.stat:
path: "{{ matrix_base_data_path }}"
register: matrix_base_data_path_stat
when: matrix_base_host_is_synology
- name: Ensure Matrix base paths exists
ansible.builtin.file:
path: "{{ item }}"
@@ -28,3 +37,18 @@
src: "{{ role_path }}/templates/bin/remove-all.j2"
dest: "{{ matrix_bin_path }}/remove-all"
mode: '0750'
# On Synology, name-based chown works for directly-touched paths but leaves
# existing sub-paths with stale numeric ownership when uid/gid changes between
# runs. We recurse only when the pre-task uid/gid didn't match, so normal runs
# skip the expensive tree walk entirely. chown -R is used instead of the file
# module's recurse option to avoid Ansible iterating every entry in Python.
- name: Ensure Matrix base path ownership is correct using numeric UID/GID (Synology)
ansible.builtin.command: chown -R {{ matrix_user_uid }}:{{ matrix_user_gid }} {{ matrix_base_data_path }}
changed_when: true
when: >-
matrix_base_host_is_synology and (
not matrix_base_data_path_stat.stat.exists or
matrix_base_data_path_stat.stat.uid | int != matrix_user_uid | int or
matrix_base_data_path_stat.stat.gid | int != matrix_user_gid | int
)
@@ -1,31 +1,13 @@
# SPDX-FileCopyrightText: 2020 - 2022 Slavi Pantaleev
# SPDX-FileCopyrightText: 2022 Marko Weltzer
# SPDX-FileCopyrightText: 2026 Chiu Ki Sit
#
# SPDX-License-Identifier: AGPL-3.0-or-later
---
- name: Ensure Matrix group is created
ansible.builtin.group:
name: "{{ matrix_group_name }}"
gid: "{{ omit if matrix_user_gid is none else matrix_user_gid }}"
state: present
system: "{{ matrix_group_system }}"
register: matrix_group
- ansible.builtin.include_tasks: "{{ role_path }}/tasks/setup_matrix_user_synology.yml"
when: matrix_base_host_is_synology
- name: Ensure Matrix user is created
ansible.builtin.user:
name: "{{ matrix_user_name }}"
uid: "{{ omit if matrix_user_uid is none else matrix_user_uid }}"
state: present
group: "{{ matrix_group_name }}"
home: "{{ matrix_base_data_path }}"
create_home: false
system: "{{ matrix_user_system }}"
shell: "{{ matrix_user_shell }}"
register: matrix_user
- name: Initialize matrix_user_uid and matrix_user_gid
ansible.builtin.set_fact:
matrix_user_uid: "{{ matrix_user.uid }}"
matrix_user_gid: "{{ matrix_group.gid }}"
- ansible.builtin.include_tasks: "{{ role_path }}/tasks/setup_matrix_user_linux.yml"
when: not matrix_base_host_is_synology
@@ -0,0 +1,31 @@
# SPDX-FileCopyrightText: 2020 - 2022 Slavi Pantaleev
# SPDX-FileCopyrightText: 2022 Marko Weltzer
#
# SPDX-License-Identifier: AGPL-3.0-or-later
---
- name: Ensure Matrix group is created
ansible.builtin.group:
name: "{{ matrix_group_name }}"
gid: "{{ omit if matrix_user_gid is none else matrix_user_gid }}"
state: present
system: "{{ matrix_group_system }}"
register: matrix_group
- name: Ensure Matrix user is created
ansible.builtin.user:
name: "{{ matrix_user_name }}"
uid: "{{ omit if matrix_user_uid is none else matrix_user_uid }}"
state: present
group: "{{ matrix_group_name }}"
home: "{{ matrix_base_data_path }}"
create_home: false
system: "{{ matrix_user_system }}"
shell: "{{ matrix_user_shell }}"
register: matrix_user
- name: Initialize matrix_user_uid and matrix_user_gid
ansible.builtin.set_fact:
matrix_user_uid: "{{ matrix_user.uid }}"
matrix_user_gid: "{{ matrix_group.gid }}"
@@ -0,0 +1,69 @@
# SPDX-FileCopyrightText: 2026 Chiu Ki Sit
#
# SPDX-License-Identifier: AGPL-3.0-or-later
---
- name: Fail if matrix_synology_user_password is not set
ansible.builtin.fail:
msg: >-
You must set `matrix_synology_user_password` to a non-empty value in your vars.yml.
This password secures the Matrix service account on Synology DSM.
The account is created as expired so the password cannot be used to log in.
when: matrix_synology_user_password == '' or matrix_synology_user_password is none
- name: Check if Matrix user exists (Synology)
ansible.builtin.command: id {{ matrix_user_name }}
register: matrix_user_check
changed_when: false
failed_when: false
# Created with expired=1 (cannot log in)
# as this is a service account. If you pre-create the user, you are responsible
# for securing it; the playbook will not modify an existing account's settings.
- name: Ensure Matrix user is created (Synology)
ansible.builtin.command: >
/usr/syno/sbin/synouser --add {{ matrix_user_name }}
"{{ matrix_synology_user_password }}" "{{ matrix_user_name }}" 1 "" 0
when: matrix_user_check.rc != 0
changed_when: true
no_log: true
- name: Ensure Matrix user password is up to date (Synology)
ansible.builtin.command: /usr/syno/sbin/synouser --setpw {{ matrix_user_name }} "{{ matrix_synology_user_password }}"
when: matrix_user_check.rc == 0
changed_when: false
no_log: true
- name: Check if Matrix group exists (Synology)
ansible.builtin.command: /usr/syno/sbin/synogroup --get {{ matrix_group_name }}
register: matrix_group_check
changed_when: false
failed_when: false
- name: Ensure Matrix group is created (Synology)
ansible.builtin.command: /usr/syno/sbin/synogroup --add {{ matrix_group_name }} {{ matrix_user_name }}
when: matrix_group_check.rc != 0
changed_when: true
- name: Get Matrix user UID (Synology)
ansible.builtin.command: id -u {{ matrix_user_name }}
register: matrix_user_uid_result
changed_when: false
- name: Get Matrix group info (Synology)
ansible.builtin.command: /usr/syno/sbin/synogroup --get {{ matrix_group_name }}
register: matrix_synogroup_result
changed_when: false
- name: Initialize matrix_user_uid and matrix_user_gid
ansible.builtin.set_fact:
matrix_user_uid: "{{ matrix_user_uid_result.stdout }}"
matrix_user_gid: >-
{{
matrix_synogroup_result.stdout_lines
| select('match', '^Group ID:')
| first
| regex_search('\[(\d+)\]', '\1')
| first
}}
@@ -0,0 +1,27 @@
# SPDX-FileCopyrightText: 2026 Chiu Ki Sit
#
# SPDX-License-Identifier: AGPL-3.0-or-later
---
- name: Deploy Matrix boot recovery script (Synology)
ansible.builtin.template:
src: "{{ role_path }}/templates/bin/matrix-synology-boot-fix.j2"
dest: "{{ matrix_bin_path }}/matrix-synology-boot-fix"
mode: "0750"
owner: root
group: root
- name: Deploy Matrix boot recovery service (Synology)
ansible.builtin.template:
src: "{{ role_path }}/templates/systemd/matrix-synology-boot-fix.service.j2"
dest: /etc/systemd/system/matrix-synology-boot-fix.service
mode: "0644"
register: matrix_synology_boot_fix_service
- name: Reload systemd and enable Matrix boot recovery service (Synology)
ansible.builtin.systemd:
name: matrix-synology-boot-fix.service
daemon_reload: true
enabled: true
when: matrix_synology_boot_fix_service.changed
@@ -0,0 +1,34 @@
# SPDX-FileCopyrightText: 2026 Chiu Ki Sit
#
# SPDX-License-Identifier: AGPL-3.0-or-later
---
- name: Ensure requests Python package is constrained for Docker SDK compatibility (Synology)
ansible.builtin.pip:
name: "{{ matrix_base_synology_requests_version_constraint }}"
state: present
# Determine whether the volume is already a shared mount, so that the
# make-shared command below only runs (and only reports `changed`) when it
# actually needs to. We read /proc/self/mountinfo (always present on Linux)
# and look for the ` shared:` optional tag on the volume's mount point line.
# grep exits non-zero on no-match or any error, so the make-shared command is
# skipped only when shared propagation is positively confirmed; every other
# case falls through to running it (which is idempotent).
- name: Determine current mount propagation of the Synology volume
ansible.builtin.command: grep -E ' {{ matrix_base_synology_volume_path }} .* shared:' /proc/self/mountinfo
register: matrix_base_synology_volume_propagation
changed_when: false
failed_when: false
# Run immediately during setup so matrix services can start without a manual
# step. The boot-fix service handles this on every subsequent reboot.
# noqa command-instead-of-module: ansible.builtin.mount does not support
# changing mount propagation (--make-shared); command is the only option here.
- name: Ensure the Synology volume has shared mount propagation
ansible.builtin.command: mount --make-shared {{ matrix_base_synology_volume_path }} # noqa command-instead-of-module
when: matrix_base_synology_volume_propagation.rc != 0
changed_when: true
- ansible.builtin.include_tasks: "{{ role_path }}/tasks/setup_synology_boot_fix.yml"
@@ -0,0 +1,54 @@
#!/bin/sh
# SPDX-FileCopyrightText: 2026 Chiu Ki Sit
#
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Boot recovery for Matrix services on Synology DSM.
#
# This script runs after multi-user.target (outside Container Manager's dependency
# chain) and does two things:
#
# 1. Makes {{ matrix_base_synology_volume_path }} mount-shared so Docker bind-propagation=slave mounts work.
# Inserting this into the systemd chain Before=pkg-ContainerManager-dockerd.service
# causes Container Manager to detect a broken dependency and prompt for repair,
# so it must run here instead, after Docker is already up.
#
# 2. Starts any enabled matrix-*.service that systemd skipped at boot.
# Synology's systemd drops services with multi-level dependency chains
# (e.g. traefik -> socket-proxy -> docker) from the boot activation queue.
# Services that need bind-propagation=slave (e.g. matrix-synapse) are
# created after step 1, so the propagation is already in effect.
# Wait up to 120s for Docker to be ready
i=0
while [ "$i" -lt 60 ]; do
{{ devture_systemd_docker_base_host_command_docker }} info >/dev/null 2>&1 && break
i=$((i + 1))
sleep 2
done
if ! {{ devture_systemd_docker_base_host_command_docker }} info >/dev/null 2>&1; then
echo "matrix-synology-boot-fix: Docker not ready after 120s, aborting" >&2
exit 1
fi
# Make {{ matrix_base_synology_volume_path }} shared so Docker bind-propagation=slave mounts work correctly.
# Must run after Docker is up to avoid interfering with Container Manager's
# integrity checks, but before matrix-synapse (and any other service using
# bind-propagation=slave) creates its containers.
/bin/mount --make-shared {{ matrix_base_synology_volume_path }}
echo "matrix-synology-boot-fix: {{ matrix_base_synology_volume_path }} set to shared mount propagation"
# Start any enabled matrix-*.service that is inactive or failed.
# Both states indicate the service did not come up at boot — either skipped by
# Synology's boot ordering or failed due to Docker/mount-propagation not being
# ready yet (the conditions above now satisfy those prerequisites).
{{ devture_systemd_docker_base_host_command_systemctl }} list-unit-files 'matrix-*.service' --state=enabled --no-legend 2>/dev/null | \
while read -r unit _state; do
[ "$unit" = "matrix-synology-boot-fix.service" ] && continue
status="$({{ devture_systemd_docker_base_host_command_systemctl }} is-active "$unit" 2>/dev/null)"
if [ "$status" = "inactive" ] || [ "$status" = "failed" ]; then
echo "matrix-synology-boot-fix: starting $unit (was $status)"
{{ devture_systemd_docker_base_host_command_systemctl }} start "$unit"
fi
done
@@ -0,0 +1,16 @@
# SPDX-FileCopyrightText: 2026 Chiu Ki Sit
#
# SPDX-License-Identifier: AGPL-3.0-or-later
[Unit]
Description=Matrix Services Boot Recovery (Synology)
# Run after multi-user.target so all matrix services have been attempted first.
After=multi-user.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart={{ matrix_bin_path }}/matrix-synology-boot-fix
[Install]
WantedBy=multi-user.target