From ba7a75cfb044cbb70a3d572e149c9bc7d9d283dc Mon Sep 17 00:00:00 2001 From: George Tasioulis Date: Thu, 6 Aug 2026 02:19:24 +0300 Subject: [PATCH 1/3] Fix fresh-install failures in mirror playbooks Four issues that break the playbooks on a fresh AlmaLinux 10 server: 1. Disk-space check ran before the mirror directory was created, aborting the play on first run (df on a nonexistent path). Reordered directory creation before the check. 2. The SELinux relabel used a non-recursive restorecon, leaving subdirectories (including the ACME webroot) labeled var_t. Nginx then returned 403 for Let's Encrypt HTTP-01 challenges and no certificate could ever be issued. Now relabels recursively. 3. rsync run from the systemd service is confined to rsync_t, which cannot make outbound connections by default (AVC name_connect denial on port 873, rsync exit code 10). Every timer-triggered sync failed instantly, while manual/Ansible-invoked rsync worked because it runs unconfined - masking the bug until the timer fired. Set the rsync_client and rsync_anon_write booleans in the rsync-based playbooks. 4. The HTTPS vhost was deployed even when certbot failed (its failure is ignored), pointing nginx at nonexistent certificate files. nginx then failed to start at all, which also blocks any webroot retry - a chicken-and-egg state requiring manual recovery. The HTTPS config is now gated on the certificate actually existing, with a warning when it is skipped. Co-Authored-By: Claude Fable 5 --- ansible/combined-mirror/playbook.yml | 47 ++++++++++++++----- ansible/complete-swng-rsync/playbook.yml | 45 +++++++++++++----- .../playbook.yml | 45 +++++++++++++----- ansible/yum-reposync/playbook.yml | 40 +++++++++++----- 4 files changed, 127 insertions(+), 50 deletions(-) diff --git a/ansible/combined-mirror/playbook.yml b/ansible/combined-mirror/playbook.yml index 830c4fd..09e63ae 100644 --- a/ansible/combined-mirror/playbook.yml +++ b/ansible/combined-mirror/playbook.yml @@ -6,15 +6,6 @@ - defaults/main.yml tasks: - - name: Check available disk space - command: df -h {{ mirror_base_path }} - register: disk_space - changed_when: false - - - name: Display available disk space - debug: - msg: "{{ disk_space.stdout_lines }}" - - name: Create CloudLinux mirror directory file: path: "{{ cloudlinux_mirror_path }}" @@ -31,6 +22,15 @@ owner: root group: root + - name: Check available disk space + command: df -h {{ mirror_base_path }} + register: disk_space + changed_when: false + + - name: Display available disk space + debug: + msg: "{{ disk_space.stdout_lines }}" + - name: Install rsync if not present package: name: rsync @@ -198,8 +198,8 @@ ansible.builtin.shell: | semanage fcontext -a -t public_content_rw_t '/var/www(/.*)?' 2>/dev/null \ || semanage fcontext -m -t public_content_rw_t '/var/www(/.*)?' - restorecon -Fv /var/www {{ healthcheck_file }} {{ healthcheck_json_file }} 2>/dev/null \ - || restorecon -Fv /var/www + restorecon -RFv /var/www {{ healthcheck_file }} {{ healthcheck_json_file }} 2>/dev/null \ + || restorecon -RFv /var/www register: selinux_relabel changed_when: "'Relabeled' in selinux_relabel.stdout" when: @@ -207,6 +207,13 @@ - ansible_facts['selinux'] is defined - ansible_facts['selinux'].status | default('disabled') == 'enabled' + - name: Allow systemd-run rsync to act as a network client and write mirror data (SELinux enforcing) + ansible.builtin.command: setsebool -P rsync_client=1 rsync_anon_write=1 + when: + - ansible_facts['os_family'] == 'RedHat' + - ansible_facts['selinux'] is defined + - ansible_facts['selinux'].status | default('disabled') == 'enabled' + - name: Generate initial /healthcheck so it is available before first sync completes tags: healthcheck ansible.builtin.command: @@ -311,6 +318,12 @@ state: started when: certbot_enabled | bool and certbot_authenticator == 'standalone' + - name: Check whether the Let's Encrypt certificate exists + stat: + path: "/etc/letsencrypt/live/{{ mirror_domain }}/fullchain.pem" + register: letsencrypt_cert + when: certbot_enabled | bool + - name: Create Nginx HTTPS configuration for combined mirror tags: healthcheck template: @@ -318,7 +331,7 @@ dest: /etc/nginx/conf.d/combined-mirror-https.conf mode: '0644' notify: restart nginx - when: certbot_enabled | bool + when: certbot_enabled | bool and letsencrypt_cert.stat.exists - name: Update HTTP Nginx configuration to redirect to HTTPS tags: healthcheck @@ -327,7 +340,15 @@ dest: /etc/nginx/conf.d/combined-mirror.conf mode: '0644' notify: restart nginx - when: certbot_enabled | bool + when: certbot_enabled | bool and letsencrypt_cert.stat.exists + + - name: Warn when certificate could not be obtained (HTTPS config skipped) + debug: + msg: >- + Let's Encrypt certificate for {{ mirror_domain }} was not obtained. + The HTTPS vhost was NOT deployed so nginx keeps serving over HTTP. + Check /var/log/letsencrypt/letsencrypt.log and re-run the playbook. + when: certbot_enabled | bool and not letsencrypt_cert.stat.exists - name: Ensure certbot renew cron job exists cron: diff --git a/ansible/complete-swng-rsync/playbook.yml b/ansible/complete-swng-rsync/playbook.yml index 6042505..d1fc826 100644 --- a/ansible/complete-swng-rsync/playbook.yml +++ b/ansible/complete-swng-rsync/playbook.yml @@ -6,6 +6,14 @@ - defaults/main.yml tasks: + - name: Create mirror directory + file: + path: "{{ swng_mirror_path }}" + state: directory + mode: '0755' + owner: root + group: root + - name: Check available disk space command: df -h {{ mirror_base_path }} register: disk_space @@ -15,14 +23,6 @@ debug: msg: "{{ disk_space.stdout_lines }}" - - name: Create mirror directory - file: - path: "{{ swng_mirror_path }}" - state: directory - mode: '0755' - owner: root - group: root - - name: Install rsync if not present package: name: rsync @@ -129,8 +129,8 @@ ansible.builtin.shell: | semanage fcontext -a -t public_content_rw_t '/var/www(/.*)?' 2>/dev/null \ || semanage fcontext -m -t public_content_rw_t '/var/www(/.*)?' - restorecon -Fv /var/www {{ healthcheck_file }} {{ healthcheck_json_file }} 2>/dev/null \ - || restorecon -Fv /var/www + restorecon -RFv /var/www {{ healthcheck_file }} {{ healthcheck_json_file }} 2>/dev/null \ + || restorecon -RFv /var/www register: selinux_relabel changed_when: "'Relabeled' in selinux_relabel.stdout" when: @@ -138,6 +138,13 @@ - ansible_facts['selinux'] is defined - ansible_facts['selinux'].status | default('disabled') == 'enabled' + - name: Allow systemd-run rsync to act as a network client and write mirror data (SELinux enforcing) + ansible.builtin.command: setsebool -P rsync_client=1 rsync_anon_write=1 + when: + - ansible_facts['os_family'] == 'RedHat' + - ansible_facts['selinux'] is defined + - ansible_facts['selinux'].status | default('disabled') == 'enabled' + - name: Generate initial /healthcheck so it is available before first sync completes tags: healthcheck ansible.builtin.command: @@ -242,6 +249,12 @@ state: started when: certbot_enabled | bool and certbot_authenticator == 'standalone' + - name: Check whether the Let's Encrypt certificate exists + stat: + path: "/etc/letsencrypt/live/{{ mirror_domain }}/fullchain.pem" + register: letsencrypt_cert + when: certbot_enabled | bool + - name: Create Nginx HTTPS configuration for SWNG mirror tags: healthcheck template: @@ -249,7 +262,7 @@ dest: /etc/nginx/conf.d/swng-mirror-https.conf mode: '0644' notify: restart nginx - when: certbot_enabled | bool + when: certbot_enabled | bool and letsencrypt_cert.stat.exists - name: Update HTTP Nginx configuration to redirect to HTTPS tags: healthcheck @@ -258,7 +271,15 @@ dest: /etc/nginx/conf.d/swng-mirror.conf mode: '0644' notify: restart nginx - when: certbot_enabled | bool + when: certbot_enabled | bool and letsencrypt_cert.stat.exists + + - name: Warn when certificate could not be obtained (HTTPS config skipped) + debug: + msg: >- + Let's Encrypt certificate for {{ mirror_domain }} was not obtained. + The HTTPS vhost was NOT deployed so nginx keeps serving over HTTP. + Check /var/log/letsencrypt/letsencrypt.log and re-run the playbook. + when: certbot_enabled | bool and not letsencrypt_cert.stat.exists - name: Ensure certbot renew cron job exists cron: diff --git a/ansible/specific-version-rsync(Recomended)/playbook.yml b/ansible/specific-version-rsync(Recomended)/playbook.yml index 356201e..b8cd15a 100644 --- a/ansible/specific-version-rsync(Recomended)/playbook.yml +++ b/ansible/specific-version-rsync(Recomended)/playbook.yml @@ -13,6 +13,14 @@ fail_msg: "cloudlinux_version must be 10" success_msg: "CloudLinux version {{ cloudlinux_version }} is valid" + - name: Create mirror directory + file: + path: "{{ swng_mirror_path }}" + state: directory + mode: '0755' + owner: root + group: root + - name: Check available disk space command: df -h {{ mirror_base_path }} register: disk_space @@ -22,14 +30,6 @@ debug: msg: "{{ disk_space.stdout_lines }}" - - name: Create mirror directory - file: - path: "{{ swng_mirror_path }}" - state: directory - mode: '0755' - owner: root - group: root - - name: Install rsync if not present package: name: rsync @@ -133,8 +133,8 @@ ansible.builtin.shell: | semanage fcontext -a -t public_content_rw_t '/var/www(/.*)?' 2>/dev/null \ || semanage fcontext -m -t public_content_rw_t '/var/www(/.*)?' - restorecon -Fv /var/www {{ healthcheck_file }} {{ healthcheck_json_file }} 2>/dev/null \ - || restorecon -Fv /var/www + restorecon -RFv /var/www {{ healthcheck_file }} {{ healthcheck_json_file }} 2>/dev/null \ + || restorecon -RFv /var/www register: selinux_relabel changed_when: "'Relabeled' in selinux_relabel.stdout" when: @@ -142,6 +142,13 @@ - ansible_facts['selinux'] is defined - ansible_facts['selinux'].status | default('disabled') == 'enabled' + - name: Allow systemd-run rsync to act as a network client and write mirror data (SELinux enforcing) + ansible.builtin.command: setsebool -P rsync_client=1 rsync_anon_write=1 + when: + - ansible_facts['os_family'] == 'RedHat' + - ansible_facts['selinux'] is defined + - ansible_facts['selinux'].status | default('disabled') == 'enabled' + - name: Generate initial /healthcheck so it is available before first sync completes tags: healthcheck ansible.builtin.command: @@ -246,6 +253,12 @@ state: started when: certbot_enabled | bool and certbot_authenticator == 'standalone' + - name: Check whether the Let's Encrypt certificate exists + stat: + path: "/etc/letsencrypt/live/{{ mirror_domain }}/fullchain.pem" + register: letsencrypt_cert + when: certbot_enabled | bool + - name: Create Nginx HTTPS configuration for SWNG mirror tags: healthcheck template: @@ -253,7 +266,7 @@ dest: "/etc/nginx/conf.d/swng-mirror-https.conf" mode: '0644' notify: restart nginx - when: certbot_enabled | bool + when: certbot_enabled | bool and letsencrypt_cert.stat.exists - name: Update HTTP Nginx configuration to redirect to HTTPS tags: healthcheck @@ -262,7 +275,15 @@ dest: "/etc/nginx/conf.d/swng-mirror.conf" mode: '0644' notify: restart nginx - when: certbot_enabled | bool + when: certbot_enabled | bool and letsencrypt_cert.stat.exists + + - name: Warn when certificate could not be obtained (HTTPS config skipped) + debug: + msg: >- + Let's Encrypt certificate for {{ mirror_domain }} was not obtained. + The HTTPS vhost was NOT deployed so nginx keeps serving over HTTP. + Check /var/log/letsencrypt/letsencrypt.log and re-run the playbook. + when: certbot_enabled | bool and not letsencrypt_cert.stat.exists - name: Ensure certbot renew cron job exists cron: diff --git a/ansible/yum-reposync/playbook.yml b/ansible/yum-reposync/playbook.yml index 06cdf52..5673ee7 100644 --- a/ansible/yum-reposync/playbook.yml +++ b/ansible/yum-reposync/playbook.yml @@ -6,6 +6,15 @@ - defaults/main.yml tasks: + - name: Create mirror directory + file: + path: "{{ swng_mirror_path }}" + state: directory + mode: '0755' + owner: root + group: root + recurse: yes + - name: Check available disk space command: df -h {{ mirror_base_path }} register: disk_space @@ -29,15 +38,6 @@ - createrepo state: present - - name: Create mirror directory - file: - path: "{{ swng_mirror_path }}" - state: directory - mode: '0755' - owner: root - group: root - recurse: yes - - name: Import CloudLinux GPG key if not present rpm_key: state: present @@ -160,8 +160,8 @@ ansible.builtin.shell: | semanage fcontext -a -t public_content_rw_t '/var/www(/.*)?' 2>/dev/null \ || semanage fcontext -m -t public_content_rw_t '/var/www(/.*)?' - restorecon -Fv /var/www {{ healthcheck_file }} {{ healthcheck_json_file }} 2>/dev/null \ - || restorecon -Fv /var/www + restorecon -RFv /var/www {{ healthcheck_file }} {{ healthcheck_json_file }} 2>/dev/null \ + || restorecon -RFv /var/www register: selinux_relabel changed_when: "'Relabeled' in selinux_relabel.stdout" when: @@ -273,6 +273,12 @@ state: started when: certbot_enabled | bool and certbot_authenticator == 'standalone' + - name: Check whether the Let's Encrypt certificate exists + stat: + path: "/etc/letsencrypt/live/{{ mirror_domain }}/fullchain.pem" + register: letsencrypt_cert + when: certbot_enabled | bool + - name: Create Nginx HTTPS configuration for SWNG reposync mirror tags: healthcheck template: @@ -280,7 +286,7 @@ dest: /etc/nginx/conf.d/swng-reposync-mirror-https.conf mode: '0644' notify: restart nginx - when: certbot_enabled | bool + when: certbot_enabled | bool and letsencrypt_cert.stat.exists - name: Update HTTP Nginx configuration to redirect to HTTPS tags: healthcheck @@ -289,7 +295,15 @@ dest: /etc/nginx/conf.d/swng-reposync-mirror.conf mode: '0644' notify: restart nginx - when: certbot_enabled | bool + when: certbot_enabled | bool and letsencrypt_cert.stat.exists + + - name: Warn when certificate could not be obtained (HTTPS config skipped) + debug: + msg: >- + Let's Encrypt certificate for {{ mirror_domain }} was not obtained. + The HTTPS vhost was NOT deployed so nginx keeps serving over HTTP. + Check /var/log/letsencrypt/letsencrypt.log and re-run the playbook. + when: certbot_enabled | bool and not letsencrypt_cert.stat.exists - name: Ensure certbot renew cron job exists cron: From f6fbf32291ddd681ce1bcce35456f92a443a2f20 Mon Sep 17 00:00:00 2001 From: George Tasioulis Date: Thu, 6 Aug 2026 03:02:15 +0300 Subject: [PATCH 2/3] Sync mirror trees with local ownership so confined rsync can write With the rsync_client boolean set, timer-driven syncs connect but still fail to write: rsync -a preserves upstream ownership, so the tree ends up owned by non-root uids (e.g. 11122/11373) with mode 775 directories. The systemd-run rsync receiver is confined to rsync_t, which lacks the dac_override capability, so root cannot create files in directories it does not own: avc: denied { dac_override } for comm="rsync" capability=1 scontext=system_u:system_r:rsync_t:s0 tclass=capability permissive=0 The denial is dontaudited by default, so the audit log stays empty (visible only with semodule -DB) while every mkstemp fails EACCES and the sync grinds through the whole file list writing nothing. The rsync_full_access boolean does not help - on AlmaLinux 10 it grants file access rules but not the capability. Upstream UID/GID values have no meaning on a mirror that only serves files over HTTP, so stop preserving them instead of widening policy: - Add --no-owner --no-group to the initial sync commands and every systemd service template; transferred files are then owned by the receiving user (root) and confined rsync_t writes root-owned directories without any extra capability. - Normalize existing trees once (guarded by a find probe so the recursive chown only runs when non-root-owned files exist, e.g. on deployments synced with the previous owner-preserving flags). Verified on a real AlmaLinux 10.2 deployment: the confined service fails on the upstream-owned tree and writes normally after chown -R root:root. Co-Authored-By: Claude Fable 5 --- .../cloudlinux-complete-mirror.service.j2 | 2 +- .../cloudlinux-mirror.service.j2 | 2 +- ansible/combined-mirror/playbook.yml | 17 +++++++++++++++-- ansible/combined-mirror/swng-mirror.service.j2 | 2 +- ansible/complete-swng-rsync/playbook.yml | 11 ++++++++++- .../complete-swng-rsync/swng-mirror.service.j2 | 2 +- .../playbook.yml | 11 ++++++++++- .../swng-version-mirror.service.j2 | 2 +- 8 files changed, 40 insertions(+), 9 deletions(-) diff --git a/ansible/combined-mirror/cloudlinux-complete-mirror.service.j2 b/ansible/combined-mirror/cloudlinux-complete-mirror.service.j2 index 6c8f568..f58ce0a 100644 --- a/ansible/combined-mirror/cloudlinux-complete-mirror.service.j2 +++ b/ansible/combined-mirror/cloudlinux-complete-mirror.service.j2 @@ -6,7 +6,7 @@ Wants=network-online.target [Service] Type=oneshot SuccessExitStatus=24 -ExecStart=/bin/bash -c '/usr/bin/rsync -av --delete {{ cloudlinux_rsync_source }} {{ cloudlinux_mirror_path }}/; /usr/bin/rsync -av --delete {{ swng_rsync_source }} {{ swng_mirror_path }}/' +ExecStart=/bin/bash -c '/usr/bin/rsync -av --no-owner --no-group --delete {{ cloudlinux_rsync_source }} {{ cloudlinux_mirror_path }}/; /usr/bin/rsync -av --no-owner --no-group --delete {{ swng_rsync_source }} {{ swng_mirror_path }}/' ExecStartPost=/usr/bin/python3 /opt/healthcheck/healthcheck_update.py --service sync --field repo --value {{ healthcheck_source_name }} --status OK StandardOutput=append:{{ combined_sync_log }} StandardError=append:{{ combined_sync_log }} diff --git a/ansible/combined-mirror/cloudlinux-mirror.service.j2 b/ansible/combined-mirror/cloudlinux-mirror.service.j2 index 8b91b84..51cf44c 100644 --- a/ansible/combined-mirror/cloudlinux-mirror.service.j2 +++ b/ansible/combined-mirror/cloudlinux-mirror.service.j2 @@ -6,6 +6,6 @@ Wants=network-online.target [Service] Type=oneshot SuccessExitStatus=24 -ExecStart=/usr/bin/rsync -av --delete {{ cloudlinux_rsync_source }} {{ cloudlinux_mirror_path }}/ +ExecStart=/usr/bin/rsync -av --no-owner --no-group --delete {{ cloudlinux_rsync_source }} {{ cloudlinux_mirror_path }}/ StandardOutput=append:{{ cloudlinux_sync_log }} StandardError=append:{{ cloudlinux_sync_log }} diff --git a/ansible/combined-mirror/playbook.yml b/ansible/combined-mirror/playbook.yml index 09e63ae..fadd14b 100644 --- a/ansible/combined-mirror/playbook.yml +++ b/ansible/combined-mirror/playbook.yml @@ -36,9 +36,22 @@ name: rsync state: present + - name: Detect mirror files not owned by root (from earlier owner-preserving syncs) + command: find {{ item }} -not -user root -print -quit + loop: + - "{{ cloudlinux_mirror_path }}" + - "{{ swng_mirror_path }}" + register: mirror_nonroot + changed_when: false + + - name: Normalize mirror tree ownership to root (one-time migration) + command: chown -R root:root {{ item.item }} + loop: "{{ mirror_nonroot.results }}" + when: item.stdout | length > 0 + - name: Perform initial CloudLinux repository sync command: > - rsync -av --delete + rsync -av --no-owner --no-group --delete --progress --log-file={{ cloudlinux_sync_log }} {{ cloudlinux_rsync_source }} @@ -50,7 +63,7 @@ - name: Perform initial SWNG repository sync command: > - rsync -av --delete + rsync -av --no-owner --no-group --delete --progress --log-file={{ swng_sync_log }} {{ swng_rsync_source }} diff --git a/ansible/combined-mirror/swng-mirror.service.j2 b/ansible/combined-mirror/swng-mirror.service.j2 index 9ef30aa..c3d6cfb 100644 --- a/ansible/combined-mirror/swng-mirror.service.j2 +++ b/ansible/combined-mirror/swng-mirror.service.j2 @@ -6,7 +6,7 @@ Wants=network-online.target [Service] Type=oneshot SuccessExitStatus=24 -ExecStart=/usr/bin/rsync -av --delete {{ swng_rsync_source }} {{ swng_mirror_path }}/ +ExecStart=/usr/bin/rsync -av --no-owner --no-group --delete {{ swng_rsync_source }} {{ swng_mirror_path }}/ ExecStartPost=/usr/bin/python3 /opt/healthcheck/healthcheck_update.py --service sync --field repo --value {{ healthcheck_source_name }} --status OK StandardOutput=append:{{ swng_sync_log }} StandardError=append:{{ swng_sync_log }} diff --git a/ansible/complete-swng-rsync/playbook.yml b/ansible/complete-swng-rsync/playbook.yml index d1fc826..e473c95 100644 --- a/ansible/complete-swng-rsync/playbook.yml +++ b/ansible/complete-swng-rsync/playbook.yml @@ -28,9 +28,18 @@ name: rsync state: present + - name: Detect mirror files not owned by root (from earlier owner-preserving syncs) + command: find {{ swng_mirror_path }} -not -user root -print -quit + register: swng_nonroot + changed_when: false + + - name: Normalize mirror tree ownership to root (one-time migration) + command: chown -R root:root {{ swng_mirror_path }} + when: swng_nonroot.stdout | length > 0 + - name: Perform initial SWNG repository sync command: > - rsync -av --delete + rsync -av --no-owner --no-group --delete --progress --log-file={{ sync_log_file }} {{ rsync_source }} diff --git a/ansible/complete-swng-rsync/swng-mirror.service.j2 b/ansible/complete-swng-rsync/swng-mirror.service.j2 index cc85b42..20d1dc6 100644 --- a/ansible/complete-swng-rsync/swng-mirror.service.j2 +++ b/ansible/complete-swng-rsync/swng-mirror.service.j2 @@ -6,7 +6,7 @@ Wants=network-online.target [Service] Type=oneshot SuccessExitStatus=24 -ExecStart=/usr/bin/rsync -av --delete {{ rsync_source }} {{ swng_mirror_path }}/ +ExecStart=/usr/bin/rsync -av --no-owner --no-group --delete {{ rsync_source }} {{ swng_mirror_path }}/ ExecStartPost=/usr/bin/python3 /opt/healthcheck/healthcheck_update.py --service sync --field repo --value {{ healthcheck_source_name }} --status OK StandardOutput=append:{{ sync_log_file }} StandardError=append:{{ sync_log_file }} diff --git a/ansible/specific-version-rsync(Recomended)/playbook.yml b/ansible/specific-version-rsync(Recomended)/playbook.yml index b8cd15a..b279d37 100644 --- a/ansible/specific-version-rsync(Recomended)/playbook.yml +++ b/ansible/specific-version-rsync(Recomended)/playbook.yml @@ -35,9 +35,18 @@ name: rsync state: present + - name: Detect mirror files not owned by root (from earlier owner-preserving syncs) + command: find {{ swng_mirror_path }} -not -user root -print -quit + register: swng_nonroot + changed_when: false + + - name: Normalize mirror tree ownership to root (one-time migration) + command: chown -R root:root {{ swng_mirror_path }} + when: swng_nonroot.stdout | length > 0 + - name: Perform initial SWNG {{ cloudlinux_version }} repository sync command: > - rsync -av --delete + rsync -av --no-owner --no-group --delete --progress --log-file={{ sync_log_file }} {{ rsync_source }} diff --git a/ansible/specific-version-rsync(Recomended)/swng-version-mirror.service.j2 b/ansible/specific-version-rsync(Recomended)/swng-version-mirror.service.j2 index 01ca222..cf5170b 100644 --- a/ansible/specific-version-rsync(Recomended)/swng-version-mirror.service.j2 +++ b/ansible/specific-version-rsync(Recomended)/swng-version-mirror.service.j2 @@ -6,7 +6,7 @@ Wants=network-online.target [Service] Type=oneshot SuccessExitStatus=24 -ExecStart=/usr/bin/rsync -av --delete {{ rsync_source }} {{ swng_mirror_path }}/ +ExecStart=/usr/bin/rsync -av --no-owner --no-group --delete {{ rsync_source }} {{ swng_mirror_path }}/ ExecStartPost=/usr/bin/python3 /opt/healthcheck/healthcheck_update.py --service sync --field repo --value {{ healthcheck_source_name }} --status OK StandardOutput=append:{{ sync_log_file }} StandardError=append:{{ sync_log_file }} From 39f6b7f16162f044b34593c5421b143aaa4aa705 Mon Sep 17 00:00:00 2001 From: George Tasioulis Date: Thu, 6 Aug 2026 11:55:52 +0300 Subject: [PATCH 3/3] Drop Requires= from timers; make verify mountpoint check a warning Two smaller issues found while operating a deployed mirror: 1. Every timer unit declares Requires=.service. With Requires, an explicit systemctl stop of the sync service propagates and stops the timer as well, silently disabling all future scheduled syncs - the mirror then goes stale with nothing reporting a failure. (It also makes starting the timer trigger an immediate sync run.) Timer units need no dependency on the service they trigger; remove it. 2. verify.yml hard-fails when the mirror path is not a dedicated mountpoint, aborting before any of the useful checks run. A mirror on the root filesystem (e.g. a single-disk VM) is a valid setup - downgrade the check to a warning so verification can proceed. Co-Authored-By: Claude Fable 5 --- ansible/combined-mirror/cloudlinux-complete-mirror.timer.j2 | 1 - ansible/combined-mirror/cloudlinux-mirror.timer.j2 | 1 - ansible/combined-mirror/swng-mirror.timer.j2 | 1 - ansible/combined-mirror/verify.yml | 6 +++--- ansible/complete-swng-rsync/swng-mirror.timer.j2 | 1 - ansible/complete-swng-rsync/verify.yml | 6 +++--- .../swng-version-mirror.timer.j2 | 1 - ansible/specific-version-rsync(Recomended)/verify.yml | 6 +++--- ansible/yum-reposync/swng-reposync.timer.j2 | 1 - ansible/yum-reposync/verify.yml | 6 +++--- 10 files changed, 12 insertions(+), 18 deletions(-) diff --git a/ansible/combined-mirror/cloudlinux-complete-mirror.timer.j2 b/ansible/combined-mirror/cloudlinux-complete-mirror.timer.j2 index 26400f5..4d42d5e 100644 --- a/ansible/combined-mirror/cloudlinux-complete-mirror.timer.j2 +++ b/ansible/combined-mirror/cloudlinux-complete-mirror.timer.j2 @@ -1,6 +1,5 @@ [Unit] Description=Run Complete CloudLinux and SWNG Mirror Sync Every {{ sync_interval_hours }} Hours -Requires={{ service_name }}.service [Timer] OnCalendar={{ timer_schedule }} diff --git a/ansible/combined-mirror/cloudlinux-mirror.timer.j2 b/ansible/combined-mirror/cloudlinux-mirror.timer.j2 index 9f932ae..8810514 100644 --- a/ansible/combined-mirror/cloudlinux-mirror.timer.j2 +++ b/ansible/combined-mirror/cloudlinux-mirror.timer.j2 @@ -1,6 +1,5 @@ [Unit] Description=Run CloudLinux Mirror Sync Every {{ sync_interval_hours }} Hours -Requires=cloudlinux-mirror.service [Timer] OnCalendar={{ timer_schedule }} diff --git a/ansible/combined-mirror/swng-mirror.timer.j2 b/ansible/combined-mirror/swng-mirror.timer.j2 index 0441775..92f1584 100644 --- a/ansible/combined-mirror/swng-mirror.timer.j2 +++ b/ansible/combined-mirror/swng-mirror.timer.j2 @@ -1,6 +1,5 @@ [Unit] Description=Run SWNG Mirror Sync Every {{ sync_interval_hours }} Hours -Requires=swng-mirror.service [Timer] OnCalendar={{ timer_schedule }} diff --git a/ansible/combined-mirror/verify.yml b/ansible/combined-mirror/verify.yml index 3d47c7a..e82374e 100644 --- a/ansible/combined-mirror/verify.yml +++ b/ansible/combined-mirror/verify.yml @@ -17,9 +17,9 @@ msg: "OK: {{ mirror_base_path }} is mounted" when: mountpoint_check.rc == 0 - - name: Fail if storage not mounted - fail: - msg: "ERROR: {{ mirror_base_path }} is not mounted" + - name: Warn if storage is not a dedicated mountpoint + debug: + msg: "WARNING: {{ mirror_base_path }} is not a dedicated mountpoint - the mirror lives on the root filesystem" when: mountpoint_check.rc != 0 - name: Check CloudLinux mirror directory exists diff --git a/ansible/complete-swng-rsync/swng-mirror.timer.j2 b/ansible/complete-swng-rsync/swng-mirror.timer.j2 index 960f93c..652832c 100644 --- a/ansible/complete-swng-rsync/swng-mirror.timer.j2 +++ b/ansible/complete-swng-rsync/swng-mirror.timer.j2 @@ -1,6 +1,5 @@ [Unit] Description=Run Complete SWNG Mirror Sync Every {{ sync_interval_hours }} Hours -Requires=swng-mirror.service [Timer] OnCalendar={{ timer_schedule }} diff --git a/ansible/complete-swng-rsync/verify.yml b/ansible/complete-swng-rsync/verify.yml index 818a207..3eb13cc 100644 --- a/ansible/complete-swng-rsync/verify.yml +++ b/ansible/complete-swng-rsync/verify.yml @@ -17,9 +17,9 @@ msg: "OK: {{ mirror_base_path }} is mounted" when: mountpoint_check.rc == 0 - - name: Fail if storage not mounted - fail: - msg: "ERROR: {{ mirror_base_path }} is not mounted" + - name: Warn if storage is not a dedicated mountpoint + debug: + msg: "WARNING: {{ mirror_base_path }} is not a dedicated mountpoint - the mirror lives on the root filesystem" when: mountpoint_check.rc != 0 - name: Check mirror directory exists diff --git a/ansible/specific-version-rsync(Recomended)/swng-version-mirror.timer.j2 b/ansible/specific-version-rsync(Recomended)/swng-version-mirror.timer.j2 index d91b9bb..bcf0b22 100644 --- a/ansible/specific-version-rsync(Recomended)/swng-version-mirror.timer.j2 +++ b/ansible/specific-version-rsync(Recomended)/swng-version-mirror.timer.j2 @@ -1,6 +1,5 @@ [Unit] Description=Run CloudLinux {{ cloudlinux_version }} SWNG Mirror Sync Every {{ sync_interval_hours }} Hours -Requires={{ service_name }}.service [Timer] OnCalendar={{ timer_schedule }} diff --git a/ansible/specific-version-rsync(Recomended)/verify.yml b/ansible/specific-version-rsync(Recomended)/verify.yml index cd40d02..267d862 100644 --- a/ansible/specific-version-rsync(Recomended)/verify.yml +++ b/ansible/specific-version-rsync(Recomended)/verify.yml @@ -17,9 +17,9 @@ msg: "OK: {{ mirror_base_path }} is mounted" when: mountpoint_check.rc == 0 - - name: Fail if storage not mounted - fail: - msg: "ERROR: {{ mirror_base_path }} is not mounted" + - name: Warn if storage is not a dedicated mountpoint + debug: + msg: "WARNING: {{ mirror_base_path }} is not a dedicated mountpoint - the mirror lives on the root filesystem" when: mountpoint_check.rc != 0 - name: Check SWNG 10 directory exists diff --git a/ansible/yum-reposync/swng-reposync.timer.j2 b/ansible/yum-reposync/swng-reposync.timer.j2 index ea594ed..0f24916 100644 --- a/ansible/yum-reposync/swng-reposync.timer.j2 +++ b/ansible/yum-reposync/swng-reposync.timer.j2 @@ -1,6 +1,5 @@ [Unit] Description=Run SWNG reposync Every {{ sync_interval_hours }} Hours -Requires={{ service_name }}.service [Timer] OnCalendar={{ timer_schedule }} diff --git a/ansible/yum-reposync/verify.yml b/ansible/yum-reposync/verify.yml index 4328489..14bc36d 100644 --- a/ansible/yum-reposync/verify.yml +++ b/ansible/yum-reposync/verify.yml @@ -17,9 +17,9 @@ msg: "OK: {{ mirror_base_path }} is mounted" when: mountpoint_check.rc == 0 - - name: Fail if storage not mounted - fail: - msg: "ERROR: {{ mirror_base_path }} is not mounted" + - name: Warn if storage is not a dedicated mountpoint + debug: + msg: "WARNING: {{ mirror_base_path }} is not a dedicated mountpoint - the mirror lives on the root filesystem" when: mountpoint_check.rc != 0 - name: Check SWNG mirror directory exists