4
0
forked from jakub/ansible

105 Commits

Author SHA1 Message Date
jakub 40d7560e44 Merge pull request 'Manage own borgmatic systemd service and timer' (#1) from backup-systemd-units into main
Reviewed-on: jakub/ansible#1
2026-05-30 18:55:15 +00:00
jakub b84afb3abf Manage own borgmatic systemd service and timer
Ship borgmatic.service and borgmatic.timer from the backup role instead
of relying on the package-provided units. The units are deployed to
/etc/systemd/system (overriding the package units), with a configurable
schedule via role defaults.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 20:53:27 +02:00
jakub 65a02177fa Prioritize SSS over local accounts in nsswitch
Rewrites the passwd and group lines in /etc/nsswitch.conf so SSSD
is consulted before local files, and notifies the existing SSSD
restart handler so the change takes effect immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 18:02:29 +02:00
jakub 9eb3e446af Wire FreeIPA enrolment into setup_linux
Reachable via --tags sssd; reuses the existing freeipa_client role
via relative path from the umbrella playbook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 17:43:39 +02:00
jakub 52bb82f900 Wire backup into setup_linux and add portainer1-jim backup host
Imports backup.yml from setup_linux.yml so the backup play is
reachable from the umbrella playbook via --tags backup. Also adds
a backup_hosts entry for portainer1-jim.im.lab (5GB, /data/compose).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:07:29 +02:00
jakub f657767632 Tag baseline user play with 'users'
Lets the user play be targeted with --tags users while still running
by default when no tags are passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 15:45:41 +02:00
jakub 5bcdf66bb5 Update inv_linuxes 2026-05-23 13:18:25 +00:00
jakub 8f14ec2e69 Rename hellsos/jim to hellsoslocal/jimlocal
Carry forward the local-account rename from the direct edit on
initial_setup.yml into the new canonical users list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 14:55:48 +02:00
jakub 6f73b83bc0 Centralize users list in group_vars and rename baseline playbook
Move the canonical user list to group_vars/all/users.yml so both
setup_linux.yml (renamed from initial_setup.yml) and the
initial_install users role consume the same source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 14:55:38 +02:00
jakub b5cb064bd8 Update initial_setup.yml 2026-05-23 12:18:57 +00:00
jakub ef49608ccc Add hostname play and hellsos SSH keys to initial_setup
Adds an opt-in (tags: never,hostname) play that sets the system
hostname to inventory_hostname, and fills in both real authorized
keys for the hellsos user.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 14:13:23 +02:00
jakub a23492d9d1 Update inv_linuxes 2026-05-23 11:28:47 +00:00
jakub b7f4ba6502 Add dockhand role to initial_install
Tagged never,dockhand_install so it only runs when explicitly requested.
Installs docker.io + docker-compose-v2, templates a compose file for
fnsys/dockhand:latest at /docker/dockhand, and wires a oneshot systemd
unit that brings the stack up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 15:51:14 +02:00
jakub 54e111338d Encrypt borg repos with repokey-blake2 + shared passphrase
borg_passphrase is required (Semaphore secret, same across hosts).
The role writes it to /etc/borgmatic/passphrase (0600 root) and
configures borgmatic to use BORG_PASSCOMMAND=cat /etc/borgmatic/passphrase,
and runs `borg init --encryption=repokey-blake2` with BORG_PASSPHRASE in
the env. no_log on the tasks that touch the passphrase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:58:14 +02:00
jakub 885a617388 Fix borg SSH URI and auto-init the remote repo
The URI was wrong — BorgWarehouse uses a single shared SSH user
(`borgwarehouse`) and routes by the repo id in the path, so the form
is `ssh://<borgSshHost>/./<repo.id>` (not per-repo user with /./repos).

Role now also trusts the borg server's SSH host key in root's
known_hosts and runs `borg init --encryption=none` (idempotent — treats
"already exists" as success) so first backups don't need manual prep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:45:06 +02:00
jakub 0a97f00356 Auto-register borg repo on the controller per host
backup role now logs into borgcontroller and creates (or looks up) a
repository with alias=inventory_hostname, registering root's pubkey and
the requested storageSize. The resulting SSH URI is injected into the
borgmatic config so each host gets a remote-managed repo without manual
config.

backup_hosts entries gain a `storage_size_gb` field (stripped before
templating) and lose the manual `repositories` entry — the role fills it.
borgcontroller_{username,password} are expected from Semaphore secrets.

Also gitignores .claude/ local state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:37:10 +02:00
jakub 4275f2e8fe Enable borgmatic timer and generate root SSH key for borg server
The role now ensures the systemd timer is running (so backups actually
fire on the schedule borgmatic ships by default) and generates an
ed25519 key for root that can be authorized on the remote borg server.

Also adds a testipaclient entry to backup_hosts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:49:58 +02:00
jakub 0d027a2c73 Add .gitignore for venv and mikrotik backup output
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:41:48 +02:00
jakub e43c3aaae3 Add backup role and manage_ssh_keys role
- Borgmatic backup role driven by per-host config in group_vars/all/backup.yml
- manage_ssh_keys role with add/remove paths; remove_ssh_key_playbook.yml uses it

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:38:11 +02:00
jakub f540af580f Update inv_linuxes 2026-05-06 14:40:39 +00:00
jakub 34edead9b4 Update initial_install/roles/freeipa_client/tasks/main.yml 2026-04-24 20:44:46 +00:00
jakub 4987003c4e Update initial_install/roles/baseline_sudo/tasks/main.yml 2026-04-24 20:18:37 +00:00
jakub 22413beb25 Update initial_install/roles/users/tasks/main.yml 2026-04-24 20:16:31 +00:00
jakub 04ce966b20 Update initial_install/roles/users/tasks/main.yml 2026-04-24 20:12:19 +00:00
jakub 6368edcb67 Update initial_install/roles/users/tasks/main.yml 2026-04-24 20:09:02 +00:00
jakub 949940c730 Update initial_install/roles/freeipa_client/tasks/main.yml 2026-04-24 15:00:33 +00:00
jakub 9cc587cda5 Update initial_install/roles/users/tasks/main.yml 2026-04-24 14:53:48 +00:00
jakub 65dc887749 Update initial_install/roles/freeipa_client/tasks/main.yml 2026-04-24 14:51:35 +00:00
jakub e2015fe03e Update initial_install/roles/freeipa_client/tasks/main.yml 2026-04-24 14:48:14 +00:00
jakub ef3b293977 Update initial_install/roles/freeipa_client/tasks/main.yml 2026-04-24 14:45:40 +00:00
jakub 9d9695a7b3 Update initial_install/roles/freeipa_client/tasks/main.yml 2026-04-24 14:43:37 +00:00
jakub becc21ff9e Update initial_install/roles/freeipa_client/tasks/main.yml 2026-04-24 14:36:32 +00:00
jakub 22deb79b46 Update initial_install/roles/users/tasks/main.yml 2026-04-24 14:30:08 +00:00
jakub 9c2f0e577b Update initial_install/roles/baseline_sudo/tasks/main.yml 2026-04-24 12:20:39 +00:00
jakub 50b4bfa6fc Add initial_install/collections/requirements.yml 2026-04-24 12:09:57 +00:00
jakub 873af66079 Update inv_linuxes 2026-04-24 12:08:29 +00:00
jakub 504dc88756 Add initial_install/roles/freeipa_client/handlers/main.yml 2026-04-24 11:58:18 +00:00
jakub a2393f7f44 Update initial_install/roles/freeipa_client/tasks/main.yml 2026-04-24 11:57:32 +00:00
jakub 0583f5c85f Add initial_install/roles/freeipa_client/tasks/main.yml 2026-04-24 11:56:42 +00:00
jakub 7af44add30 Add initial_install/roles/ssh_hardening/handlers/main.yml 2026-04-24 11:56:22 +00:00
jakub ce17a4b00e Add initial_install/roles/ssh_hardening/tasks/main.yml 2026-04-24 11:56:01 +00:00
jakub cc679c5953 Add initial_install/roles/users/tasks/main.yml 2026-04-24 11:55:36 +00:00
jakub 26ae15cd46 Add initial_install/roles/baseline_sudo/tasks/main.yml 2026-04-24 11:54:41 +00:00
jakub e4cabaf2d5 Add initial_install/playbook.yml 2026-04-24 11:54:03 +00:00
jakub c22c566fa2 Update test_sms.yml 2026-04-13 16:50:08 +00:00
jakub 6de6fde4cd Update mikrotikbackup_clean.yml 2026-03-25 10:50:50 +00:00
jakub 5ba549a052 Update mikrotikbackup_clean.yml 2026-03-24 15:11:28 +00:00
jakub 699d7ef089 Update mikrotikbackup_clean.yml 2026-03-24 15:03:43 +00:00
jakub ce1ba9cf97 Update mikrotikbackup_clean.yml 2026-03-24 15:00:40 +00:00
jakub 724e954828 Update mikrotikbackup_clean.yml 2026-03-24 14:59:02 +00:00
jakub b0f5825d8a Update mikrotikbackup_clean.yml 2026-03-24 14:56:42 +00:00
jakub 2779970324 Update mikrotikbackup_clean.yml 2026-03-24 14:54:06 +00:00
jakub 93e8d5abb4 Update mikrotikbackup_clean.yml 2026-03-24 14:52:22 +00:00
jakub 7ab8b46e3e Update mikrotikbackup_clean.yml 2026-03-24 14:50:17 +00:00
jakub 482a298fdb Update mikrotikbackup_clean.yml 2026-03-24 14:46:32 +00:00
jakub 069579aa5a Update mikrotikbackup_clean.yml 2026-03-24 14:43:42 +00:00
jakub c8dbc4c518 Update mikrotikbackup_clean.yml 2026-03-24 14:41:22 +00:00
jakub 33a2d86910 Update mikrotikbackup_clean.yml 2026-03-24 14:39:19 +00:00
jakub 2fa716624e Update mikrotikbackup_clean.yml 2026-03-24 14:35:26 +00:00
jakub d65ac431e5 Update inv_linuxes 2026-03-23 17:11:03 +00:00
jakub 5d17ba7d6b Update initial_setup.yml 2026-03-23 17:09:47 +00:00
jakub daf198ea82 Update initial_setup.yml 2026-03-23 17:08:37 +00:00
jakub 3563c69d54 Update inv_linuxes 2026-03-23 16:25:22 +00:00
jakub b727d51dfd Update mikrotikbackup_clean.yml 2026-03-21 18:45:03 +00:00
jakub 75f2f20531 Update mikrotikbackup_clean.yml 2026-03-18 16:03:10 +00:00
jakub 8e5c1377eb Update mikrotikbackup_clean.yml 2026-03-18 15:49:20 +00:00
jakub 5ac5e82b16 Update mikrotikbackup_clean.yml 2026-03-18 15:26:08 +00:00
jakub b95bdf0b3a Update mikrotikbackup_clean.yml 2026-03-18 15:24:05 +00:00
jakub 3464fe007a Update mikrotikbackup_clean.yml 2026-03-18 15:20:54 +00:00
jakub 62d64b0411 Update update.yml 2026-03-11 16:35:49 +00:00
jakub 1bad80c04d Update update.yml 2026-03-11 16:33:58 +00:00
jakub f46ab32d7c Update update.yml 2026-03-11 16:12:30 +00:00
jakub bd775c5163 Update update.yml 2026-03-11 16:04:55 +00:00
jakub ad318c50fd Update update.yml 2026-03-11 16:02:07 +00:00
jakub fdc61bd22e Add initial_setup.yml 2026-02-20 18:17:06 +00:00
jakub 3238ad0a5e Update inv_linuxes 2026-02-20 18:15:08 +00:00
jakub b1a849824f Update users-ssh.yml 2026-02-20 14:10:11 +00:00
jakub 11a48e4ccb Update inv_mikrotiks 2026-02-14 00:01:21 +00:00
jakub e42363aaec Update mikrotikbackup_clean.yml 2026-02-13 23:45:02 +00:00
jakub 79ee0ecd46 Update inv_mikrotiks 2026-02-13 22:51:37 +00:00
jakub 8fd180ab11 Add requirements.yml 2026-02-13 22:47:42 +00:00
jakub 07bc4693e3 Update inv_mikrotiks 2026-02-13 22:37:37 +00:00
jakub 8ea60d9e15 Add mikrotikbackup_clean.yml 2026-02-13 22:36:29 +00:00
jakub 4eb25cb78b Update mikrotikbackup.yml 2026-02-13 14:38:51 +00:00
jakub 4de04d0d3a Add homarr.yml 2026-01-09 18:23:56 +00:00
jakub f4262bcb27 Update test_sms.yml 2025-10-14 12:49:40 +00:00
jakub 5c69d3a03f Update test_sms.yml 2025-10-14 12:35:33 +00:00
jakub 547c9fadc5 Update test_sms.yml 2025-10-14 11:57:23 +00:00
jakub c07181291c Update test_sms.yml 2025-10-14 11:55:48 +00:00
jakub 1a0ce36efe Update test_sms.yml 2025-10-13 12:39:59 +00:00
jakub 8b57f27ec6 Update test_sms.yml 2025-10-13 09:56:02 +00:00
jakub 085e7177f4 Update test_sms.yml 2025-10-13 09:47:28 +00:00
jakub 3099a0b2b8 Update test_sms.yml 2025-10-13 09:45:40 +00:00
jakub 3d89bc523e Update test_sms.yml 2025-10-13 09:43:52 +00:00
jakub 61d288f92a Update test_sms.yml 2025-10-12 19:00:20 +00:00
jakub 61beedd023 Update test_sms.yml 2025-10-12 18:59:20 +00:00
jakub bb37cdaa53 Update test_sms.yml 2025-10-12 18:58:25 +00:00
jakub b805b506b4 Update test_sms.yml 2025-10-12 18:56:33 +00:00
jakub 9fad4e4d1a Update inv_linuxes 2025-10-12 18:53:08 +00:00
jakub a632da2a62 Update test_sms.yml 2025-10-12 18:50:51 +00:00
jakub cf21ad70c1 Add test_sms.yml 2025-10-12 18:45:30 +00:00
jakub 1deb268d73 Update inv_mikrotiks 2025-09-20 10:26:36 +00:00
jakub 8373252ae9 Update update.yml 2025-09-20 10:12:50 +00:00
jakub 13a48cd734 Update inv_mikrotiks 2025-09-19 11:20:04 +00:00
jakub b497723769 Update mikrotikbackup.yml 2025-09-19 11:13:44 +00:00
51 changed files with 1161 additions and 2566 deletions
+10
View File
@@ -0,0 +1,10 @@
# Python virtual environments
.venv/
venv/
# MikroTik backup output
mikrotik/backups/
mikrotik/output/
# Claude Code local state
.claude/
+8
View File
@@ -0,0 +1,8 @@
---
- name: Install borgmatic and deploy per-host config
hosts: all
become: true
tags: never,backup
roles:
- role: backup
-183
View File
@@ -1,183 +0,0 @@
# check_raid.yml
- name: Check Linux MD RAID health on VM via Proxmox
hosts: linux_servers
gather_facts: false
become: true
become_user: root
become_method: sudo
vars:
# VM connection (provided by Semaphore env vars)
vm_ip: "{{ lookup('env', 'VM_IP') }}"
vm_user: "{{ lookup('env', 'VM_USER') }}"
vm_pass: "{{ lookup('env', 'VM_PASS') }}"
use_sudo: false
# Debug mode
DEBUG: "{{ lookup('env', 'DEBUG') | default(0) | int }}"
RETRIES: "{{ lookup('env', 'RETRIES') | default(25) | int }}"
# RAID specifics
# RAID_MD can be: md0 / md1 / ... OR "auto" to check all arrays found in /proc/mdstat
raid_md_device: "{{ lookup('env', 'RAID_MD') | default('md0', true) }}"
raid_allow_sync: "{{ lookup('env', 'RAID_ALLOW_SYNC') | default(1, true) | int }}"
raid_allow_no_array: "{{ lookup('env', 'RAID_ALLOW_NO_ARRAY') | default(0, true) | int }}"
raid_retries: "{{ RETRIES }}"
raid_delay: 2
ssh_hard_timeout: 30
# SSH options
ssh_opts:
- "-o" # English comments
- "StrictHostKeyChecking=no"
- "-o"
- "UserKnownHostsFile=/dev/null"
- "-o"
- "GlobalKnownHostsFile=/dev/null"
- "-o"
- "LogLevel=ERROR"
- "-o"
- "ConnectTimeout=15"
- "-o"
- "PreferredAuthentications=password"
- "-o"
- "PubkeyAuthentication=no"
- "-o"
- "KbdInteractiveAuthentication=no"
- "-o"
- "NumberOfPasswordPrompts=1"
raid_check_cmd: |
python3 - <<'PY'
# Print exactly one status line and exit with code:
# 0=OK, 1=FAIL (degraded/disallowed sync), 2=ERROR (unexpected/misconfig)
import re, sys
target = "{{ raid_md_device }}"
allow_sync = int("{{ raid_allow_sync }}")
allow_no_array = int("{{ raid_allow_no_array }}")
try:
txt = open("/proc/mdstat", "r", encoding="utf-8", errors="ignore").read()
except Exception as e:
print(f"ERROR RAID read_mdstat err={e}")
sys.exit(2)
arrays = {}
header_re = re.compile(r"^(md\d+)\s*:\s*active.*$", re.MULTILINE)
token_re = re.compile(r"^\s*\d+\s+blocks.*\[\d+/\d+\]\s*\[([U_]+)\]\s*$", re.MULTILINE)
for m in header_re.finditer(txt):
name = m.group(1)
chunk = txt[m.end():m.end() + 3000]
tm = token_re.search(chunk)
if tm:
arrays[name] = tm.group(1)
if not arrays:
if allow_no_array:
print("OK RAID none=no-md-arrays")
sys.exit(0)
print("ERROR RAID none=no-md-arrays")
sys.exit(2)
syncing = bool(re.search(r"\b(resync|recovery|reshape|check|repair)\b", txt))
if target == "auto":
to_check = sorted(arrays.keys())
else:
if target not in arrays:
found = ",".join(sorted(arrays.keys()))
print(f"ERROR RAID target_not_found target={target} found={found}")
sys.exit(2)
to_check = [target]
tokens_str = " ".join([f"{name}=[{arrays[name]}]" for name in to_check])
degraded = any("_" in arrays[name] for name in to_check)
if degraded:
print(f"FAIL RAID {tokens_str} syncing={int(syncing)}")
sys.exit(1)
if syncing and not allow_sync:
print(f"FAIL RAID {tokens_str} syncing={int(syncing)} allow_sync={allow_sync}")
sys.exit(1)
print(f"OK RAID {tokens_str} syncing={int(syncing)}")
sys.exit(0)
PY
tasks:
- name: Ensure sshpass is installed (for password-based SSH) # English comments
ansible.builtin.apt:
name: sshpass
state: present
update_cache: yes
run_once: true
- name: Run RAID check on VM (via SSH) # single command, no loop
ansible.builtin.command:
argv: >-
{{
['timeout', '-k', '5', (ssh_hard_timeout | string)]
+ ['sshpass', '-e', 'ssh']
+ ssh_opts
+ [ vm_user ~ '@' ~ vm_ip,
'bash', '-lc',
('sudo ' if use_sudo else '') + raid_check_cmd
]
}}
environment:
SSHPASS: "{{ vm_pass }}"
register: raid_cmd
changed_when: false
failed_when: false # we decide via assert below
retries: "{{ raid_retries }}"
delay: "{{ raid_delay }}"
until: raid_cmd.rc not in [124, 255]
run_once: true
- name: Build one-line summary (always)
ansible.builtin.set_fact:
raid_line: >-
{{
(raid_cmd.stdout | default('') | trim)
if ((raid_cmd.stdout | default('') | trim) | length) > 0
else ('ERROR RAID no-output rc=' ~ (raid_cmd.rc | string))
}}
changed_when: false
run_once: true
- name: RAID result (always one line)
ansible.builtin.assert:
that:
- raid_cmd.rc == 0
success_msg: "{{ raid_line }}"
fail_msg: "{{ raid_line }}"
run_once: true
# Optional verbose debug
- name: Debug | /proc/mdstat (VM)
ansible.builtin.command:
argv: >-
{{
['timeout', '-k', '5', (ssh_hard_timeout | string)]
+ ['sshpass', '-e', 'ssh']
+ ssh_opts
+ [ vm_user ~ '@' ~ vm_ip, 'bash', '-lc', "cat /proc/mdstat" ]
}}
environment:
SSHPASS: "{{ vm_pass }}"
register: mdstat_dbg
changed_when: false
failed_when: false
when: DEBUG == 1
run_once: true
- name: Debug | mdstat output
ansible.builtin.debug:
msg: "{{ mdstat_dbg.stdout | default('') }}"
when: DEBUG == 1
run_once: true
+25
View File
@@ -0,0 +1,25 @@
---
# Borg Controller — auto-creates a repo per host on a BorgWarehouse-backed server.
# borgcontroller_username / borgcontroller_password come from Semaphore secrets.
borgcontroller_url: https://borgcontroller.internet-master.cz
# Per-host borgmatic config. Hosts not listed here are skipped by the `backup` role.
# `storage_size_gb` is stripped before rendering and used to size the controller-side
# repo. `repositories` is auto-filled from the controller — don't set it manually.
# Other keys are passed through verbatim to borgmatic, see
# https://torsion.org/borgmatic/docs/reference/configuration/
backup_hosts:
testipaclient:
storage_size_gb: 10
source_directories:
- /home/jakub
keep_daily: 7
keep_weekly: 4
keep_monthly: 6
portainer1-jim.im.lab:
storage_size_gb: 5
source_directories:
- /data/compose
keep_daily: 7
keep_weekly: 4
keep_monthly: 6
+22
View File
@@ -0,0 +1,22 @@
---
# Canonical user list — consumed by both setup_linux.yml and
# initial_install/roles/users.
users:
- name: automation
shell: /bin/bash
sudo_nopasswd: true
ssh_keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEx+ltCKNIEM7F4PzGLv22cIu7N0Fpn5gxwV02xq0GS9 automation@internet-master.cz"
- name: hellsoslocal
shell: /bin/bash
sudo_nopasswd: true
ssh_keys:
- "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB+wC3T4/HSs1q5jf34sCqicSQOb05k+bxfmNMMKEGzRrGT3BfCG428F19OBIswvcuBPC0Q4TpPz84BkiATCx2o1JUH1xIOFcHzxxXbyzHAhjwto1wOr1DkwZWAvDPbdnJ39OsC0EdmrAHSXut93q4vzOsLlS34bOWP1THGY9nBKOHwJUQmS5tLw6dqbhKA886TrPXJDR9euEC+SYaytMyDUPYEa6dlDyRp77eII/uI/hf/6e+34wm9XyFyGMiMrQeO0u6Gq5NhsoBlhrCW3ds0To+DBZ/YKNzpzcN+uPKM1+r9nN8KwdwjRkQEwSdB4osz/UeGTiXB0jwb0+ftFthBFdOil86cd1OiAMmuKB/19QHv0NsVhs2JocP5JcrAgx8ktQzLIkOM4lt6Kt9rjHv1KNfdsZdiHqlOwrDv9B2Ei44qEUAsWlFSzEi7R3mOED4F04N3FeQ9TkrRRH6SE733t1Kum2VAz6wr4BSNyYxQOCnXoANy/JyoM5e5tQ7pht7tuxX78zhFlC7pAmVu0dnCQimSsIsXNlYGM7DQ7QMva8mKu49V3B7tU0ChghSRJUb2Sg0tkTAxzCR+WOOf8kAnkXNOV5vExQ3h5Xmb52A+az37Hyex379ryKqpffaf9RTx9pBagQf1XbUtA4KazuEi3fMmqCQjz+xFZJAZQ== hellsos@hellsos-PC"
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKhfQt1VNQo8EbIog4yjU5VEF3mTyMEC7o1Qe95X4JwG jan@rabcan.cz"
- name: jimlocal
shell: /bin/bash
sudo_nopasswd: true
ssh_keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPFS4fsqMjMMu/Bi/884bw7yJBqvWusDRESvanH6Owco jakub@jimbuntu"
+3 -4
View File
@@ -1,7 +1,6 @@
# update_homarr2.yml
- name: Update Homarr
hosts: pve2_vm
hosts: linux_servers
become: true
gather_facts: false
vars:
@@ -37,4 +36,4 @@
- name: Check Homarr HTTP endpoint
ansible.builtin.uri:
url: "http://127.0.0.1:{{ homarr_port }}/"
status_code: 200
status_code: 200
@@ -0,0 +1,2 @@
collections:
- name: freeipa.ansible_freeipa
+47
View File
@@ -0,0 +1,47 @@
---
- name: Baseline system setup
hosts: all
become: true
roles:
- role: baseline_sudo
tags: sudo
- role: users
tags: users
# ==============================
# FREEIPA / SSSD (optional)
# ==============================
- name: FreeIPA client setup
hosts: all
become: true
tags: never,sssd
roles:
- role: freeipa_client
# ==============================
# DOCKHAND (optional)
# ==============================
- name: Install dockhand
hosts: all
become: true
tags: never,dockhand_install
roles:
- role: dockhand
# ==============================
# SSH HARDENING (run last!)
# ==============================
- name: SSH hardening
hosts: all
become: true
tags: never,hardening
roles:
- role: ssh_hardening
@@ -0,0 +1,23 @@
---
- name: Ensure sudo package is installed
ansible.builtin.package:
name: sudo
state: present
- name: Ensure automation user has passwordless sudo
ansible.builtin.copy:
dest: /etc/sudoers.d/automation
owner: root
group: root
mode: '0440'
content: |
automation ALL=(ALL:ALL) NOPASSWD: ALL
validate: 'visudo -cf %s'
- name: Ensure sudo binary has correct permissions
ansible.builtin.file:
path: /usr/bin/sudo
owner: root
group: root
mode: '4755'
when: ansible_facts.os_family in ["Debian", "RedHat"]
@@ -0,0 +1,9 @@
---
- name: Reload systemd
ansible.builtin.systemd:
daemon_reload: true
- name: Restart dockhand
ansible.builtin.systemd:
name: dockhand
state: restarted
@@ -0,0 +1,46 @@
---
- name: Install Docker and Compose
ansible.builtin.package:
name:
- docker.io
- docker-compose-v2
state: present
- name: Ensure Docker is running
ansible.builtin.systemd:
name: docker
enabled: true
state: started
- name: Ensure /docker/dockhand exists
ansible.builtin.file:
path: /docker/dockhand
state: directory
owner: root
group: root
mode: '0755'
- name: Deploy dockhand docker-compose.yml
ansible.builtin.template:
src: docker-compose.yml.j2
dest: /docker/dockhand/docker-compose.yml
owner: root
group: root
mode: '0644'
notify: Restart dockhand
- name: Deploy dockhand systemd unit
ansible.builtin.template:
src: dockhand.service.j2
dest: /etc/systemd/system/dockhand.service
owner: root
group: root
mode: '0644'
notify: Reload systemd
- name: Enable and start dockhand
ansible.builtin.systemd:
name: dockhand
enabled: true
state: started
daemon_reload: true
@@ -0,0 +1,14 @@
# Managed by Ansible — do not edit by hand.
services:
dockhand:
image: fnsys/dockhand:latest
container_name: dockhand
restart: unless-stopped
ports:
- "3000:3000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- dockhand_data:/app/data
volumes:
dockhand_data:
@@ -0,0 +1,16 @@
[Unit]
Description=dockhand (docker compose stack)
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/docker/dockhand
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=300
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,5 @@
---
- name: Restart SSSD
ansible.builtin.service:
name: sssd
state: restarted
@@ -0,0 +1,67 @@
---
- name: Install FreeIPA client packages
ansible.builtin.package:
name:
- freeipa-client
- sssd
- sssd-tools
- oddjob
- oddjob-mkhomedir
state: present
- name: Set hostname FQDN
ansible.builtin.hostname:
name: "{{ inventory_hostname }}.im.lab"
- name: Check if FreeIPA client is already configured
ansible.builtin.stat:
path: /etc/ipa/default.conf
register: ipa_client_conf
- name: Enroll to FreeIPA
ansible.builtin.command:
argv:
- ipa-client-install
- --domain=im.lab
- --realm=IM.LAB
- --server=ipa.im.lab
- "--hostname={{ inventory_hostname }}.im.lab"
- --mkhomedir
- --principal=admin
- --password={{ ipa_admin_password }}
- --unattended
- --force-join
no_log: false
when: not ipa_client_conf.stat.exists
- name: Prioritize SSS over local accounts in NSS
ansible.builtin.lineinfile:
path: /etc/nsswitch.conf
regexp: '^{{ item }}:'
line: '{{ item }}: sss files systemd'
loop:
- passwd
- group
notify: Restart SSSD
- name: Enable mkhomedir
ansible.builtin.command:
argv:
- authselect
- enable-feature
- with-mkhomedir
register: authselect_mkhomedir
changed_when: "'already enabled' not in authselect_mkhomedir.stdout"
failed_when: false
- name: Enable and start oddjobd
ansible.builtin.service:
name: oddjobd
state: started
enabled: true
- name: Enable and start SSSD
ansible.builtin.service:
name: sssd
state: started
enabled: true
@@ -0,0 +1,8 @@
---
- name: Restart SSH
ansible.builtin.service:
name: "{{ 'sshd'
if ansible_facts.os_family in
['RedHat','Rocky','AlmaLinux','Fedora','OracleLinux','Suse']
else 'ssh' }}"
state: restarted
@@ -0,0 +1,29 @@
---
- name: Detect if system is Proxmox
ansible.builtin.stat:
path: /usr/bin/pveversion
register: proxmox_check
- name: Ensure sshd_config.d exists
ansible.builtin.file:
path: /etc/ssh/sshd_config.d
state: directory
- name: Deploy SSH hardening config
ansible.builtin.copy:
dest: /etc/ssh/sshd_config.d/99-ansible-hardening.conf
mode: '0644'
content: |
PasswordAuthentication no
ChallengeResponseAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
UsePAM yes
{% if not proxmox_check.stat.exists %}
PermitRootLogin no
{% else %}
PermitRootLogin prohibit-password
{% endif %}
validate: 'sshd -t -f %s'
notify: Restart SSH
@@ -0,0 +1,39 @@
---
# `users` comes from group_vars/all/users.yml
- name: Ensure users exist
ansible.builtin.user:
name: "{{ item.name }}"
shell: "{{ item.shell }}"
create_home: true
loop: "{{ users }}"
# --------------------------------------------------
# Configure passwordless sudo safely
# --------------------------------------------------
- name: Configure passwordless sudo
ansible.builtin.copy:
dest: "/etc/sudoers.d/{{ item.name }}"
mode: '0440'
owner: root
group: root
content: |
{{ item.name }} ALL=(ALL:ALL) NOPASSWD: ALL
validate: 'visudo -cf %s'
loop: "{{ users }}"
when: item.sudo_nopasswd | default(false)
# --------------------------------------------------
# Install SSH keys
# --------------------------------------------------
- name: Install authorized SSH keys
ansible.builtin.authorized_key:
user: "{{ item.name }}"
key: "{{ item.ssh_keys | join('\n') }}"
exclusive: true
loop: "{{ users }}"
# --------------------------------------------------
# Reset connection so sudo rules take effect immediately
# --------------------------------------------------
- name: Reset SSH connection
meta: reset_connection
+20 -3
View File
@@ -1,3 +1,20 @@
[linux_servers]
proxmox_nextcloud ansible_host=192.168.69.2
proxmox_services ansible_host=192.168.69.3
[linux_servers_jim]
jimbuntu ansible_host=192.168.19.4
portainernode2_jim ansible_host=192.168.19.8
galera3 ansible_host=192.168.19.92
galera2 ansible_host=192.168.19.91
testipaclient ansible_host=192.168.19.98
testclient ansible_host=192.168.19.115
portainer1-jim.im.lab ansible_host=192.168.19.7
[linux_servers_hellsos]
portainer2_hellsos ansible_host=192.168.52.9
portainernode_hellsos ansible_host=192.168.52.21
[linux_servers:children]
linux_servers_jim
linux_servers_hellsos
[local]
localhost ansible_connection=local
+16 -3
View File
@@ -1,3 +1,16 @@
[mikrotiks]
mikrotik_fencl_server ansible_host=192.168.69.1
mikrotik_fencl_5G ansible_host=192.168.68.1
[mikrotik_routers]
jim_main ansible_host=192.168.19.2
jim_gw2 ansible_host=192.168.19.3
hellsos ansible_host=192.168.40.1
ewolet ansible_host=192.168.90.1
Poli ansible_host=192.168.2.1
Schmid ansible_host=192.168.177.1
#Volf ansible_host=192.168.88.1
fencl_home ansible_host=192.168.68.1
fencl_tata ansible_host=192.168.69.1
[mikrotik_routers:vars]
ansible_connection=network_cli
ansible_network_os=community.routeros.routeros
ansible_command_timeout=15
-3
View File
@@ -1,3 +0,0 @@
[vm]
pve1_vm ansible_host=192.168.69.253
pve2_vm ansible_host=192.168.69.254
+2 -2
View File
@@ -33,7 +33,7 @@
current_date: "{{ date_output.stdout }}"
- name: Export router config
shell: timeout 15 ssh -o StrictHostKeyChecking=no {{ ansible_user }}@{{ ansible_host }} -p {{ ansible_port }} "/export"
shell: timeout 15 ssh -o StrictHostKeyChecking=no {{ ansible_user }}@{{ ansible_host }} -p {{ ansible_port }} "/export show-sensitive"
register: export_output
delegate_to: localhost
when: system_identity.rc == 0
@@ -59,4 +59,4 @@
- name: Remove backup file from router
shell: timeout 15 ssh -o StrictHostKeyChecking=no {{ ansible_user }}@{{ ansible_host }} -p {{ ansible_port }} "/file remove {{ router_name }}-{{ current_date }}-backup.backup"
delegate_to: localhost
when: system_identity.rc == 0
when: system_identity.rc == 0
+146
View File
@@ -0,0 +1,146 @@
- name: Backup and/or Upgrade MikroTik
hosts: mikrotik_routers
gather_facts: no
vars:
backup_dir: /opt/mikrotik_backups/
tasks:
# ----------------------------
# Always: identity + timestamp
# ----------------------------
- name: Get router identity
community.routeros.command:
commands: /system identity print
register: identity_raw
tags: always
- name: Parse router name
set_fact:
router_name: "{{ identity_raw.stdout[0].split(': ')[1] | trim }}"
tags: always
- name: Get timestamp
ansible.builtin.command: date +%Y-%m-%d_%H-%M-%S
register: date_out
delegate_to: localhost
tags: always
- name: Set timestamp fact
set_fact:
ts: "{{ date_out.stdout }}"
tags: always
# ----------------------------
# Backup (tag: backup)
# ----------------------------
- name: Ensure local backup directory exists
ansible.builtin.file:
path: "{{ backup_dir }}"
state: directory
mode: "0755"
delegate_to: localhost
tags: [backup, never]
- name: Export router config
community.routeros.command:
commands: /export terse show-sensitive
register: export_cfg
tags: [backup, never]
- name: Save export locally
ansible.builtin.copy:
content: "{{ export_cfg.stdout[0] }}"
dest: "{{ backup_dir }}/{{ router_name }}-{{ ts }}.rsc"
delegate_to: localhost
tags: [backup, never]
# ----------------------------
# Upgrade (tag: upgrade)
# ----------------------------
- name: Check current and latest available package versions
community.routeros.command:
commands: /system package update check-for-updates
register: update_check
tags: [upgrade, never]
- name: Normalize update output
set_fact:
_update_text: "{{ update_check.stdout[0] | replace('\r', '') }}"
tags: [upgrade, never]
# ⬇️ Add this to see exactly what RouterOS returns before parsing
- name: Debug raw update output
ansible.builtin.debug:
msg: "{{ _update_text }}"
tags: [upgrade, never]
- name: Parse installed and latest versions
set_fact:
installed_version: >-
{{
(_update_text | regex_findall('(?:installed|current)-version:[ ]*([0-9A-Za-z.]+)'))[0]
if (_update_text | regex_findall('(?:installed|current)-version:[ ]*([0-9A-Za-z.]+)'))
else 'unknown'
}}
latest_version: >-
{{
(_update_text | regex_findall('(?:latest|newest)-version:[ ]*([0-9A-Za-z.]+)'))[0]
if (_update_text | regex_findall('(?:latest|newest)-version:[ ]*([0-9A-Za-z.]+)'))
else 'unknown'
}}
tags: [upgrade, never]
- name: Fail if versions could not be parsed
ansible.builtin.fail:
msg: >
Could not parse versions from update output.
Raw text was: {{ _update_text }}
when: installed_version == 'unknown' or latest_version == 'unknown'
tags: [upgrade, never]
- name: Debug parsed versions
ansible.builtin.debug:
msg:
- "Installed: {{ installed_version }}"
- "Latest: {{ latest_version }}"
tags: [upgrade, never]
- name: Skip upgrade if already on latest
ansible.builtin.debug:
msg: "Router {{ router_name }} is already on latest version {{ installed_version }}. Skipping upgrade."
when: installed_version == latest_version
tags: [upgrade, never]
- name: Trigger package download and install
community.routeros.command:
commands: /system package update install
register: upgrade_result
when: installed_version != latest_version
tags: [upgrade, never]
- name: Wait for router to come back online after reboot
community.routeros.command:
commands: /system resource print
register: reboot_wait
until: reboot_wait is not failed
retries: 30
delay: 15
when:
- installed_version != latest_version
- upgrade_result is not failed
tags: [upgrade, never]
- name: Confirm upgraded version
community.routeros.command:
commands: /system resource print
register: post_upgrade_info
when: installed_version != latest_version
tags: [upgrade, never]
- name: Show post-upgrade RouterOS version
ansible.builtin.debug:
msg: "{{ post_upgrade_info.stdout[0] | regex_search('version: .+') }}"
when: installed_version != latest_version
tags: [upgrade, never]
-68
View File
@@ -1,68 +0,0 @@
# nextcloud/check_stack_nextcloud.yml
- name: Run Nextcloud maintenance on VM via Proxmox
hosts: linux_servers
gather_facts: false
become: true
become_user: root
become_method: sudo
vars:
vm_ip: "{{ lookup('env', 'VM_IP') }}"
vm_user: "{{ lookup('env', 'VM_USER') }}"
vm_pass: "{{ lookup('env', 'VM_PASS') }}"
# Flip to true if Docker needs sudo on the VM
use_sudo: false
vm_commands:
- "docker exec -u www-data nextcloud php -f /var/www/html/cron.php"
- "docker exec -u www-data nextcloud php occ app:update --all"
- "docker exec -u www-data nextcloud php occ maintenance:repair --include-expensive"
- "docker exec -u www-data nextcloud php occ status"
- "set -o pipefail; timeout 180s bash -x /data/compose/nextcloud/stack-health.sh </dev/null"
tasks:
- name: Ensure sshpass is installed (for password-based SSH) # English comments
ansible.builtin.apt:
name: sshpass
state: present
update_cache: yes
- name: Run Nextcloud commands on VM (via SSH, argv, no line breaks)
ansible.builtin.command:
argv:
- sshpass
- -p
- "{{ vm_pass }}"
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- "{{ ('sudo ' if use_sudo else '') + item }}"
loop: "{{ vm_commands }}"
register: vm_cmds
changed_when: false
- name: Show outputs for each command
ansible.builtin.debug:
msg: |
CMD: {{ item.item }}
RC: {{ item.rc }}
STDOUT:
{{ (item.stdout | default('')).strip() }}
STDERR:
{{ (item.stderr | default('')).strip() }}
loop: "{{ vm_cmds.results }}"
- name: Fail play if any command failed
ansible.builtin.assert:
that: "item.rc == 0"
fail_msg: "Command failed on VM: {{ item.item }} (rc={{ item.rc }})"
success_msg: "All commands succeeded."
loop: "{{ vm_cmds.results }}"
-174
View File
@@ -1,174 +0,0 @@
# nextcloud/update_collabora.yml
- name: Update Collabora CODE on VM via Proxmox
hosts: linux_servers
gather_facts: false
become: true
become_user: root
become_method: sudo
vars:
# --- Connection to VM (provided by Semaphore env vars) ---
vm_ip: "{{ lookup('env', 'VM_IP') }}"
vm_user: "{{ lookup('env', 'VM_USER') }}"
vm_pass: "{{ lookup('env', 'VM_PASS') }}"
use_sudo: false
# --- Debug mode (controlled via Semaphore variable) ---
DEBUG: "{{ lookup('env', 'DEBUG') | default(0) | int }}"
RETRIES: "{{ lookup('env', 'RETRIES') | default(25) | int }}"
# --- Collabora specifics ---
collabora_debug_caps: true
collabora_caps_url: "https://collabora.martinfencl.eu/hosting/capabilities"
# Use the FULL Nextcloud stack compose file; only target the 'collabora' service inside it
collabora_project: "nextcloud-collabora"
collabora_compose_file: "/data/compose/nextcloud/nextcloud-collabora.yml"
collabora_service: "collabora"
# Docker command prefix (consistent behavior and quiet hints)
docker_prefix: "unalias docker 2>/dev/null || true; DOCKER_CLI_HINTS=0; command docker"
# Commands to run on the target VM (quiet outputs)
collabora_commands:
- "{{ docker_prefix }} pull -q collabora/code:latest >/dev/null"
- "{{ docker_prefix }} compose -p {{ collabora_project }} -f {{ collabora_compose_file }} pull {{ collabora_service }} >/dev/null"
- "{{ docker_prefix }} compose -p {{ collabora_project }} -f {{ collabora_compose_file }} up -d --no-deps --force-recreate {{ collabora_service }} >/dev/null"
tasks:
- name: Ensure sshpass is installed (for password-based SSH) # English comments
ansible.builtin.apt:
name: sshpass
state: present
update_cache: yes
- name: Run Collabora update commands on VM (via SSH) # use SSHPASS env, hide item value
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- "{{ ('sudo ' if use_sudo else '') + item }}"
environment:
SSHPASS: "{{ vm_pass }}"
loop: "{{ collabora_commands }}"
loop_control:
index_var: idx # <-- capture loop index here
label: "cmd-{{ idx }}" # <-- use idx instead of loop.index
register: collab_cmds
changed_when: false
no_log: "{{ DEBUG == 0 }}"
- name: Show outputs for each Collabora command
ansible.builtin.debug:
msg: |
CMD: {{ item.item }}
RC: {{ item.rc }}
STDOUT:
{{ (item.stdout | default('')).strip() }}
STDERR:
{{ (item.stderr | default('')).strip() }}
loop: "{{ collab_cmds.results }}"
when: DEBUG == 1
- name: Fail play if any Collabora command failed # also hide item label
ansible.builtin.assert:
that: "item.rc == 0"
fail_msg: "Collabora update failed on VM: {{ item.item }} (rc={{ item.rc }})"
success_msg: "All Collabora update commands succeeded."
loop: "{{ collab_cmds.results }}"
loop_control:
index_var: idx
label: "cmd-{{ idx }}"
# -------------------------
# Readiness checks (controller first, then VM fallback)
# -------------------------
- name: Collabora | Wait for capabilities (controller first)
ansible.builtin.uri:
url: "{{ collabora_caps_url }}"
method: GET
return_content: true
validate_certs: true
status_code: 200
register: caps_controller
delegate_to: localhost
run_once: true
retries: "{{ RETRIES }}"
delay: 2
until: caps_controller.status == 200
failed_when: false
changed_when: false
- name: Collabora | VM-side fetch (pure JSON via Python) # use SSHPASS env here too
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- |
python3 - <<'PY'
import json, urllib.request, sys
try:
with urllib.request.urlopen("{{ collabora_caps_url }}", timeout=15) as r:
sys.stdout.write(r.read().decode())
except Exception:
pass
PY
environment:
SSHPASS: "{{ vm_pass }}"
register: caps_vm
changed_when: false
failed_when: false
when: caps_controller.status | default(0) != 200 or caps_controller.json is not defined
no_log: "{{ DEBUG == 0 }}"
- name: Collabora | Choose JSON (controller wins, else VM)
ansible.builtin.set_fact:
collab_caps_json: >-
{{
(caps_controller.json
if (caps_controller.status|default(0))==200 and (caps_controller.json is defined)
else (
(caps_vm.stdout | default('') | trim | length > 0)
| ternary((caps_vm.stdout | trim | from_json), omit)
)
)
}}
failed_when: false
- name: Collabora | Print concise summary
ansible.builtin.debug:
msg: >-
Collabora {{ collab_caps_json.productVersion | default('?') }}
({{ collab_caps_json.productName | default('?') }}),
convert-to.available={{ collab_caps_json['convert-to']['available'] | default('n/a') }},
serverId={{ collab_caps_json.serverId | default('n/a') }}
when: collab_caps_json is defined and DEBUG == 1
- name: Collabora | Capabilities unavailable (after retries)
ansible.builtin.debug:
msg: "Capabilities endpoint není dostupný ani po pokusech."
when: collab_caps_json is not defined and DEBUG == 1
# Optional full JSON (debug)
- name: Collabora | Full JSON (debug)
ansible.builtin.debug:
var: collab_caps_json
when: collabora_debug_caps and (collab_caps_json is defined) and DEBUG == 1
@@ -1,293 +0,0 @@
# nextcloud/update_nextcloud_db_redis.yml
- name: Update Nextcloud DB and Redis on VM via Proxmox
hosts: linux_servers
gather_facts: false
become: true
become_user: root
become_method: sudo
vars:
# --- Connection to VM (provided by Semaphore env vars) ---
vm_ip: "{{ lookup('env', 'VM_IP') }}"
vm_user: "{{ lookup('env', 'VM_USER') }}"
vm_pass: "{{ lookup('env', 'VM_PASS') }}"
use_sudo: false
# --- Debug / retries ---
DEBUG: "{{ lookup('env', 'DEBUG') | default(0) | int }}"
RETRIES: "{{ lookup('env', 'RETRIES') | default(25) | int }}"
# --- Nextcloud specifics ---
nextcloud_project: "nextcloud-collabora"
nextcloud_compose_file: "/data/compose/nextcloud/docker-compose-nextcloud.yml"
# Service names from docker-compose file
nextcloud_web_service: "nextcloud"
nextcloud_db_service: "nextclouddb"
redis_service: "redis"
# Backup directory on the VM (timestamped on controller)
backup_dir: "/data/compose/nextcloud/backup-db-redis-{{ lookup('pipe', 'date +%F-%H%M%S') }}"
nextcloud_base_url: "https://cloud.martinfencl.eu"
nextcloud_status_url: "{{ nextcloud_base_url }}/status.php"
# Docker command prefix (consistent behavior and quiet hints)
docker_prefix: "unalias docker 2>/dev/null || true; DOCKER_CLI_HINTS=0; command docker"
# --- Backup phase commands (run on VM) ---
# Same idea as in update_nextcloud.yml: maintenance on + config/custom_apps + DB dump
nextcloud_backup_commands:
- >
mkdir -p "{{ backup_dir }}"
- >
docker exec -u www-data nextcloud php occ maintenance:mode --on
- >
docker exec nextcloud sh -c 'tar czf /tmp/nextcloud_conf.tgz -C /var/www/html config custom_apps'
- >
docker cp nextcloud:/tmp/nextcloud_conf.tgz "{{ backup_dir }}/nextcloud_conf.tgz"
- >
docker exec nextcloud rm /tmp/nextcloud_conf.tgz || true
- >
docker exec nextcloud-db sh -c 'command -v mariadb-dump >/dev/null && mariadb-dump -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE" || mysqldump -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"' > "{{ backup_dir }}/db.sql"
# --- DB + Redis upgrade commands (run on VM) ---
db_redis_upgrade_commands:
# Update MariaDB service
- >
{{ docker_prefix }} compose -p {{ nextcloud_project }} -f {{ nextcloud_compose_file }} pull {{ nextcloud_db_service }}
- >
{{ docker_prefix }} compose -p {{ nextcloud_project }} -f {{ nextcloud_compose_file }} up -d --no-deps --force-recreate {{ nextcloud_db_service }}
# Simple DB health check (non-fatal)
- >
docker exec nextcloud-db sh -c 'mysqladmin ping -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"' || true
# Update Redis service
- >
{{ docker_prefix }} compose -p {{ nextcloud_project }} -f {{ nextcloud_compose_file }} pull {{ redis_service }}
- >
{{ docker_prefix }} compose -p {{ nextcloud_project }} -f {{ nextcloud_compose_file }} up -d --no-deps --force-recreate {{ redis_service }}
# Simple Redis health check (non-fatal)
- >
docker exec redis redis-cli PING || true
tasks:
- name: Ensure sshpass is installed (for password-based SSH)
ansible.builtin.apt:
name: sshpass
state: present
update_cache: yes
- name: Nextcloud | Show current version before DB/Redis upgrade (DEBUG)
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- 'docker exec -u www-data nextcloud php occ -V || true'
environment:
SSHPASS: "{{ vm_pass }}"
register: nc_version_before
changed_when: false
failed_when: false
when: DEBUG == 1
# -------------------------
# Backup phase
# -------------------------
- name: Nextcloud | Run backup commands on VM (via SSH)
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- "{{ ('sudo ' if use_sudo else '') + item }}"
environment:
SSHPASS: "{{ vm_pass }}"
loop: "{{ nextcloud_backup_commands }}"
loop_control:
index_var: idx
label: "backup-cmd-{{ idx }}"
register: nc_backup_cmds
changed_when: false
no_log: "{{ DEBUG == 0 }}"
- name: Nextcloud | Show outputs of backup commands (DEBUG)
ansible.builtin.debug:
msg: |
CMD: {{ item.item }}
RC: {{ item.rc }}
STDOUT:
{{ (item.stdout | default('')).strip() }}
STDERR:
{{ (item.stderr | default('')).strip() }}
loop: "{{ nc_backup_cmds.results }}"
when: DEBUG == 1
- name: Nextcloud | Fail play if any backup command failed
ansible.builtin.assert:
that: "item.rc == 0"
fail_msg: "Nextcloud DB/Redis backup step failed on VM: {{ item.item }} (rc={{ item.rc }})"
success_msg: "All Nextcloud DB/Redis backup commands succeeded."
loop: "{{ nc_backup_cmds.results }}"
loop_control:
index_var: idx
label: "backup-cmd-{{ idx }}"
# -------------------------
# DB + Redis upgrade phase
# -------------------------
- name: Nextcloud | Run DB/Redis upgrade commands on VM (via SSH)
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- "{{ ('sudo ' if use_sudo else '') + item }}"
environment:
SSHPASS: "{{ vm_pass }}"
loop: "{{ db_redis_upgrade_commands }}"
loop_control:
index_var: idx
label: "db-redis-cmd-{{ idx }}"
register: nc_db_redis_cmds
changed_when: false
failed_when: false
no_log: "{{ DEBUG == 0 }}"
- name: Nextcloud | Show outputs of DB/Redis upgrade commands (DEBUG)
ansible.builtin.debug:
msg: |
CMD: {{ item.item }}
RC: {{ item.rc }}
STDOUT:
{{ (item.stdout | default('')).strip() }}
STDERR:
{{ (item.stderr | default('')).strip() }}
loop: "{{ nc_db_redis_cmds.results }}"
when: DEBUG == 1
- name: Nextcloud | Fail play if any DB/Redis upgrade command failed
ansible.builtin.assert:
that: "item.rc == 0"
fail_msg: "Nextcloud DB/Redis upgrade step failed on VM: {{ item.item }} (rc={{ item.rc }})"
success_msg: "All Nextcloud DB/Redis upgrade commands succeeded."
loop: "{{ nc_db_redis_cmds.results }}"
loop_control:
index_var: idx
label: "db-redis-cmd-{{ idx }}"
# -------------------------
# Disable maintenance + readiness check
# -------------------------
- name: Nextcloud | Disable maintenance mode after DB/Redis upgrade
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- "{{ ('sudo ' if use_sudo else '') }}docker exec -u www-data nextcloud php occ maintenance:mode --off"
environment:
SSHPASS: "{{ vm_pass }}"
register: nc_maint_off
changed_when: false
no_log: "{{ DEBUG == 0 }}"
- name: Nextcloud | Wait for status.php (controller first)
ansible.builtin.uri:
url: "{{ nextcloud_status_url }}"
method: GET
return_content: true
validate_certs: true
status_code: 200
register: nc_status_controller
delegate_to: localhost
run_once: true
retries: "{{ RETRIES }}"
delay: 4
failed_when: false
changed_when: false
- name: Nextcloud | VM-side fetch status.php (JSON via Python)
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- |
python3 - <<'PY'
import json, urllib.request, sys
try:
with urllib.request.urlopen("{{ nextcloud_status_url }}", timeout=15) as r:
sys.stdout.write(r.read().decode())
except Exception:
pass
PY
environment:
SSHPASS: "{{ vm_pass }}"
register: nc_status_vm
changed_when: false
failed_when: false
when: nc_status_controller.status | default(0) != 200 or nc_status_controller.json is not defined
no_log: "{{ DEBUG == 0 }}"
- name: Nextcloud | Choose status JSON (controller wins, else VM)
ansible.builtin.set_fact:
nextcloud_status_json: >-
{{
(nc_status_controller.json
if (nc_status_controller.status | default(0)) == 200 and (nc_status_controller.json is defined)
else (
(nc_status_vm.stdout | default('') | trim | length > 0)
| ternary((nc_status_vm.stdout | trim | from_json), omit)
)
)
}}
failed_when: false
- name: Nextcloud | Print concise status summary (DEBUG)
ansible.builtin.debug:
msg: >-
Nextcloud {{ nextcloud_status_json.version | default('?') }}
(installed={{ nextcloud_status_json.installed | default('?') }},
maintenance={{ nextcloud_status_json.maintenance | default('?') }},
needsDbUpgrade={{ nextcloud_status_json.needsDbUpgrade | default('?') }})
when: nextcloud_status_json is defined and DEBUG == 1
- name: Nextcloud | Status JSON not available (DEBUG)
ansible.builtin.debug:
msg: "status.php is not reachable or did not return JSON."
when: nextcloud_status_json is not defined and DEBUG == 1
-287
View File
@@ -1,287 +0,0 @@
# nextcloud/update_nextcloud.yml
- name: Update Nextcloud on VM via Proxmox
hosts: proxmox_nextcloud # linux_servers
gather_facts: false
become: true
become_user: root
become_method: sudo
vars:
# --- Connection to VM (provided by Semaphore env vars) ---
vm_ip: "{{ lookup('env', 'VM_IP') }}"
vm_user: "{{ lookup('env', 'VM_USER') }}"
vm_pass: "{{ lookup('env', 'VM_PASS') }}"
use_sudo: false
# --- Debug / retries ---
DEBUG: "{{ lookup('env', 'DEBUG') | default(0) | int }}"
RETRIES: "{{ lookup('env', 'RETRIES') | default(25) | int }}"
# --- Nextcloud specifics ---
nextcloud_project: "nextcloud-collabora"
nextcloud_compose_file: "/data/compose/nextcloud/docker-compose-nextcloud.yml"
nextcloud_service: "nextcloud"
# Backup directory on the VM (timestamped on controller)
backup_dir: "/data/compose/nextcloud/backup-{{ lookup('pipe', 'date +%F-%H%M%S') }}"
nextcloud_base_url: "https://cloud.martinfencl.eu"
nextcloud_status_url: "{{ nextcloud_base_url }}/status.php"
# Docker command prefix (consistent behavior and quiet hints)
docker_prefix: "unalias docker 2>/dev/null || true; DOCKER_CLI_HINTS=0; command docker"
# --- Backup phase commands (run on VM) ---
nextcloud_backup_commands:
- >
mkdir -p "{{ backup_dir }}"
- >
docker exec -u www-data nextcloud php occ maintenance:mode --on
# Create tarball of config + custom_apps inside the container
- >
docker exec nextcloud sh -c 'tar czf /tmp/nextcloud_conf.tgz -C /var/www/html config custom_apps'
# Copy that tarball to the host backup directory
- >
docker cp nextcloud:/tmp/nextcloud_conf.tgz "{{ backup_dir }}/nextcloud_conf.tgz"
# Remove temporary file inside the container
- >
docker exec nextcloud rm /tmp/nextcloud_conf.tgz || true
# Database dump from DB container (unchanged)
- >
docker exec nextcloud-db sh -c 'command -v mariadb-dump >/dev/null && mariadb-dump -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE" || mysqldump -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"' > "{{ backup_dir }}/db.sql"
# --- Upgrade phase commands (run on VM) ---
nextcloud_upgrade_commands:
- >
{{ docker_prefix }} compose -p {{ nextcloud_project }} -f {{ nextcloud_compose_file }} pull {{ nextcloud_service }}
- >
{{ docker_prefix }} compose -p {{ nextcloud_project }} -f {{ nextcloud_compose_file }} up -d --no-deps --force-recreate {{ nextcloud_service }}
- >
docker exec -u www-data nextcloud php occ upgrade
- >
docker exec -u www-data nextcloud php occ app:update --all || true
- >
docker exec -u www-data nextcloud php occ maintenance:repair --include-expensive || true
tasks:
- name: Ensure sshpass is installed (for password-based SSH)
ansible.builtin.apt:
name: sshpass
state: present
update_cache: yes
- name: Nextcloud | Show current version before upgrade (DEBUG)
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- 'docker exec -u www-data nextcloud php occ -V || true'
environment:
SSHPASS: "{{ vm_pass }}"
register: nc_version_before
changed_when: false
failed_when: false
when: DEBUG == 1
# -------------------------
# Backup phase
# -------------------------
- name: Nextcloud | Run backup commands on VM (via SSH) # run plain commands via SSH
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- "{{ ('sudo ' if use_sudo else '') + item }}"
environment:
SSHPASS: "{{ vm_pass }}"
loop: "{{ nextcloud_backup_commands }}"
loop_control:
index_var: idx
label: "backup-cmd-{{ idx }}"
register: nc_backup_cmds
changed_when: false
no_log: "{{ DEBUG == 0 }}"
- name: Nextcloud | Show outputs of backup commands (DEBUG)
ansible.builtin.debug:
msg: |
CMD: {{ item.item }}
RC: {{ item.rc }}
STDOUT:
{{ (item.stdout | default('')).strip() }}
STDERR:
{{ (item.stderr | default('')).strip() }}
loop: "{{ nc_backup_cmds.results }}"
when: DEBUG == 1
- name: Nextcloud | Fail play if any backup command failed
ansible.builtin.assert:
that: "item.rc == 0"
fail_msg: "Nextcloud backup step failed on VM: {{ item.item }} (rc={{ item.rc }})"
success_msg: "All Nextcloud backup commands succeeded."
loop: "{{ nc_backup_cmds.results }}"
loop_control:
index_var: idx
label: "backup-cmd-{{ idx }}"
# -------------------------
# Upgrade phase
# -------------------------
- name: Nextcloud | Run upgrade commands on VM (via SSH)
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- "{{ ('sudo ' if use_sudo else '') + item }}"
environment:
SSHPASS: "{{ vm_pass }}"
loop: "{{ nextcloud_upgrade_commands }}"
loop_control:
index_var: idx
label: "upgrade-cmd-{{ idx }}"
register: nc_upgrade_cmds
changed_when: false
failed_when: false
no_log: "{{ DEBUG == 0 }}"
- name: Nextcloud | Show outputs of upgrade commands (DEBUG)
ansible.builtin.debug:
msg: |
CMD: {{ item.item }}
RC: {{ item.rc }}
STDOUT:
{{ (item.stdout | default('')).strip() }}
STDERR:
{{ (item.stderr | default('')).strip() }}
loop: "{{ nc_upgrade_cmds.results }}"
when: DEBUG == 1
- name: Nextcloud | Fail play if any upgrade command failed
ansible.builtin.assert:
that: "item.rc == 0"
fail_msg: "Nextcloud upgrade step failed on VM: {{ item.item }} (rc={{ item.rc }})"
success_msg: "All Nextcloud upgrade commands succeeded."
loop: "{{ nc_upgrade_cmds.results }}"
loop_control:
index_var: idx
label: "upgrade-cmd-{{ idx }}"
- name: Nextcloud | Disable maintenance mode (only after successful upgrade)
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- "{{ ('sudo ' if use_sudo else '') }}docker exec -u www-data nextcloud php occ maintenance:mode --off"
environment:
SSHPASS: "{{ vm_pass }}"
register: nc_maint_off
changed_when: false
no_log: "{{ DEBUG == 0 }}"
# -------------------------
# Readiness check (status.php)
# -------------------------
- name: Nextcloud | Wait for status.php (controller first)
ansible.builtin.uri:
url: "{{ nextcloud_status_url }}"
method: GET
return_content: true
validate_certs: true
status_code: 200
register: nc_status_controller
delegate_to: localhost
run_once: true
retries: "{{ RETRIES }}"
delay: 4
until: nc_status_controller.status == 200
failed_when: false
changed_when: false
- name: Nextcloud | VM-side fetch status.php (JSON via Python)
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- |
python3 - <<'PY'
import json, urllib.request, sys
try:
with urllib.request.urlopen("{{ nextcloud_status_url }}", timeout=15) as r:
sys.stdout.write(r.read().decode())
except Exception:
pass
PY
environment:
SSHPASS: "{{ vm_pass }}"
register: nc_status_vm
changed_when: false
failed_when: false
when: nc_status_controller.status | default(0) != 200 or nc_status_controller.json is not defined
no_log: "{{ DEBUG == 0 }}"
- name: Nextcloud | Choose status JSON (controller wins, else VM)
ansible.builtin.set_fact:
nextcloud_status_json: >-
{{
(nc_status_controller.json
if (nc_status_controller.status | default(0)) == 200 and (nc_status_controller.json is defined)
else (
(nc_status_vm.stdout | default('') | trim | length > 0)
| ternary((nc_status_vm.stdout | trim | from_json), omit)
)
)
}}
failed_when: false
- name: Nextcloud | Print concise status summary (DEBUG)
ansible.builtin.debug:
msg: >-
Nextcloud {{ nextcloud_status_json.version | default('?') }}
(installed={{ nextcloud_status_json.installed | default('?') }},
maintenance={{ nextcloud_status_json.maintenance | default('?') }},
needsDbUpgrade={{ nextcloud_status_json.needsDbUpgrade | default('?') }})
when: nextcloud_status_json is defined and DEBUG == 1
- name: Nextcloud | Status JSON not available (DEBUG)
ansible.builtin.debug:
msg: "status.php is not reachable or did not return JSON."
when: nextcloud_status_json is not defined and DEBUG == 1
-113
View File
@@ -1,113 +0,0 @@
- name: Update Nextcloud (Ansible-native)
hosts: proxmox_nextcloud
become: true
gather_facts: false
vars:
DEBUG: "{{ lookup('env', 'DEBUG') | default(0) | int }}"
RETRIES: "{{ lookup('env', 'RETRIES') | default(25) | int }}"
nextcloud_project: "nextcloud-collabora"
compose_file: "/data/compose/nextcloud/docker-compose-nextcloud.yml"
backup_dir: "/data/compose/nextcloud/backup-{{ ansible_date_time.iso8601_basic_short }}"
nextcloud_base_url: "https://cloud.martinfencl.eu"
nextcloud_status_url: "{{ nextcloud_base_url }}/status.php"
tasks:
# -------------------------
# Pre-check
# -------------------------
- name: Show current Nextcloud version (DEBUG)
command: docker exec -u www-data nextcloud php occ -V
register: nc_version
changed_when: false
failed_when: false
when: DEBUG == 1
- debug:
var: nc_version.stdout
when: DEBUG == 1
# -------------------------
# Backup
# -------------------------
- name: Enable maintenance mode
command: docker exec -u www-data nextcloud php occ maintenance:mode --on
- name: Create backup directory
file:
path: "{{ backup_dir }}"
state: directory
- name: Backup config and custom_apps
command: >
docker exec nextcloud
tar czf /tmp/nextcloud_conf.tgz -C /var/www/html config custom_apps
- name: Copy config backup out of container
command: docker cp nextcloud:/tmp/nextcloud_conf.tgz {{ backup_dir }}/
- name: Remove temp archive from container
command: docker exec nextcloud rm -f /tmp/nextcloud_conf.tgz
- name: Backup database
shell: >
docker exec nextcloud-db sh -c
'command -v mariadb-dump >/dev/null &&
mariadb-dump -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE" ||
mysqldump -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"'
register: db_dump
- name: Save database dump
copy:
content: "{{ db_dump.stdout }}"
dest: "{{ backup_dir }}/db.sql"
# -------------------------
# Upgrade
# -------------------------
- name: Pull new Nextcloud image
command: docker compose -p {{ nextcloud_project }} -f {{ compose_file }} pull nextcloud
- name: Recreate Nextcloud container
command: docker compose -p {{ nextcloud_project }} -f {{ compose_file }}
up -d --no-deps --force-recreate nextcloud
- name: Run Nextcloud upgrade
command: docker exec -u www-data nextcloud php occ upgrade
- name: Update apps
command: docker exec -u www-data nextcloud php occ app:update --all
failed_when: false
- name: Run maintenance repair
command: docker exec -u www-data nextcloud php occ maintenance:repair --include-expensive
failed_when: false
- name: Disable maintenance mode
command: docker exec -u www-data nextcloud php occ maintenance:mode --off
# -------------------------
# Readiness check
# -------------------------
- name: Wait for status.php
uri:
url: "{{ nextcloud_status_url }}"
status_code: 200
return_content: true
validate_certs: true
register: nc_status
retries: "{{ RETRIES }}"
delay: 4
until: nc_status.status == 200
changed_when: false
- name: Print status summary (DEBUG)
debug:
msg: >
Nextcloud {{ nc_status.json.version }}
(installed={{ nc_status.json.installed }},
maintenance={{ nc_status.json.maintenance }},
needsDbUpgrade={{ nc_status.json.needsDbUpgrade }})
when: DEBUG == 1
-343
View File
@@ -1,343 +0,0 @@
# nextcloud/update_nextcloud.yml
- name: Update Nextcloud on VM via Proxmox
hosts: proxmox_nextcloud
gather_facts: false
become: true
become_user: root
become_method: sudo
vars:
# --- Connection to VM (provided by Semaphore env vars) ---
vm_ip: "{{ lookup('env', 'VM_IP') }}"
vm_user: "{{ lookup('env', 'VM_USER') }}"
vm_pass: "{{ lookup('env', 'VM_PASS') }}"
use_sudo: false
# --- Debug / retries ---
DEBUG: "{{ lookup('env', 'DEBUG') | default(0) | int }}"
RETRIES: "{{ lookup('env', 'RETRIES') | default(25) | int }}"
# --- Nextcloud specifics ---
nextcloud_project: "nextcloud-collabora"
nextcloud_compose_file: "/data/compose/nextcloud/docker-compose-nextcloud.yml"
nextcloud_service: "nextcloud"
# Backup directory on the VM (timestamped on controller)
backup_dir: "/data/compose/nextcloud/backup-{{ lookup('pipe', 'date +%F-%H%M%S') }}"
nextcloud_base_url: "https://cloud.martinfencl.eu"
nextcloud_status_url: "{{ nextcloud_base_url }}/status.php"
# Docker command prefix (consistent behavior and quiet hints)
docker_prefix: "unalias docker 2>/dev/null || true; DOCKER_CLI_HINTS=0; command docker"
# --- Backup phase commands (run on VM) ---
nextcloud_backup_commands:
- >-
mkdir -p "{{ backup_dir }}"
- >-
{{ docker_prefix }} exec -u www-data nextcloud php occ maintenance:mode --on
- >-
{{ docker_prefix }} exec nextcloud sh -c 'tar czf /tmp/nextcloud_conf.tgz -C /var/www/html config custom_apps'
- >-
{{ docker_prefix }} cp nextcloud:/tmp/nextcloud_conf.tgz "{{ backup_dir }}/nextcloud_conf.tgz"
- >-
{{ docker_prefix }} exec nextcloud rm /tmp/nextcloud_conf.tgz || true
- >-
{{ docker_prefix }} exec nextcloud-db sh -c 'command -v mariadb-dump >/dev/null && mariadb-dump -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE" || mysqldump -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"' > "{{ backup_dir }}/db.sql"
tasks:
- name: Ensure sshpass is installed (for password-based SSH)
ansible.builtin.apt:
name: sshpass
state: present
update_cache: yes
- name: Nextcloud | Show current version before upgrade (DEBUG)
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- "{{ docker_prefix }} exec -u www-data nextcloud php occ -V || true"
environment:
SSHPASS: "{{ vm_pass }}"
register: nc_version_before
changed_when: false
failed_when: false
when: DEBUG == 1
no_log: "{{ DEBUG == 0 }}"
# -------------------------
# Backup phase
# -------------------------
- name: Nextcloud | Run backup commands on VM (via SSH)
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- "{{ ('sudo ' if use_sudo else '') + item }}"
environment:
SSHPASS: "{{ vm_pass }}"
loop: "{{ nextcloud_backup_commands }}"
loop_control:
index_var: idx
label: "backup-cmd-{{ idx }}"
register: nc_backup_cmds
changed_when: false
failed_when: false
no_log: "{{ DEBUG == 0 }}"
- name: Nextcloud | Show outputs of backup commands (DEBUG)
ansible.builtin.debug:
msg: |
CMD: {{ item.item }}
RC: {{ item.rc }}
STDOUT:
{{ (item.stdout | default('')).strip() }}
STDERR:
{{ (item.stderr | default('')).strip() }}
loop: "{{ nc_backup_cmds.results }}"
when: DEBUG == 1
- name: Nextcloud | Fail play if any backup command failed
ansible.builtin.assert:
that: "item.rc == 0"
fail_msg: "Nextcloud backup step failed on VM: {{ item.item }} (rc={{ item.rc }})"
success_msg: "All Nextcloud backup commands succeeded."
loop: "{{ nc_backup_cmds.results }}"
loop_control:
index_var: idx
label: "backup-cmd-{{ idx }}"
# -------------------------
# Upgrade phase (with always cleanup)
# -------------------------
- name: Nextcloud | Upgrade block
block:
- name: Nextcloud | Pull image
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- "{{ ('sudo ' if use_sudo else '') }}{{ docker_prefix }} compose -p {{ nextcloud_project }} -f {{ nextcloud_compose_file }} pull {{ nextcloud_service }}"
environment:
SSHPASS: "{{ vm_pass }}"
register: nc_pull
changed_when: false
no_log: "{{ DEBUG == 0 }}"
- name: Nextcloud | Recreate service
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- "{{ ('sudo ' if use_sudo else '') }}{{ docker_prefix }} compose -p {{ nextcloud_project }} -f {{ nextcloud_compose_file }} up -d --no-deps --force-recreate {{ nextcloud_service }}"
environment:
SSHPASS: "{{ vm_pass }}"
register: nc_up
changed_when: false
no_log: "{{ DEBUG == 0 }}"
- name: Nextcloud | Ensure maintenance is OFF before occ upgrade
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- "{{ ('sudo ' if use_sudo else '') }}{{ docker_prefix }} exec -u www-data nextcloud php occ maintenance:mode --off || true"
environment:
SSHPASS: "{{ vm_pass }}"
register: nc_maint_off_before
changed_when: false
failed_when: false
no_log: "{{ DEBUG == 0 }}"
- name: Nextcloud | occ upgrade (must succeed)
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- "{{ ('sudo ' if use_sudo else '') }}{{ docker_prefix }} exec -u www-data nextcloud php occ upgrade"
environment:
SSHPASS: "{{ vm_pass }}"
register: nc_occ_upgrade
changed_when: false
no_log: "{{ DEBUG == 0 }}"
- name: Nextcloud | Update apps (best-effort)
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- "{{ ('sudo ' if use_sudo else '') }}{{ docker_prefix }} exec -u www-data nextcloud php occ app:update --all || true"
environment:
SSHPASS: "{{ vm_pass }}"
register: nc_app_update
changed_when: false
failed_when: false
no_log: "{{ DEBUG == 0 }}"
- name: Nextcloud | Repair (best-effort)
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- "{{ ('sudo ' if use_sudo else '') }}{{ docker_prefix }} exec -u www-data nextcloud php occ maintenance:repair --include-expensive || true"
environment:
SSHPASS: "{{ vm_pass }}"
register: nc_repair
changed_when: false
failed_when: false
no_log: "{{ DEBUG == 0 }}"
rescue:
- name: Nextcloud | Show occ upgrade output (DEBUG)
ansible.builtin.debug:
msg: |
occ upgrade FAILED
RC: {{ nc_occ_upgrade.rc | default('n/a') }}
STDOUT:
{{ (nc_occ_upgrade.stdout | default('')).strip() }}
STDERR:
{{ (nc_occ_upgrade.stderr | default('')).strip() }}
when: DEBUG == 1
- name: Nextcloud | Try to force-disable maintenance flag (best-effort)
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- "{{ ('sudo ' if use_sudo else '') }}{{ docker_prefix }} exec -u www-data nextcloud php occ config:system:set maintenance --type=boolean --value=false || true"
environment:
SSHPASS: "{{ vm_pass }}"
changed_when: false
failed_when: false
no_log: "{{ DEBUG == 0 }}"
- name: Nextcloud | Fail explicitly
ansible.builtin.fail:
msg: >-
Nextcloud occ upgrade failed. Check nextcloud.log inside the container (data/nextcloud.log).
stdout={{ (nc_occ_upgrade.stdout | default('') | trim) }}
stderr={{ (nc_occ_upgrade.stderr | default('') | trim) }}
always:
- name: Nextcloud | Ensure maintenance mode is OFF (always)
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- "{{ ('sudo ' if use_sudo else '') }}{{ docker_prefix }} exec -u www-data nextcloud php occ maintenance:mode --off || true"
environment:
SSHPASS: "{{ vm_pass }}"
changed_when: false
failed_when: false
no_log: "{{ DEBUG == 0 }}"
# -------------------------
# Readiness check (status.php)
# -------------------------
- name: Nextcloud | Wait for status.php (controller first)
ansible.builtin.uri:
url: "{{ nextcloud_status_url }}"
method: GET
return_content: true
validate_certs: true
status_code: 200
register: nc_status_controller
delegate_to: localhost
run_once: true
retries: "{{ RETRIES }}"
delay: 4
until: nc_status_controller.status == 200
failed_when: false
changed_when: false
- name: Nextcloud | Print concise status summary (DEBUG)
ansible.builtin.debug:
msg: >-
Nextcloud {{ nc_status_controller.json.version | default('?') }}
(installed={{ nc_status_controller.json.installed | default('?') }},
maintenance={{ nc_status_controller.json.maintenance | default('?') }},
needsDbUpgrade={{ nc_status_controller.json.needsDbUpgrade | default('?') }})
when: DEBUG == 1 and nc_status_controller.json is defined
-118
View File
@@ -1,118 +0,0 @@
# update_portainer_agent.yml
- name: Update Portainer Agent on VM via Proxmox
hosts: linux_servers
gather_facts: false
become: true
become_user: root
become_method: sudo
vars:
# --- Connection to VM (provided by Semaphore env vars) ---
vm_ip: "{{ lookup('env', 'VM_IP') }}"
vm_user: "{{ lookup('env', 'VM_USER') }}"
vm_pass: "{{ lookup('env', 'VM_PASS') }}"
use_sudo: false
# --- Debug mode (controlled via Semaphore variable) ---
DEBUG: "{{ lookup('env', 'DEBUG') | default(0) | int }}"
RETRIES: "{{ lookup('env', 'RETRIES') | default(25) | int }}"
# --- Portainer Agent specifics ---
portainer_agent_image: "portainer/agent:latest"
portainer_agent_container: "portainer_agent"
portainer_agent_port: 9001
# Docker command prefix (consistent behavior and quiet hints)
docker_prefix: "unalias docker 2>/dev/null || true; DOCKER_CLI_HINTS=0; command docker"
# Commands to run on the target VM (quiet outputs)
portainer_commands:
- "{{ docker_prefix }} pull -q {{ portainer_agent_image }} >/dev/null"
- "{{ docker_prefix }} stop {{ portainer_agent_container }} >/dev/null 2>&1 || true"
- "{{ docker_prefix }} rm {{ portainer_agent_container }} >/dev/null 2>&1 || true"
- >
{{ docker_prefix }} run -d
--name {{ portainer_agent_container }}
--restart=always
-p {{ portainer_agent_port }}:9001
-v /var/run/docker.sock:/var/run/docker.sock
-v /var/lib/docker/volumes:/var/lib/docker/volumes
{{ portainer_agent_image }} >/dev/null
tasks:
- name: Ensure sshpass is installed (for password-based SSH) # English comments
ansible.builtin.apt:
name: sshpass
state: present
update_cache: yes
- name: Run Portainer Agent update commands on VM (via SSH) # run all commands via sshpass
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- "{{ ('sudo ' if use_sudo else '') + item }}"
environment:
SSHPASS: "{{ vm_pass }}"
loop: "{{ portainer_commands }}"
loop_control:
index_var: idx # capture loop index
label: "cmd-{{ idx }}" # avoid printing full command in (item=...) line
register: portainer_cmds
changed_when: false
no_log: "{{ DEBUG == 0 }}" # hide outputs and env when not debugging
- name: Show outputs for each Portainer command
ansible.builtin.debug:
msg: |
CMD: {{ item.item }}
RC: {{ item.rc }}
STDOUT:
{{ (item.stdout | default('')).strip() }}
STDERR:
{{ (item.stderr | default('')).strip() }}
loop: "{{ portainer_cmds.results }}"
when: DEBUG == 1
- name: Fail play if any Portainer command failed
ansible.builtin.assert:
that: "item.rc == 0"
fail_msg: "Portainer Agent update failed on VM: {{ item.item }} (rc={{ item.rc }})"
success_msg: "All Portainer Agent update commands succeeded."
loop: "{{ portainer_cmds.results }}"
loop_control:
index_var: idx
label: "cmd-{{ idx }}"
# -------------------------
# Readiness check (TCP port)
# -------------------------
- name: Portainer Agent | Wait for TCP port to be open from controller
ansible.builtin.wait_for:
host: "{{ vm_ip }}"
port: "{{ portainer_agent_port }}"
delay: 2 # initial delay before first check
timeout: "{{ RETRIES * 2 }}" # total timeout in seconds
state: started
register: portainer_wait
delegate_to: localhost
run_once: true
changed_when: false
- name: Portainer Agent | Print concise summary
ansible.builtin.debug:
msg: >-
Portainer Agent TCP {{ vm_ip }}:{{ portainer_agent_port }}
reachable={{ (portainer_wait is defined) and (not portainer_wait.failed | default(false)) }}
elapsed={{ portainer_wait.elapsed | default('n/a') }}s
when: DEBUG == 1
+6
View File
@@ -0,0 +1,6 @@
- name: Remove SSH key
hosts: all
become: yes
roles:
- role: manage_ssh_keys
remove_user: true
+2
View File
@@ -0,0 +1,2 @@
collections:
- name: community.routeros
+10
View File
@@ -0,0 +1,10 @@
---
# Schedule for our own borgmatic.timer (overrides the package-shipped unit).
# OnCalendar uses systemd.time(7) syntax. RandomizedDelaySec spreads load so
# every host doesn't hit the borg server at the same instant.
borgmatic_oncalendar: "*-*-* 03:00:00"
borgmatic_randomized_delay_sec: 3h
borgmatic_persistent: true
# Extra flags passed to the borgmatic invocation in our borgmatic.service.
borgmatic_verbosity_args: "--verbosity -1 --syslog-verbosity 1"
+131
View File
@@ -0,0 +1,131 @@
---
- name: Login to borg controller
ansible.builtin.uri:
url: "{{ borgcontroller_url }}/api/auth/login"
method: POST
body_format: json
body:
username: "{{ borgcontroller_username }}"
password: "{{ borgcontroller_password }}"
status_code: 200
delegate_to: localhost
become: false
register: _bc_login
no_log: true
- name: Get borg server SSH endpoint
ansible.builtin.uri:
url: "{{ borgcontroller_url }}/api/config"
method: GET
headers:
Cookie: "{{ _bc_login.cookies_string }}"
delegate_to: localhost
become: false
register: _bc_config
- name: List repositories
ansible.builtin.uri:
url: "{{ borgcontroller_url }}/api/repositories"
method: GET
headers:
Cookie: "{{ _bc_login.cookies_string }}"
delegate_to: localhost
become: false
register: _bc_repos
- name: Find existing repository for this host
ansible.builtin.set_fact:
_bc_existing: >-
{{ _bc_repos.json | selectattr('alias', 'eq', inventory_hostname) | list }}
- name: Create repository if missing
ansible.builtin.uri:
url: "{{ borgcontroller_url }}/api/repositories"
method: POST
body_format: json
body:
alias: "{{ inventory_hostname }}"
sshPublicKey: "{{ root_ssh.ssh_public_key }}"
storageSize: "{{ backup_hosts[inventory_hostname].storage_size_gb | int }}"
headers:
Cookie: "{{ _bc_login.cookies_string }}"
status_code: [200, 201]
delegate_to: localhost
become: false
when: _bc_existing | length == 0
- name: Update repository SSH key if root's key changed
ansible.builtin.uri:
url: "{{ borgcontroller_url }}/api/repositories/{{ _bc_existing[0].id }}"
method: PATCH
body_format: json
body:
sshPublicKey: "{{ root_ssh.ssh_public_key }}"
headers:
Cookie: "{{ _bc_login.cookies_string }}"
status_code: 200
delegate_to: localhost
become: false
when:
- _bc_existing | length > 0
- _bc_existing[0].sshPublicKey != root_ssh.ssh_public_key
- name: Re-list repositories after possible create/update
ansible.builtin.uri:
url: "{{ borgcontroller_url }}/api/repositories"
method: GET
headers:
Cookie: "{{ _bc_login.cookies_string }}"
delegate_to: localhost
become: false
register: _bc_repos_after
- name: Resolve repository for this host
ansible.builtin.set_fact:
_bc_repo: >-
{{ (_bc_repos_after.json | selectattr('alias', 'eq', inventory_hostname) | list)[0] }}
- name: Build borg SSH URI
ansible.builtin.set_fact:
borgcontroller_repo_uri: "ssh://{{ _bc_config.json.borgSshHost }}/./{{ _bc_repo.id }}"
_bc_borg_host: "{{ _bc_config.json.borgSshHost.split('@')[1].split(':')[0] }}"
_bc_borg_port: "{{ _bc_config.json.borgSshHost.split('@')[1].split(':')[1] | default('22') }}"
- name: Ensure /root/.ssh exists
ansible.builtin.file:
path: /root/.ssh
state: directory
owner: root
group: root
mode: '0700'
- name: Scan borg server SSH host key
ansible.builtin.command: ssh-keyscan -p {{ _bc_borg_port }} {{ _bc_borg_host }}
register: _bc_keyscan
changed_when: false
check_mode: false
- name: Trust borg server SSH host key (root known_hosts)
ansible.builtin.lineinfile:
path: /root/.ssh/known_hosts
line: "{{ item }}"
create: true
owner: root
group: root
mode: '0600'
loop: "{{ _bc_keyscan.stdout_lines }}"
when:
- item | length > 0
- not item.startswith('#')
- name: Initialize borg repository (no-op if already initialized)
ansible.builtin.command:
cmd: borg init --encryption=repokey-blake2 {{ borgcontroller_repo_uri }}
environment:
BORG_PASSPHRASE: "{{ borg_passphrase }}"
register: _borg_init
changed_when: _borg_init.rc == 0
failed_when:
- _borg_init.rc != 0
- "'already exists' not in (_borg_init.stderr | default(''))"
no_log: true
+105
View File
@@ -0,0 +1,105 @@
---
- name: Skip hosts without backup config
ansible.builtin.debug:
msg: "No entry in backup_hosts for {{ inventory_hostname }}; skipping backup role."
when: inventory_hostname not in (backup_hosts | default({}))
- name: Configure borgmatic
when: inventory_hostname in (backup_hosts | default({}))
block:
- name: Ensure borg_passphrase is set (Semaphore secret)
ansible.builtin.assert:
that:
- borg_passphrase is defined
- borg_passphrase | length > 0
fail_msg: "borg_passphrase must be defined (provided by Semaphore secrets)"
- name: Install borgmatic
ansible.builtin.package:
name: borgmatic
state: present
- name: Ensure /etc/borgmatic exists
ansible.builtin.file:
path: /etc/borgmatic
state: directory
owner: root
group: root
mode: '0750'
- name: Write borg passphrase file
ansible.builtin.copy:
dest: /etc/borgmatic/passphrase
content: "{{ borg_passphrase }}"
owner: root
group: root
mode: '0600'
no_log: true
- name: Ensure root has an SSH key for the borg server
ansible.builtin.user:
name: root
generate_ssh_key: true
ssh_key_type: ed25519
ssh_key_file: .ssh/id_ed25519
ssh_key_comment: "borgmatic@{{ inventory_hostname }}"
register: root_ssh
- name: Register / look up repository on borg controller
ansible.builtin.include_tasks: borgcontroller.yml
when:
- borgcontroller_username is defined
- borgcontroller_password is defined
- name: Build borgmatic config (strip controller-only keys, inject repository + passcommand)
ansible.builtin.set_fact:
_borgmatic_config: >-
{{
(backup_hosts[inventory_hostname]
| dict2items
| rejectattr('key', 'in', ['storage_size_gb'])
| items2dict)
| combine({'encryption_passcommand': 'cat /etc/borgmatic/passphrase'})
| combine(
{'repositories': [{'path': borgcontroller_repo_uri, 'label': inventory_hostname}]}
if borgcontroller_repo_uri is defined else {}
)
}}
- name: Deploy borgmatic config
ansible.builtin.template:
src: borgmatic.yaml.j2
dest: /etc/borgmatic/config.yaml
owner: root
group: root
mode: '0640'
- name: Deploy borgmatic systemd service (overrides package unit)
ansible.builtin.template:
src: borgmatic.service.j2
dest: /etc/systemd/system/borgmatic.service
owner: root
group: root
mode: '0644'
register: _borgmatic_service_unit
- name: Deploy borgmatic systemd timer (overrides package unit)
ansible.builtin.template:
src: borgmatic.timer.j2
dest: /etc/systemd/system/borgmatic.timer
owner: root
group: root
mode: '0644'
register: _borgmatic_timer_unit
- name: Reload systemd if units changed
ansible.builtin.systemd:
daemon_reload: true
when: _borgmatic_service_unit is changed or _borgmatic_timer_unit is changed
- name: Enable and start borgmatic timer
ansible.builtin.systemd:
name: borgmatic.timer
enabled: true
state: started
@@ -0,0 +1,21 @@
# Managed by Ansible — do not edit by hand.
[Unit]
Description=borgmatic backup
Wants=network-online.target
After=network-online.target
# Don't run on battery power.
ConditionACPower=true
[Service]
Type=oneshot
# Lower priority so backups don't starve foreground work.
Nice=19
CPUSchedulingPolicy=batch
IOSchedulingClass=best-effort
IOSchedulingPriority=7
IOWeight=100
Restart=no
# Prevent rate limiting of borgmatic log events.
LogRateLimitIntervalSec=0
# Delay start by a random amount handled in the timer; keep the service simple.
ExecStart=systemd-inhibit --who="borgmatic" --what="sleep:shutdown" --why="Prevent interrupting scheduled backup" /usr/bin/borgmatic {{ borgmatic_verbosity_args }}
+11
View File
@@ -0,0 +1,11 @@
# Managed by Ansible — do not edit by hand.
[Unit]
Description=Run borgmatic backup
[Timer]
OnCalendar={{ borgmatic_oncalendar }}
RandomizedDelaySec={{ borgmatic_randomized_delay_sec }}
Persistent={{ borgmatic_persistent | bool | lower }}
[Install]
WantedBy=timers.target
+3
View File
@@ -0,0 +1,3 @@
#jinja2: trim_blocks: True, lstrip_blocks: True
# Managed by Ansible — do not edit by hand.
{{ _borgmatic_config | to_nice_yaml(indent=2, width=1000) }}
@@ -0,0 +1,4 @@
- name: Add user and authorized key
authorized_keys:
user: "{{ user }}"
key: "{{ key }}"
+5
View File
@@ -0,0 +1,5 @@
- include_tasks: add_ssh_key.yml
when: add_user | default(false)
- include_tasks: remove_ssh_key.yml
when: remove_user | default(false)
@@ -0,0 +1,10 @@
- name: Remove authorized key
authorized_keys:
user: "{{ user }}"
key: "{{ key }}"
state: absent
- name: Ensure user is absent
user:
name: "{{ user }}"
state: absent
+122
View File
@@ -0,0 +1,122 @@
---
- name: Baseline user setup
hosts: all
become: true
tags: users
tasks:
- name: Pick sudo group per distro
set_fact:
sudo_group: >-
{{ 'wheel'
if ansible_facts.os_family in
['RedHat','Rocky','AlmaLinux','Fedora','OracleLinux','Suse']
else 'sudo' }}
- name: Ensure user exists
ansible.builtin.user:
name: "{{ item.name }}"
shell: "{{ item.shell }}"
groups: "{{ sudo_group }}"
append: true
create_home: true
loop: "{{ users }}"
- name: Enforce authorized SSH keys
ansible.builtin.authorized_key:
user: "{{ item.name }}"
key: "{{ item.ssh_keys | join('\n') }}"
exclusive: true
loop: "{{ users }}"
- name: Grant passwordless sudo
ansible.builtin.copy:
dest: "/etc/sudoers.d/{{ item.name }}"
mode: '0440'
content: "{{ item.name }} ALL=(ALL) NOPASSWD:ALL\n"
validate: 'visudo -cf %s'
loop: "{{ users }}"
when: item.sudo_nopasswd
# ==============================
# SECOND PLAY: SSH HARDENING
# ==============================
- name: SSH Hardening
hosts: all
become: true
tags: never,hardening
tasks:
- name: Detect if system is Proxmox
ansible.builtin.stat:
path: /usr/bin/pveversion
register: proxmox_check
- name: Ensure sshd_config.d directory exists
ansible.builtin.file:
path: /etc/ssh/sshd_config.d
state: directory
- name: Deploy SSH hardening config
ansible.builtin.copy:
dest: /etc/ssh/sshd_config.d/99-ansible-hardening.conf
mode: '0644'
content: |
PasswordAuthentication no
ChallengeResponseAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
UsePAM yes
{% if not proxmox_check.stat.exists %}
PermitRootLogin no
{% else %}
PermitRootLogin prohibit-password
{% endif %}
validate: 'sshd -t -f %s'
notify: Restart SSH
handlers:
- name: Restart SSH
ansible.builtin.service:
name: "{{ 'sshd'
if ansible_facts.os_family in
['RedHat','Rocky','AlmaLinux','Fedora','OracleLinux','Suse']
else 'ssh' }}"
state: restarted
# ==============================
# THIRD PLAY: HOSTNAME
# ==============================
- name: Set hostname from inventory
hosts: all
become: true
tags: never,hostname
tasks:
- name: Set system hostname to inventory_hostname
ansible.builtin.hostname:
name: "{{ inventory_hostname }}"
# ==============================
# FOURTH PLAY: FREEIPA / SSSD
# ==============================
- name: FreeIPA client setup
hosts: all
become: true
tags: never,sssd
roles:
- role: initial_install/roles/freeipa_client
# ==============================
# FIFTH PLAY: BACKUP
# ==============================
- import_playbook: backup.yml
+57
View File
@@ -0,0 +1,57 @@
---
- name: Send and verify SMS delivery via internet-master.cz
hosts: localhost
gather_facts: false
vars:
sms_number: "601358865"
sms_username: "mikrotik"
sms_password_send: "jdkotzHJIOPWhjtr32D"
sms_password_recv: "jdkotzHJIOPWhjtr32D"
sms_wait_seconds: 15 # Wait 15s for delivery
tasks:
- name: Generate random test string
set_fact:
random_string: "mikrotik_{{ lookup('password', '/dev/null length=8 chars=ascii_letters') }}"
- name: Send SMS message
uri:
url: "https://sms.internet-master.cz/send/?number={{ sms_number }}&message=@mikrotik@{{ random_string | urlencode }}&type=class-1&username={{ sms_username }}&password={{ sms_password_send }}"
method: GET
return_content: true
register: send_result
- name: Show send API response
debug:
var: send_result.content
- name: Wait for SMS to be delivered
pause:
seconds: "{{ sms_wait_seconds }}"
- name: Fetch received messages
uri:
url: "https://sms.internet-master.cz/receive/?username={{ sms_username }}&password={{ sms_password_recv }}"
method: GET
return_content: true
register: recv_result
- name: Parse received JSON
set_fact:
inbox: "{{ recv_result.json.inbox | default([]) }}"
- name: Check if random string message was received
set_fact:
message_found: "{{ inbox | selectattr('message', 'equalto', random_string) | list | length > 0 }}"
- name: Report result
debug:
msg: >
SMS with message '{{ random_string }}' was {{
'delivered ✅' if message_found else 'NOT delivered ❌'
}}.
- name: Fail if not delivered
fail:
msg: "Message '{{ random_string }}' not found in received inbox!"
when: not message_found
+91
View File
@@ -0,0 +1,91 @@
---
- name: Update system (APT + Flatpak)
hosts: all
become: yes
gather_facts: yes
serial: 5
tasks:
- name: Ensure SSH is reachable (skip host if not)
delegate_to: localhost
wait_for:
host: "{{ ansible_host | default(inventory_hostname) }}"
port: 22
timeout: 5
register: ssh_check
ignore_errors: yes
- meta: end_host
when: ssh_check is failed
- name: Ping with retries (handle intermittent flaps)
ping:
register: ping_result
retries: 5
delay: 5
until: ping_result is success
- name: Wait for apt lock to be released
shell: |
while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do
echo "Waiting for apt lock..."
sleep 5
done
changed_when: false
- name: Update apt cache
apt:
update_cache: yes
- name: Perform full upgrade
apt:
upgrade: full
autoremove: yes
autoclean: yes
register: apt_upgrade
retries: 3
delay: 10
until: apt_upgrade is succeeded
- name: Fix broken packages
command: apt-get -f install -y
register: fix_result
failed_when: false
changed_when: "'Setting up' in fix_result.stdout"
- name: Check if Flatpak is installed
command: which flatpak
register: flatpak_check
changed_when: false
failed_when: false
- name: Update system Flatpaks
command: flatpak update -y --noninteractive --system
when: flatpak_check.rc == 0
failed_when: false
- name: Update user Flatpaks
command: flatpak update -y --noninteractive --user
become: false
when: flatpak_check.rc == 0
failed_when: false
- name: Remove unused Flatpaks
command: flatpak uninstall -y --noninteractive --unused
when: flatpak_check.rc == 0
failed_when: false
- name: Update snap packages
command: snap refresh
failed_when: false
- name: Check if reboot is required
stat:
path: /var/run/reboot-required
register: reboot_required
- name: Notify if reboot required
debug:
msg: "Reboot required on {{ inventory_hostname }}"
when: reboot_required.stat.exists
-155
View File
@@ -1,155 +0,0 @@
# update_broker_kafka-ui.yml
- name: Update Kafka broker3 and Redpanda Console on VM via Proxmox
hosts: linux_servers
gather_facts: false
become: true
become_user: root
become_method: sudo
vars:
# --- Connection to VM (provided by Semaphore env vars) ---
vm_ip: "{{ lookup('env', 'VM_IP') }}"
vm_user: "{{ lookup('env', 'VM_USER') }}"
vm_pass: "{{ lookup('env', 'VM_PASS') }}"
use_sudo: false
# --- Debug mode (controlled via Semaphore variable) ---
DEBUG: "{{ lookup('env', 'DEBUG') | default(0) | int }}"
RETRIES: "{{ lookup('env', 'RETRIES') | default(25) | int }}"
# --- Kafka / Redpanda Console specifics ---
kafka_project: "kafka"
# Adjusted to match your actual compose file location
kafka_compose_file: "/data/compose/docker-compose.yml"
kafka_services:
- broker3
- kafka-ui
redpanda_console_port: 8084
# Controller-side URL (default to direct VM IP/port or external URL)
redpanda_console_url: "{{ lookup('env', 'REDPANDA_CONSOLE_URL') | default('http://192.168.69.254:8084/overview', true) }}"
redpanda_retries: "{{ RETRIES }}"
redpanda_delay: 2
# Docker command prefix (consistent behavior and quiet hints)
docker_prefix: "unalias docker 2>/dev/null || true; DOCKER_CLI_HINTS=0; command docker"
# Commands to run on the target VM (quiet outputs)
# 1) Pull latest images for broker3 + kafka-ui
# 2) Stop any running containers with these names (legacy or compose-managed)
# 3) Remove any containers with these names to avoid name conflicts
# 4) Recreate services via docker compose
kafka_commands:
- "{{ docker_prefix }} compose -p {{ kafka_project }} -f {{ kafka_compose_file }} pull {{ kafka_services | join(' ') }} >/dev/null"
- "{{ docker_prefix }} stop {{ kafka_services | join(' ') }} >/dev/null 2>&1 || true"
- "{{ docker_prefix }} rm -f {{ kafka_services | join(' ') }} >/dev/null 2>&1 || true"
- "{{ docker_prefix }} compose -p {{ kafka_project }} -f {{ kafka_compose_file }} up -d --no-deps --force-recreate {{ kafka_services | join(' ') }} >/dev/null"
tasks:
- name: Ensure sshpass is installed (for password-based SSH) # English comments
ansible.builtin.apt:
name: sshpass
state: present
update_cache: yes
- name: Run Kafka update commands on VM (via SSH) # use SSHPASS env, hide item value
ansible.builtin.command:
argv:
- sshpass
- -e # read password from SSHPASS environment
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- "{{ ('sudo ' if use_sudo else '') + item }}"
environment:
SSHPASS: "{{ vm_pass }}" # supply password via environment
loop: "{{ kafka_commands }}"
loop_control:
index_var: idx # capture loop index
label: "cmd-{{ idx }}" # avoid printing full command in (item=...) line
register: kafka_cmds
changed_when: false
no_log: "{{ DEBUG == 0 }}" # hide outputs and env when not debugging
- name: Show outputs for each Kafka command
ansible.builtin.debug:
msg: |
CMD: {{ item.item }}
RC: {{ item.rc }}
STDOUT:
{{ (item.stdout | default('')).strip() }}
STDERR:
{{ (item.stderr | default('')).strip() }}
loop: "{{ kafka_cmds.results }}"
when: DEBUG == 1
- name: Fail play if any Kafka command failed # also hide item label
ansible.builtin.assert:
that: "item.rc == 0"
fail_msg: "Kafka/Redpanda Console update failed on VM: {{ item.item }} (rc={{ item.rc }})"
success_msg: "All Kafka/Redpanda Console update commands succeeded."
loop: "{{ kafka_cmds.results }}"
loop_control:
index_var: idx
label: "cmd-{{ idx }}"
# -------------------------
# Readiness check Redpanda Console UI
# -------------------------
- name: Redpanda Console | Wait for overview page (controller, with retries)
ansible.builtin.uri:
url: "{{ redpanda_console_url }}"
method: GET
return_content: true
validate_certs: false # plain HTTP on 192.168.69.254 (or as configured)
status_code: 200
register: redpanda_controller
delegate_to: localhost
run_once: true
when: redpanda_console_url is defined and (redpanda_console_url | length) > 0
retries: "{{ redpanda_retries }}"
delay: "{{ redpanda_delay }}"
until: redpanda_controller.status == 200
failed_when: false
changed_when: false
- name: Redpanda Console | Print concise summary
ansible.builtin.debug:
msg: >-
Redpanda Console overview {{ 'reachable' if (redpanda_controller is defined and (redpanda_controller.status|default(0))==200) else 'NOT reachable' }}.
status={{ redpanda_controller.status | default('n/a') }};
length={{ (redpanda_controller.content | default('')) | length }};
when: DEBUG == 1 and (redpanda_controller is defined)
# Optional detailed dump (short excerpt only)
- name: Redpanda Console | HTML excerpt (debug)
ansible.builtin.debug:
msg: "{{ (redpanda_controller.content | default(''))[:500] }}"
when: DEBUG == 1 and (redpanda_controller is defined) and (redpanda_controller.content is defined)
# -------------------------
# Final assertion: Console URL must be reachable
# -------------------------
- name: Redpanda Console | Assert overview reachable (if URL configured)
ansible.builtin.assert:
that:
- >
not (redpanda_console_url is defined and (redpanda_console_url | length) > 0)
or
(
redpanda_controller is defined
and (redpanda_controller.status | default(0)) == 200
)
fail_msg: "Redpanda Console URL {{ redpanda_console_url }} is NOT reachable with HTTP 200 after retries."
success_msg: "Redpanda Console URL {{ redpanda_console_url }} is reachable with HTTP 200."
-194
View File
@@ -1,194 +0,0 @@
# update_homarr.yml
- name: Update Homarr on VM via Proxmox
hosts: linux_servers
gather_facts: false
become: true
become_user: root
become_method: sudo
vars:
# VM connection (provided by Semaphore env vars)
vm_ip: "{{ lookup('env', 'VM_IP') }}"
vm_user: "{{ lookup('env', 'VM_USER') }}"
vm_pass: "{{ lookup('env', 'VM_PASS') }}"
use_sudo: false
# --- Debug mode (controlled via Semaphore variable) ---
DEBUG: "{{ lookup('env', 'DEBUG') | default(0) | int }}"
RETRIES: "{{ lookup('env', 'RETRIES') | default(25) | int }}"
# Homarr specifics
homarr_project: "homarr"
homarr_compose_file: "/data/compose/homarr/docker-compose-homarr.yml"
homarr_service: "homarr"
homarr_image: "ghcr.io/homarr-labs/homarr:latest"
homarr_port: 7575
# Optional external URL for controller-side readiness check (e.g., https://homarr.example.com)
# If empty/undefined, controller check is skipped and we only probe from the VM.
homarr_url: "{{ lookup('env', 'HOMARR_URL') | default('', true) }}"
# Fixed container name used in your compose (avoid conflicts with any leftover container)
homarr_container_name: "homarr"
# Retry policy (same pattern as Kuma): 25x with 2s delay
homarr_retries: "{{ RETRIES }}"
homarr_delay: 2
# Docker command prefix (consistent behavior)
docker_prefix: "unalias docker 2>/dev/null || true; DOCKER_CLI_HINTS=0; command docker"
# Commands to run on the target VM (quiet outputs)
homarr_commands:
- "{{ docker_prefix }} pull -q {{ homarr_image }} >/dev/null"
- "{{ docker_prefix }} compose -p {{ homarr_project }} -f {{ homarr_compose_file }} pull {{ homarr_service }} >/dev/null"
# remove conflicting container name before compose up (silently)
- "{{ docker_prefix }} rm -f {{ homarr_container_name }} >/dev/null 2>&1 || true"
- "{{ docker_prefix }} compose -p {{ homarr_project }} -f {{ homarr_compose_file }} up -d --no-deps --force-recreate {{ homarr_service }} >/dev/null"
tasks:
- name: Ensure sshpass is installed (for password-based SSH) # English comments
ansible.builtin.apt:
name: sshpass
state: present
update_cache: yes
- name: Run Homarr update commands on VM (via SSH) # use SSHPASS env, hide item label
ansible.builtin.command:
argv:
- sshpass
- -e # read password from SSHPASS environment
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- "{{ ('sudo ' if use_sudo else '') + item }}"
environment:
SSHPASS: "{{ vm_pass }}" # supply password via environment
loop: "{{ homarr_commands }}"
loop_control:
index_var: idx # capture loop index
label: "cmd-{{ idx }}" # avoid printing full command in (item=...) line
register: homarr_cmds
changed_when: false
no_log: "{{ DEBUG == 0 }}" # hide outputs and env when not debugging
- name: Show outputs for each Homarr command
ansible.builtin.debug:
msg: |
CMD: {{ item.item }}
RC: {{ item.rc }}
STDOUT:
{{ (item.stdout | default('')).strip() }}
STDERR:
{{ (item.stderr | default('')).strip() }}
loop: "{{ homarr_cmds.results }}"
when: DEBUG == 1
- name: Fail play if any Homarr command failed # also hide item label
ansible.builtin.assert:
that: "item.rc == 0"
fail_msg: "Homarr update failed on VM: {{ item.item }} (rc={{ item.rc }})"
success_msg: "All Homarr update commands succeeded."
loop: "{{ homarr_cmds.results }}"
loop_control:
index_var: idx
label: "cmd-{{ idx }}"
# -------------------------
# Readiness checks (controller first, then VM fallback)
# -------------------------
- name: Homarr | Wait for homepage (controller first, with retries)
ansible.builtin.uri:
url: "{{ (homarr_url | regex_replace('/$','')) + '/' }}"
method: GET
return_content: true
# Validate TLS only when using https://
validate_certs: "{{ (homarr_url | default('')) is match('^https://') }}"
status_code: 200
register: homarr_controller
delegate_to: localhost
run_once: true
when: homarr_url is defined and (homarr_url | length) > 0
retries: "{{ homarr_retries }}"
delay: "{{ homarr_delay }}"
until: homarr_controller.status == 200
failed_when: false
changed_when: false
- name: Homarr | VM-side fetch (HTML via Python, with retries) # use SSHPASS env here too
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- |
python3 - <<'PY'
# Fetch Homarr homepage from localhost and print HTML to stdout
import urllib.request, sys
try:
with urllib.request.urlopen("http://127.0.0.1:{{ homarr_port }}/", timeout=15) as r:
sys.stdout.write(r.read().decode(errors='ignore'))
except Exception:
pass
PY
environment:
SSHPASS: "{{ vm_pass }}"
register: homarr_vm
changed_when: false
failed_when: false
when: homarr_controller.status | default(0) != 200 or homarr_controller.content is not defined
retries: "{{ homarr_retries }}"
delay: "{{ homarr_delay }}"
until: (homarr_vm.stdout | default('') | trim | length) > 0 and ('Homarr' in (homarr_vm.stdout | default('')))
no_log: "{{ DEBUG == 0 }}"
- name: Homarr | Choose homepage HTML (controller wins, else VM) # safe guard against empty result
ansible.builtin.set_fact:
homarr_home_html: >-
{{
(
homarr_controller.content
if (homarr_controller is defined)
and ((homarr_controller.status|default(0))==200)
and (homarr_controller.content is defined)
else
(homarr_vm.stdout | default('') | trim)
)
}}
when:
- (homarr_controller is defined and (homarr_controller.status|default(0))==200 and (homarr_controller.content is defined))
or ((homarr_vm.stdout | default('') | trim | length) > 0)
- name: Homarr | Print concise summary
ansible.builtin.debug:
msg: >-
Homarr homepage {{ 'reachable' if (homarr_home_html is defined) else 'NOT reachable' }}.
Source={{ 'controller' if ((homarr_controller is defined) and (homarr_controller.status|default(0))==200 and (homarr_controller.content is defined)) else 'vm' if (homarr_vm.stdout|default('')|trim|length>0) else 'n/a' }};
length={{ (homarr_home_html | default('')) | length }};
contains('Homarr')={{ (homarr_home_html is defined) and ('Homarr' in homarr_home_html) }}
when: DEBUG == 1
- name: Homarr | Homepage unavailable (after retries)
ansible.builtin.debug:
msg: "Homarr web není dostupný ani po pokusech."
when: homarr_home_html is not defined and DEBUG == 1
# Optional detailed dump (short excerpt only)
- name: Homarr | HTML excerpt (debug)
ansible.builtin.debug:
msg: "{{ (homarr_home_html | default(''))[:500] }}"
when: homarr_home_html is defined and DEBUG == 1
-313
View File
@@ -1,313 +0,0 @@
# update_immich.yml
- name: Update Immich on VM via Proxmox
hosts: linux_servers
gather_facts: false
become: true
become_user: root
become_method: sudo
vars:
# VM connection (provided by Semaphore env vars)
vm_ip: "{{ lookup('env', 'VM_IP') }}"
vm_user: "{{ lookup('env', 'VM_USER') }}"
vm_pass: "{{ lookup('env', 'VM_PASS') }}"
use_sudo: false
# --- Debug mode (controlled via Semaphore variable) ---
DEBUG: "{{ lookup('env', 'DEBUG') | default(0) | int }}"
RETRIES: "{{ lookup('env', 'RETRIES') | default(25) | int }}"
# Immich specifics
immich_dir: "/opt/immich"
immich_project: "immich"
immich_compose_url: "https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml"
immich_compose_file: "/opt/immich/docker-compose.yml"
immich_override_file: "/opt/immich/docker-compose.override.yml"
immich_port: 2283
# Optional external URL for controller-side readiness check (e.g., https://photos.example.com)
immich_url: "{{ lookup('env', 'IMMICH_URL') | default('', true) }}"
# Retry policy
immich_retries: "{{ RETRIES }}"
immich_delay: 2
# Docker command prefix (consistent behavior)
docker_prefix: "unalias docker 2>/dev/null || true; DOCKER_CLI_HINTS=0; command docker"
# Compose command (always include override to keep local mounts separate from upstream compose)
immich_compose_cmd: >-
{{ docker_prefix }} compose
-p {{ immich_project }}
-f {{ immich_compose_file }}
-f {{ immich_override_file }}
# Commands to run on the target VM
immich_commands:
- "cd {{ immich_dir }}"
- |
cd {{ immich_dir }}
mkdir -p backups
if [ -f docker-compose.yml ]; then
cp -a docker-compose.yml "backups/docker-compose.yml.$(date +%F_%H%M%S).bak"
fi
if [ -f .env ]; then
cp -a .env "backups/.env.$(date +%F_%H%M%S).bak"
fi
if [ -f docker-compose.override.yml ]; then
cp -a docker-compose.override.yml "backups/docker-compose.override.yml.$(date +%F_%H%M%S).bak"
fi
- |
cd {{ immich_dir }}
# Download latest compose from Immich releases (requires curl or wget)
if command -v curl >/dev/null 2>&1; then
curl -fsSL -o docker-compose.yml "{{ immich_compose_url }}"
elif command -v wget >/dev/null 2>&1; then
wget -qO docker-compose.yml "{{ immich_compose_url }}"
else
echo "Neither curl nor wget is available on the VM."
exit 1
fi
- |
cd {{ immich_dir }}
# Ensure override compose exists (create if missing)
if [ ! -f "{{ immich_override_file }}" ]; then
printf '%s\n' \
'services:' \
' immich-server:' \
' volumes:' \
' - /mnt/nextcloud-howard-photos:/mnt/nextcloud-howard-photos' \
' - /mnt/nextcloud-kamilkaprdelka-photos:/mnt/nextcloud-kamilkaprdelka-photos' \
> "{{ immich_override_file }}"
fi
# Fail early if override is still missing/empty
test -s "{{ immich_override_file }}"
- |
cd {{ immich_dir }}
# Ensure .env exists. If missing, try to reconstruct it from running containers to avoid breaking DB creds.
python3 - <<'PY'
import json
import subprocess
from pathlib import Path
env_path = Path(".env")
if env_path.exists():
raise SystemExit(0)
def run(cmd):
p = subprocess.run(cmd, capture_output=True, text=True)
return p.returncode, p.stdout, p.stderr
rc, out, err = run(["bash", "-lc", "command docker inspect immich_postgres immich_server"])
if rc != 0 or not out.strip():
print("ERROR: .env is missing and cannot inspect running containers (immich_postgres/immich_server).", flush=True)
print("Create /opt/immich/.env manually or ensure the containers exist.", flush=True)
raise SystemExit(1)
data = json.loads(out)
by_name = {}
for c in data:
name = (c.get("Name") or "").lstrip("/")
by_name[name] = c
pg = by_name.get("immich_postgres")
srv = by_name.get("immich_server")
if not pg or not srv:
print("ERROR: Could not find immich_postgres and immich_server in docker inspect output.", flush=True)
raise SystemExit(1)
def env_map(container):
m = {}
for kv in (container.get("Config", {}).get("Env") or []):
if "=" in kv:
k, v = kv.split("=", 1)
m[k] = v
return m
def find_mount_source(container, dest):
for m in (container.get("Mounts") or []):
if m.get("Destination") == dest:
return m.get("Source")
return ""
pg_env = env_map(pg)
db_user = pg_env.get("POSTGRES_USER", "")
db_pass = pg_env.get("POSTGRES_PASSWORD", "")
db_name = pg_env.get("POSTGRES_DB", "")
db_data = find_mount_source(pg, "/var/lib/postgresql/data")
upload_loc = find_mount_source(srv, "/usr/src/app/upload")
# Try to preserve the currently used image tag as IMMICH_VERSION (optional but safer)
immich_version = ""
image = (srv.get("Config", {}).get("Image") or "")
if ":" in image and "@" not in image:
immich_version = image.rsplit(":", 1)[-1]
elif ":" in image and "@" in image:
# image like repo:tag@sha256:...
immich_version = image.split("@", 1)[0].rsplit(":", 1)[-1]
missing = []
for k, v in [
("DB_USERNAME", db_user),
("DB_PASSWORD", db_pass),
("DB_DATABASE_NAME", db_name),
("DB_DATA_LOCATION", db_data),
("UPLOAD_LOCATION", upload_loc),
]:
if not v:
missing.append(k)
if missing:
print("ERROR: Could not reconstruct these .env values from containers: " + ", ".join(missing), flush=True)
raise SystemExit(1)
lines = [
f"UPLOAD_LOCATION={upload_loc}",
f"DB_DATA_LOCATION={db_data}",
f"DB_USERNAME={db_user}",
f"DB_PASSWORD={db_pass}",
f"DB_DATABASE_NAME={db_name}",
]
if immich_version:
lines.append(f"IMMICH_VERSION={immich_version}")
env_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
print("Created .env from running containers.", flush=True)
PY
- |
cd {{ immich_dir }}
# Comment out healthcheck.start_interval if present (safe no-op if missing)
sed -i -E 's/^([[:space:]]*)start_interval:/\1# start_interval:/' docker-compose.yml || true
- "cd {{ immich_dir }} && {{ immich_compose_cmd }} config >/dev/null"
- "cd {{ immich_dir }} && {{ immich_compose_cmd }} pull >/dev/null"
- "cd {{ immich_dir }} && {{ immich_compose_cmd }} up -d --remove-orphans --force-recreate >/dev/null"
tasks:
- name: Ensure sshpass is installed (for password-based SSH) # English comments
ansible.builtin.apt:
name: sshpass
state: present
update_cache: yes
- name: Run Immich update commands on VM (via SSH) # use SSHPASS env, hide item label
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- "{{ ('sudo ' if use_sudo else '') + item }}"
environment:
SSHPASS: "{{ vm_pass }}"
loop: "{{ immich_commands }}"
loop_control:
index_var: idx
label: "cmd-{{ idx }}"
register: immich_cmds
changed_when: false
no_log: "{{ DEBUG == 0 }}"
run_once: true
- name: Show outputs for each Immich command
ansible.builtin.debug:
msg: |
CMD: {{ item.item }}
RC: {{ item.rc }}
STDOUT:
{{ (item.stdout | default('')).strip() }}
STDERR:
{{ (item.stderr | default('')).strip() }}
loop: "{{ immich_cmds.results }}"
when: DEBUG == 1
run_once: true
- name: Fail play if any Immich command failed
ansible.builtin.assert:
that: "item.rc == 0"
fail_msg: "Immich update failed on VM: {{ item.item }} (rc={{ item.rc }})"
success_msg: "All Immich update commands succeeded."
loop: "{{ immich_cmds.results }}"
loop_control:
index_var: idx
label: "cmd-{{ idx }}"
run_once: true
# -------------------------
# Readiness checks (controller first, then VM fallback)
# -------------------------
- name: Immich | Wait for API ping (controller first, with retries)
ansible.builtin.uri:
url: "{{ (immich_url | regex_replace('/$','')) + '/api/server/ping' }}"
method: GET
return_content: true
validate_certs: "{{ (immich_url | default('')) is match('^https://') }}"
status_code: 200
register: immich_controller
delegate_to: localhost
run_once: true
when: immich_url is defined and (immich_url | length) > 0
retries: "{{ immich_retries }}"
delay: "{{ immich_delay }}"
until: immich_controller.status == 200 and ('pong' in (immich_controller.content | default('')))
failed_when: false
changed_when: false
- name: Immich | VM-side ping (JSON via Python, with retries)
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- |
python3 - <<'PY'
# Ping Immich API from localhost and print response to stdout
import urllib.request, sys
try:
with urllib.request.urlopen("http://127.0.0.1:{{ immich_port }}/api/server/ping", timeout=15) as r:
sys.stdout.write(r.read().decode(errors='ignore'))
except Exception:
pass
PY
environment:
SSHPASS: "{{ vm_pass }}"
register: immich_vm
changed_when: false
failed_when: false
when: immich_controller.status | default(0) != 200
retries: "{{ immich_retries }}"
delay: "{{ immich_delay }}"
until: (immich_vm.stdout | default('') | trim | length) > 0 and ('pong' in (immich_vm.stdout | default('')))
no_log: "{{ DEBUG == 0 }}"
run_once: true
- name: Immich | Print concise summary
ansible.builtin.debug:
msg: >-
Immich API ping {{ 'OK' if (('pong' in (immich_controller.content|default(''))) or ('pong' in (immich_vm.stdout|default('')))) else 'NOT OK' }}.
Source={{ 'controller' if (immich_controller.status|default(0))==200 else 'vm' if (immich_vm.stdout|default('')|trim|length>0) else 'n/a' }}.
when: DEBUG == 1
run_once: true
-65
View File
@@ -1,65 +0,0 @@
# update_semaphore.yml
- name: Update Semaphore on VM via Proxmox
hosts: linux_servers
gather_facts: false
become: true
become_user: root
become_method: sudo
vars:
# --- Connection to VM (provided by Semaphore env vars) ---
vm_ip: "{{ lookup('env', 'VM_IP') }}" # IP vm-portainer
vm_user: "{{ lookup('env', 'VM_USER') }}"
vm_pass: "{{ lookup('env', 'VM_PASS') }}"
use_sudo: false
# --- Debug mode (controlled via Semaphore variable) ---
DEBUG: "{{ lookup('env', 'DEBUG') | default(0) | int }}"
# --- Semaphore specifics ---
semaphore_project: "semaphore"
semaphore_compose_file: "/data/compose/semaphore/docker-compose.yml"
semaphore_service: "semaphore"
tasks:
- name: Ensure sshpass is installed (for password-based SSH) # English comments
ansible.builtin.apt:
name: sshpass
state: present
update_cache: yes
- name: Run Semaphore self-update on VM in background (nohup)
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- |
nohup bash -c '
unalias docker 2>/dev/null || true
DOCKER_CLI_HINTS=0 docker compose \
-p {{ semaphore_project }} \
-f {{ semaphore_compose_file }} \
up -d --no-deps --force-recreate --pull always {{ semaphore_service }}
' >/dev/null 2>&1 &
environment:
SSHPASS: "{{ vm_pass }}"
register: semaphore_update
changed_when: false
no_log: "{{ DEBUG == 0 }}"
- name: Show result of Semaphore self-update (debug)
ansible.builtin.debug:
msg: |
RC: {{ semaphore_update.rc }}
STDOUT: {{ (semaphore_update.stdout | default('')).strip() }}
STDERR: {{ (semaphore_update.stderr | default('')).strip() }}
when: DEBUG == 1
-34
View File
@@ -1,34 +0,0 @@
- name: Update system (APT + Flatpak)
hosts: all
become: true
become_user: root
become_method: sudo
tasks:
- name: Update APT cache
apt:
update_cache: yes
- name: Upgrade all APT packages
apt:
upgrade: dist
- name: Check if flatpak binary exists
stat:
path: /usr/bin/flatpak
register: flatpak_bin
- name: Update system Flatpaks
shell: timeout 300 flatpak update -y
register: flatpak_sys
failed_when: flatpak_sys.rc != 0 and flatpak_sys.rc != 124
when: flatpak_bin.stat.exists
- name: Update user Flatpaks
become_user: jakub
environment:
XDG_RUNTIME_DIR: /run/user/1000
shell: timeout 300 flatpak update -y
register: flatpak_user
failed_when: flatpak_user.rc != 0 and flatpak_user.rc != 124
when: flatpak_bin.stat.exists
-194
View File
@@ -1,194 +0,0 @@
# nextcloud/update_uptime_kuma.yml
- name: Update Uptime Kuma on VM via Proxmox
hosts: linux_servers
gather_facts: false
become: true
become_user: root
become_method: sudo
vars:
# VM connection (provided by Semaphore env vars)
vm_ip: "{{ lookup('env', 'VM_IP') }}"
vm_user: "{{ lookup('env', 'VM_USER') }}"
vm_pass: "{{ lookup('env', 'VM_PASS') }}"
use_sudo: false
# --- Debug mode (controlled via Semaphore variable) ---
DEBUG: "{{ lookup('env', 'DEBUG') | default(0) | int }}"
RETRIES: "{{ lookup('env', 'RETRIES') | default(25) | int }}"
# Uptime Kuma specifics
kuma_project: "uptime-kuma"
kuma_compose_file: "/data/compose/uptime-kuma/docker-compose-uptime-kuma.yml"
kuma_service: "uptime-kuma"
kuma_image: "louislam/uptime-kuma:latest"
kuma_port: 3001
# Optional external URL for controller-side readiness check (e.g., https://kuma.example.com)
# If empty/undefined, controller check is skipped and we only probe from the VM.
kuma_url: "{{ lookup('env', 'KUMA_URL') | default('', true) }}"
# Fixed container name used in your compose (conflicts with previous/Portainer-run container)
kuma_container_name: "uptime-kuma-dev"
# Retry policy
kuma_retries: "{{ RETRIES }}"
kuma_delay: 2
# Docker command prefix (consistent behavior)
docker_prefix: "unalias docker 2>/dev/null || true; DOCKER_CLI_HINTS=0; command docker"
# Commands to run on the target VM (quiet outputs like in Collabora play)
kuma_commands:
- "{{ docker_prefix }} pull -q {{ kuma_image }} >/dev/null"
- "{{ docker_prefix }} compose -p {{ kuma_project }} -f {{ kuma_compose_file }} pull {{ kuma_service }} >/dev/null"
# remove conflicting container name before compose up (silently)
- "{{ docker_prefix }} rm -f {{ kuma_container_name }} >/dev/null 2>&1 || true"
- "{{ docker_prefix }} compose -p {{ kuma_project }} -f {{ kuma_compose_file }} up -d --no-deps --force-recreate {{ kuma_service }} >/dev/null"
tasks:
- name: Ensure sshpass is installed (for password-based SSH) # English comments
ansible.builtin.apt:
name: sshpass
state: present
update_cache: yes
- name: Run Uptime Kuma update commands on VM (via SSH) # use SSHPASS env, hide item label
ansible.builtin.command:
argv:
- sshpass
- -e # read password from SSHPASS environment
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- "{{ ('sudo ' if use_sudo else '') + item }}"
environment:
SSHPASS: "{{ vm_pass }}" # supply password via environment
loop: "{{ kuma_commands }}"
loop_control:
index_var: idx # capture loop index
label: "cmd-{{ idx }}" # avoid printing full command in (item=...) line
register: kuma_cmds
changed_when: false
no_log: "{{ DEBUG == 0 }}" # hide outputs and env when not debugging
- name: Show outputs for each Uptime Kuma command
ansible.builtin.debug:
msg: |
CMD: {{ item.item }}
RC: {{ item.rc }}
STDOUT:
{{ (item.stdout | default('')).strip() }}
STDERR:
{{ (item.stderr | default('')).strip() }}
loop: "{{ kuma_cmds.results }}"
when: DEBUG == 1
- name: Fail play if any Uptime Kuma command failed # also hide item label
ansible.builtin.assert:
that: "item.rc == 0"
fail_msg: "Uptime Kuma update failed on VM: {{ item.item }} (rc={{ item.rc }})"
success_msg: "All Uptime Kuma update commands succeeded."
loop: "{{ kuma_cmds.results }}"
loop_control:
index_var: idx
label: "cmd-{{ idx }}"
# -------------------------
# Readiness checks (controller first, then VM fallback)
# -------------------------
- name: Kuma | Wait for homepage (controller first, with retries)
ansible.builtin.uri:
url: "{{ (kuma_url | regex_replace('/$','')) + '/' }}"
method: GET
return_content: true
# Validate TLS only when using https://
validate_certs: "{{ (kuma_url | default('')) is match('^https://') }}"
status_code: 200
register: kuma_controller
delegate_to: localhost
run_once: true
when: kuma_url is defined and (kuma_url | length) > 0
retries: "{{ kuma_retries }}"
delay: "{{ kuma_delay }}"
until: kuma_controller.status == 200
failed_when: false
changed_when: false
- name: Kuma | VM-side fetch (HTML via Python, with retries) # use SSHPASS env here too
ansible.builtin.command:
argv:
- sshpass
- -e
- ssh
- -o
- StrictHostKeyChecking=no
- -o
- ConnectTimeout=15
- "{{ vm_user }}@{{ vm_ip }}"
- bash
- -lc
- |
python3 - <<'PY'
# Fetch Kuma homepage from localhost and print HTML to stdout
import urllib.request, sys
try:
with urllib.request.urlopen("http://127.0.0.1:{{ kuma_port }}/", timeout=15) as r:
sys.stdout.write(r.read().decode(errors='ignore'))
except Exception:
pass
PY
environment:
SSHPASS: "{{ vm_pass }}"
register: kuma_vm
changed_when: false
failed_when: false
when: kuma_controller.status | default(0) != 200 or kuma_controller.content is not defined
retries: "{{ kuma_retries }}"
delay: "{{ kuma_delay }}"
until: (kuma_vm.stdout | default('') | trim | length) > 0 and ('Uptime Kuma' in (kuma_vm.stdout | default('')))
no_log: "{{ DEBUG == 0 }}" # hide command and output when not debugging
- name: Kuma | Choose homepage HTML (controller wins, else VM) # safe guard against empty result
ansible.builtin.set_fact:
kuma_home_html: >-
{{
(
kuma_controller.content
if (kuma_controller is defined)
and ((kuma_controller.status|default(0))==200)
and (kuma_controller.content is defined)
else
(kuma_vm.stdout | default('') | trim)
)
}}
when:
- (kuma_controller is defined and (kuma_controller.status|default(0))==200 and (kuma_controller.content is defined))
or ((kuma_vm.stdout | default('') | trim | length) > 0)
- name: Kuma | Print concise summary
ansible.builtin.debug:
msg: >-
Uptime Kuma homepage {{ 'reachable' if (kuma_home_html is defined) else 'NOT reachable' }}.
Source={{ 'controller' if ((kuma_controller is defined) and (kuma_controller.status|default(0))==200 and (kuma_controller.content is defined)) else 'vm' if (kuma_vm.stdout|default('')|trim|length>0) else 'n/a' }};
length={{ (kuma_home_html | default('')) | length }};
contains('Uptime Kuma')={{ (kuma_home_html is defined) and ('Uptime Kuma' in kuma_home_html) }}
when: DEBUG == 1
- name: Kuma | Homepage unavailable (after retries)
ansible.builtin.debug:
msg: "Kuma web není dostupná ani po pokusech."
when: kuma_home_html is not defined and DEBUG == 1
# Optional detailed dump (short excerpt only)
- name: Kuma | HTML excerpt (debug)
ansible.builtin.debug:
msg: "{{ (kuma_home_html | default(''))[:500] }}"
when: kuma_home_html is defined and DEBUG == 1
+26 -17
View File
@@ -1,15 +1,12 @@
# users-ssh-nopasswd.yml
---
- name: Ensure users, SSH keys, and passwordless sudo
hosts: all
become: true
become_user: root
become_method: sudo
vars:
users:
- name: automation
shell: /bin/bash
# optional extra groups besides sudo/wheel
groups: []
sudo_nopasswd: true
keys:
@@ -30,33 +27,43 @@
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPFS4fsqMjMMu/Bi/884bw7yJBqvWusDRESvanH6Owco jakub@jimbuntu"
tasks:
- name: Pick sudo group per distro
ansible.builtin.set_fact:
sudo_group: "{{ 'wheel' if ansible_facts.os_family in ['RedHat','Rocky','AlmaLinux','Fedora','OracleLinux','Suse'] else 'sudo' }}"
sudo_group: >-
{{ 'wheel'
if ansible_facts.os_family in
['RedHat','Rocky','AlmaLinux','Fedora','OracleLinux','Suse']
else 'sudo' }}
- name: Ensure user exists (creates home)
ansible.builtin.user:
name: "{{ item.name }}"
shell: "{{ item.shell | default('/bin/bash') }}"
shell: "{{ item.shell | default(omit) }}"
groups: >-
{{ (
(item.groups | default([]))
+ ([sudo_group] if item.sudo_nopasswd | default(false) else [])
) | unique | join(',') if
((item.groups | default([])) | length > 0) or (item.sudo_nopasswd | default(false))
else omit }}
(item.groups | default([]))
+ ([sudo_group] if item.sudo_nopasswd | default(false) else [])
) | unique | join(',')
if (
(item.groups | default([]) | length > 0)
or item.sudo_nopasswd | default(false)
)
else omit }}
append: true
create_home: true
state: present
loop: "{{ users }}"
- name: Install authorized SSH keys
- name: Enforce authorized SSH keys
ansible.builtin.authorized_key:
user: "{{ item.0.name }}"
key: "{{ item.1 }}"
user: "{{ item.name }}"
key: "{{ item.keys | join('\n') }}"
state: present
manage_dir: true
loop: "{{ users | subelements('keys', skip_missing=True) }}"
exclusive: true
loop: "{{ users }}"
when: item.keys is defined
- name: Grant passwordless sudo via sudoers.d
ansible.builtin.copy:
@@ -64,7 +71,9 @@
owner: root
group: root
mode: '0440'
content: "{{ item.name }} ALL=(ALL) NOPASSWD:ALL"
content: |
# Managed by Ansible
{{ item.name }} ALL=(ALL) NOPASSWD:ALL
validate: 'visudo -cf %s'
when: item.sudo_nopasswd | default(false)
loop: "{{ users }}"
loop: "{{ users }}"