Compare commits

..
11 Commits
44 changed files with 697 additions and 283 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
hosts hosts
proxmox.* proxmox.*
.vscode .vscode
inventory
.vault_pass .vault_pass
logs
+1 -4
View File
@@ -2,9 +2,6 @@
host_key_checking = False host_key_checking = False
roles_path = ./roles:/etc/ansible/roles roles_path = ./roles:/etc/ansible/roles
group_vars = ./group_vars group_vars = ./group_vars
forks = 20
strategy = free
[ssh_connection] [ssh_connection]
ssh_args = -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ControlMaster=auto -o ControlPersist=1800s ssh_args = -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null
pipelining = True
+1 -1
View File
@@ -1,6 +1,6 @@
- hosts: all - hosts: all
become: true become: true
remote_user: "{{ user|default('admin') }}" user: admin
tasks: tasks:
- name: Verify if system is Debian - name: Verify if system is Debian
debug: debug:
+1
View File
@@ -13,6 +13,7 @@ RUN apt update && apt install -y --no-install-recommends \
python3-pip \ python3-pip \
tzdata \ tzdata \
openssh-client \ openssh-client \
iputils-ping \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /etc/ssh/ssh_config.d \ RUN mkdir -p /etc/ssh/ssh_config.d \
Regular → Executable
View File
+37
View File
@@ -0,0 +1,37 @@
debian:
hosts:
rp:
ansible_host: 192.168.212.50
bitwarden:
ansible_host: 192.168.212.20
pdf:
ansible_host: 192.168.212.65
mailcow:
ansible_host: 192.168.212.70
plex:
ansible_host: 192.168.212.62
cloud:
ansible_host: 192.168.212.60
romm:
ansible_host: 192.168.212.64
gitea:
ansible_host: 192.168.212.30
ssh_service_name: ssh
ipam:
ansible_host: 192.168.212.56
ssh_service_name: ssh
teamspeck6:
ansible_host: 192.168.212.75
wiki:
ansible_host: 192.168.212.55
patchmon:
ansible_host: 192.168.212.80
einstein-dns:
ansible_host: 192.168.212.53
proxmox:
hosts:
dmc12:
ansible_host: 192.168.212.10
pbs:
ansible_host: 192.168.212.11
+10
View File
@@ -0,0 +1,10 @@
debian:
hosts:
healthcheck:
ansible_host: 2a01:4f8:1c1e:62dd::1
netbird:
ansible_host: 2a01:4f8:1c1c:8c8::1
teleport:
ansible_host: 2a01:4f8:1c19:c7d::1
podcast-linkstack:
ansible_host: 2a01:4f8:c17:de13::1
+8
View File
@@ -0,0 +1,8 @@
# Standardwerte, die überschrieben werden können
os_update_auto_upgrade: true
os_also_update_mirror: false
os_update_mirrors:
# Role needs two mirros to use for the sources.list.j2 Template
- "http://deb.debian.org/debian" # Enter a main mirror here (not security)
- "http://security.debian.org/debian-security" # Enter a security mirror here
os_update_version_codename: "trixie" # KEEP UNTOUCHED!! | Used for jinja2 Template fill in as it determines the current codename of system where ansible is run on
+11
View File
@@ -0,0 +1,11 @@
---
# SSH authorized keys for admin user
# Format: key and optional comment
admin_authorized_keys:
- key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL/XwF0Na+YH7lRqGtwEcyIMVGTQZetNDrC9sZ8ofjC5 niklas@Linkman-PC"
comment: "Niklas - Linkman-PC"
- key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA+EvtGavPlXfv7b00jSYsXX2+IEsqFWupEs6Rzf5z9q root@ansible"
comment: "Generic Ansible Key"
# Admin user password (hashed with SHA-512)
admin_password: "$6$NmIxM3ZbJDsbC1E6$BO8ag1ZyBqbhELjk0ppKV0CLnYXhHDp9oZc.jmkc62N9hjwbXihF1FmvsYgMKKINVcwaE73u2dRO8pnE6yEGH/"
+1
View File
@@ -0,0 +1 @@
monitoring_zabbix_passive_servers: ["10.13.37.1"]
+20
View File
@@ -0,0 +1,20 @@
home:
children:
debian:
hosts:
rp:
ansible_host: 10.13.37.11
paperless:
ansible_host: 10.13.37.30
homeassistant:
ansible_host: 10.13.37.15
teleport-jumper:
ansible_host: 10.13.37.50
idris-dns:
ansible_host: 10.13.37.53
it-tools:
ansible_host: 10.13.37.16
proxmox:
hosts:
tardis:
ansible_host: 10.13.37.5
+5
View File
@@ -16,3 +16,8 @@
include_role: include_role:
name: os-updates name: os-updates
when: ansible_facts['os_family'] == "Debian" when: ansible_facts['os_family'] == "Debian"
- name: Include healthcheck role (post-update quality gate)
include_role:
name: healthcheck
when: ansible_facts['os_family'] == "Debian"
+18
View File
@@ -0,0 +1,18 @@
- hosts: all
user: admin
become: true
tasks:
- name: Verify if system is Debian
debug:
msg: "This playbook is running on a Debian system."
when: ansible_facts['os_family'] == "Debian"
- name: Stop playbook if system is not Debian
fail:
msg: "This playbook only supports Debian."
when: ansible_facts['os_family'] != "Debian"
- name: Include hawser role
import_role:
name: hawser
when: ansible_facts['os_family'] == "Debian"
+8
View File
@@ -22,3 +22,11 @@
- import_tasks: configure-sysctl.yml - import_tasks: configure-sysctl.yml
tags: sysctl tags: sysctl
- import_role:
name: docker
when: not skip_docker | default(false)
- import_role:
name: monitoring
when: not skip_monitoring | default(false)
+1 -1
View File
@@ -1,5 +1,5 @@
docker_mirror: https://download.docker.com/linux/debian docker_mirror: https://download.docker.com/linux/debian
os_version_codename: "{{ ansible_facts['distribution_release'] | lower }}" os_version_codename: "{{ ansible_lsb.codename }}"
docker_packages: docker_packages:
- docker-ce - docker-ce
- docker-ce-cli - docker-ce-cli
-4
View File
@@ -1,6 +1,2 @@
- name: Include Docker installation tasks - name: Include Docker installation tasks
ansible.builtin.include_tasks: install-docker.yml ansible.builtin.include_tasks: install-docker.yml
when: docker_check is failed
- name: Include Docker APT migration tasks
ansible.builtin.include_tasks: migrate-apt.yml
-23
View File
@@ -1,23 +0,0 @@
- name: Remove old Docker APT repo list files (legacy deb format)
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /etc/apt/sources.list.d/docker.list
- /etc/apt/sources.list.d/docker-ce.list
- /etc/apt/sources.list.d/download_docker_com_linux_ubuntu.list
register: docker_apt_sources_cleanup
failed_when: false
- name: Remove legacy Docker repo entries from /etc/apt/sources.list
ansible.builtin.lineinfile:
path: /etc/apt/sources.list
regexp: '^deb\s+.*download\.docker\.com/linux/.*$'
state: absent
register: docker_sources_list_cleanup
failed_when: false
- name: Update apt cache after legacy Docker repo cleanup
ansible.builtin.apt:
update_cache: true
when: docker_apt_sources_cleanup is changed or docker_sources_list_cleanup is changed
+15
View File
@@ -0,0 +1,15 @@
# Name des Docker Compose Projekts
hawser_project_name: hawser
# Verzeichnis, in dem die Compose-Datei auf den Zielsystemen liegt
hawser_compose_dir: /opt/hawser
# Freigegebener Port des Hawser-Agents
hawser_port: "2376"
# Name des externen Docker Volumes für die Stack-Dateien
hawser_stacks_volume: hawser_stacks
# ALLOW_INSECURE_NO_AUTH=true erlaubt Standard-Mode ohne Token auf nicht-loopback Adressen.
# Nur setzen, wenn das Netzwerk durch andere Maßnahmen abgesichert ist.
hawser_allow_insecure_no_auth: true
+2
View File
@@ -0,0 +1,2 @@
---
# Keine Handler erforderlich – docker compose up -d ersetzt laufende Container eigenständig
+2
View File
@@ -0,0 +1,2 @@
- name: Update Hawser Docker stack
ansible.builtin.import_tasks: update-stack.yml
+31
View File
@@ -0,0 +1,31 @@
- name: Gather Docker host information
community.docker.docker_host_info:
register: hawser_docker_info
ignore_errors: true
- name: Check if Hawser compose file is present
ansible.builtin.stat:
path: "{{ hawser_compose_dir }}/docker-compose.yml"
register: hawser_compose_file
when: hawser_docker_info is not failed
- name: Deploy Hawser compose file from template
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ hawser_compose_dir }}/docker-compose.yml"
owner: root
group: root
mode: "0640"
when:
- hawser_docker_info is not failed
- hawser_compose_file.stat.exists
- name: Pull latest images and recreate Hawser stack
community.docker.docker_compose_v2:
project_src: "{{ hawser_compose_dir }}"
project_name: "{{ hawser_project_name }}"
state: present
pull: always
when:
- hawser_docker_info is not failed
- hawser_compose_file.stat.exists
@@ -0,0 +1,17 @@
services:
hawser:
container_name: hawser
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- {{ hawser_stacks_volume }}:/data/stacks
ports:
- "{{ hawser_port }}:2376"
environment:
- ALLOW_INSECURE_NO_AUTH={{ hawser_allow_insecure_no_auth | string | lower }}
image: ghcr.io/finsys/hawser:latest
restart: always
volumes:
{{ hawser_stacks_volume }}:
external: true
name: {{ hawser_stacks_volume }}
+11
View File
@@ -0,0 +1,11 @@
# Post-update health check (quality gate) defaults.
# The role verifies that every Docker container is running after an OS update
# and records the result in the same update log used by the os-updates role.
healthcheck_logging_enabled: "{{ os_update_logging_enabled | default(true) }}"
healthcheck_log_dir: "{{ os_update_log_dir | default('/ansible/logs') }}"
healthcheck_log_inventory: "{{ inventory_file | basename | splitext | first }}"
healthcheck_log_file: "{{ healthcheck_log_dir }}/{{ healthcheck_log_inventory }}/{{ ansible_facts['hostname'] }}/update.log"
# When true, the role fails the playbook if any container is not running.
healthcheck_fail_on_unhealthy: false
+85
View File
@@ -0,0 +1,85 @@
# Post-update health check (quality gate).
# Detects whether Docker is installed and, if so, verifies that every
# container is in the running state. Results are appended to the update
# log as a "quality_gate" section, tied to the current update run via the
# os-updates preflight start timestamp.
- name: Healthcheck - Detect Docker binary
ansible.builtin.command: which docker
register: healthcheck_docker_bin
changed_when: false
failed_when: false
- name: Healthcheck - Record Docker presence
ansible.builtin.set_fact:
healthcheck_docker_installed: "{{ healthcheck_docker_bin.rc == 0 }}"
- name: Healthcheck - List all container names
ansible.builtin.command: "docker ps -a --format {{ '{{' }}.Names{{ '}}' }}"
register: healthcheck_all_containers
changed_when: false
when: healthcheck_docker_installed | bool
- name: Healthcheck - List running container names
ansible.builtin.command: "docker ps --filter status=running --format {{ '{{' }}.Names{{ '}}' }}"
register: healthcheck_running_containers
changed_when: false
when: healthcheck_docker_installed | bool
- name: Healthcheck - Evaluate container health
ansible.builtin.set_fact:
healthcheck_total_containers: "{{ healthcheck_all_containers.stdout_lines | length }}"
healthcheck_running_count: "{{ healthcheck_running_containers.stdout_lines | length }}"
healthcheck_non_running_containers: "{{ (healthcheck_all_containers.stdout_lines | difference(healthcheck_running_containers.stdout_lines)) | list }}"
when: healthcheck_docker_installed | bool
- name: Healthcheck - Determine overall result
ansible.builtin.set_fact:
healthcheck_all_running: "{{ (healthcheck_non_running_containers | length) == 0 }}"
healthcheck_passed: "{{ (healthcheck_non_running_containers | length) == 0 }}"
when: healthcheck_docker_installed | bool
- name: Healthcheck - Default result when Docker is not installed
ansible.builtin.set_fact:
healthcheck_total_containers: 0
healthcheck_running_count: 0
healthcheck_non_running_containers: []
healthcheck_all_running: true
healthcheck_passed: true
when: not (healthcheck_docker_installed | bool)
- name: Healthcheck - Ensure log directory exists
ansible.builtin.file:
path: "{{ healthcheck_log_file | dirname }}"
state: directory
delegate_to: localhost
when: healthcheck_logging_enabled | bool
- name: Healthcheck - Write quality gate section to update log
ansible.builtin.blockinfile:
path: "{{ healthcheck_log_file }}"
create: yes
marker: "# {mark} ANSIBLE-HEALTHCHECK"
block: |
quality_gate:
docker_installed: {{ healthcheck_docker_installed }}
total_containers: {{ healthcheck_total_containers }}
running_containers: {{ healthcheck_running_count }}
all_running: {{ healthcheck_all_running }}
non_running_containers:
{% for c in healthcheck_non_running_containers %}
- {{ c }}
{% endfor %}
healthcheck_passed: {{ healthcheck_passed }}
delegate_to: localhost
when: healthcheck_logging_enabled | bool
- name: Healthcheck - Fail when containers are unhealthy (quality gate)
ansible.builtin.fail:
msg: >-
Quality gate failed: the following containers are not running:
{{ healthcheck_non_running_containers | join(', ') }}
when:
- healthcheck_fail_on_unhealthy | bool
- healthcheck_docker_installed | bool
- not (healthcheck_all_running | bool)
+7 -1
View File
@@ -1,11 +1,17 @@
# Standardwerte, die überschrieben werden können # Standardwerte, die überschrieben werden können
os_update_auto_upgrade: true os_update_auto_upgrade: true
os_also_update_mirror: false os_also_update_mirror: false
# Logging of OS update runs (preflight package count/list, updated packages, duration)
os_update_logging_enabled: true
os_update_log_dir: /ansible/logs
os_update_log_inventory: "{{ inventory_file | basename | splitext | first }}"
os_update_log_file: "{{ os_update_log_dir }}/{{ os_update_log_inventory }}/{{ ansible_facts['hostname'] }}/update.log"
os_update_mirrors: os_update_mirrors:
# Role needs two mirros to use for the sources.list.j2 Template # Role needs two mirros to use for the sources.list.j2 Template
- "http://deb.debian.org/debian" # Enter a main mirror here (not security) - "http://deb.debian.org/debian" # Enter a main mirror here (not security)
- "http://security.debian.org/debian-security" # Enter a security mirror here - "http://security.debian.org/debian-security" # Enter a security mirror here
os_update_version_codename: "{{ ansible_distribution_release }}" # KEEP UNTOUCHED!! | Used for jinja2 Template fill in as it determines the current codename of system where ansible is run on os_update_version_codename: "{{ ansible_facts['distribution_release'] }}" # KEEP UNTOUCHED!! | Used for jinja2 Template fill in as it determines the current codename of system where ansible is run on
os_update_debian_codenames: os_update_debian_codenames:
# Only these suites are considered Debian codenames and will be rewritten in sources.list.d # Only these suites are considered Debian codenames and will be rewritten in sources.list.d
- trixie - trixie
+3 -6
View File
@@ -3,9 +3,6 @@
clean: yes clean: yes
autoclean: yes autoclean: yes
- name: Reboot system - name: apt autoremove
command: /sbin/reboot apt:
async: 1 autoremove: yes
poll: 0
ignore_errors: true
when: reboot_required == "yes"
@@ -0,0 +1,46 @@
# Postflight logging: which packages were updated and how long it took.
# Run logging steps only when os_update_logging_enabled is true.
- name: Logging - Record end epoch
command: date +%s
register: os_update_log_end_ts
changed_when: false
when: os_update_logging_enabled | bool
- name: Logging - Compute upgrade duration
set_fact:
os_update_duration_seconds: "{{ (os_update_log_end_ts.stdout | int) - (os_update_log_start_ts.stdout | int) }}"
when: os_update_logging_enabled | bool
- name: Logging - Ensure log directory exists
file:
path: "{{ os_update_log_file | dirname }}"
state: directory
delegate_to: localhost
when: os_update_logging_enabled | bool
- name: Logging - Write update log entry
blockinfile:
path: "{{ os_update_log_file }}"
create: yes
marker: "# {mark} ANSIBLE-OS-UPDATE"
block: |
host: {{ ansible_facts['hostname'] }}
started: {{ os_update_log_start_iso.stdout }}
duration_seconds: {{ os_update_duration_seconds }}
upgradable_packages_count: {{ os_update_upgradable_count }}
upgrade_changed: {{ os_update_upgrade_result.changed | default(false) }}
upgrade_failed: {{ os_update_upgrade_result.failed | default(false) }}
upgradable_packages:
{% for ver in os_update_upgradable_versions %}
- {{ ver.split()[0] }} ({{ ver.split()[1] }} -> {{ ver.split()[2] }})
{% endfor %}
reboot_required: {{ os_update_reboot_required }}
reboot_reason: {{ os_update_reboot_reason }}
reboot_triggered: {{ os_update_reboot_triggered }}
reboot_started: {{ os_update_reboot_start_iso }}
reboot_downtime_seconds: {{ os_update_reboot_downtime_seconds }}
kernel_before: {{ os_update_running_kernel.stdout | default('n/a') }}
kernel_after: {{ os_update_running_kernel_after }}
delegate_to: localhost
when: os_update_logging_enabled | bool
@@ -0,0 +1,48 @@
# Preflight logging: how many and which packages need to be updated.
# Run logging steps only when os_update_logging_enabled is true.
- name: Logging - Refresh apt cache for accurate preflight
apt:
update_cache: yes
changed_when: false
when: os_update_logging_enabled | bool
- name: Logging - Record start epoch
command: date +%s
register: os_update_log_start_ts
changed_when: false
when: os_update_logging_enabled | bool
- name: Logging - Record start ISO time
command: date -Iseconds
register: os_update_log_start_iso
changed_when: false
when: os_update_logging_enabled | bool
- name: Preflight - Gather upgradable packages
command: apt list --upgradable
register: os_update_upgradable
changed_when: false
when: os_update_logging_enabled | bool
- name: Preflight - Build list of upgradable package names, versions and count
set_fact:
os_update_upgradable_names: >-
{{
os_update_upgradable.stdout_lines
| select('search', '\[upgradable')
| map('regex_replace', '^([^/]+)/.*$', '\1')
| list
}}
os_update_upgradable_versions: >-
{{
os_update_upgradable.stdout_lines
| select('search', '\[upgradable')
| map('regex_replace', '^([^/]+)/\S+\s+(\S+)\s+\S+\s+\[upgradable from: (\S+)\]$', '\1 \3 \2')
| list
}}
os_update_upgradable_count: >-
{{
(os_update_upgradable.stdout_lines | select('search', '\[upgradable') | list | length) | int
}}
when: os_update_logging_enabled | bool
+83
View File
@@ -0,0 +1,83 @@
# Reboot decision and execution.
# Determines whether a reboot is needed because a new kernel was installed,
# performs the reboot asynchronously, waits for the host to come back, and
# records timing facts for the update log.
- name: Gather running kernel
command: uname -r
register: os_update_running_kernel
changed_when: false
when: ansible_facts['virtualization_type'] != 'lxc'
- name: Gather latest installed kernel
shell: |
dpkg -l | grep -E '^ii' | grep 'linux-image-[0-9]' | awk '{print $2}' | sort | tail -n 1
register: os_update_latest_kernel
changed_when: false
failed_when: false
when: ansible_facts['virtualization_type'] != 'lxc'
- name: Determine reboot decision
set_fact:
os_update_reboot_required: true
os_update_reboot_reason: "new kernel installed ({{ os_update_running_kernel.stdout }} -> {{ os_update_latest_kernel.stdout | regex_replace('^linux-image-', '') }})"
when:
- ansible_facts['virtualization_type'] != 'lxc'
- os_update_latest_kernel.stdout | length > 0
- os_update_running_kernel.stdout not in os_update_latest_kernel.stdout
- name: Default reboot decision to false
set_fact:
os_update_reboot_required: "{{ os_update_reboot_required | default(false) }}"
os_update_reboot_reason: "{{ os_update_reboot_reason | default('none') }}"
- name: Default reboot timing facts
set_fact:
os_update_reboot_triggered: false
os_update_reboot_start_iso: "n/a"
os_update_reboot_downtime_seconds: 0
os_update_running_kernel_after: "{{ os_update_running_kernel.stdout | default('n/a') }}"
- block:
- name: Reboot - Record start time
command: date -Iseconds
register: os_update_reboot_start_iso_raw
changed_when: false
- name: Reboot - Record start epoch
command: date +%s
register: os_update_reboot_start_ts
changed_when: false
- name: Reboot - Reboot system
command: /sbin/reboot
async: 1
poll: 0
ignore_errors: true
- name: Reboot - Wait for host to come back online
wait_for_connection:
delay: 10
timeout: 600
register: os_update_reboot_wait
- name: Reboot - Record end epoch
command: date +%s
register: os_update_reboot_end_ts
changed_when: false
- name: Reboot - Compute downtime
set_fact:
os_update_reboot_downtime_seconds: "{{ (os_update_reboot_end_ts.stdout | int) - (os_update_reboot_start_ts.stdout | int) }}"
- name: Reboot - Gather running kernel after reboot
command: uname -r
register: os_update_running_kernel_after_raw
changed_when: false
- name: Reboot - Record reboot facts
set_fact:
os_update_reboot_triggered: true
os_update_reboot_start_iso: "{{ os_update_reboot_start_iso_raw.stdout }}"
os_update_running_kernel_after: "{{ os_update_running_kernel_after_raw.stdout }}"
when: os_update_reboot_required | bool
+9 -22
View File
@@ -1,30 +1,17 @@
- name: Run preflight logging
include_tasks: logging_preflight.yml
- name: Upgrade all installed packages - name: Upgrade all installed packages
apt: apt:
upgrade: full upgrade: full
update_cache: yes update_cache: yes
register: os_update_upgrade_result
notify: notify:
- apt cleanup - apt cleanup
- apt autoremove
- name: Check if a kernel update is available - name: Run reboot decision and reboot if required
shell: | include_tasks: reboot.yml
dpkg -l | grep -E '^ii' | grep 'linux-image-[0-9]' | awk '{print $2}' | sort | tail -n 1
register: latest_kernel
when: ansible_virtualization_type != 'lxc'
- name: Check if running kernel matches the latest installed kernel - name: Run postflight logging
shell: | include_tasks: logging_postflight.yml
echo "{{ latest_kernel.stdout }}" | grep -c $(uname -r)
register: kernel_match
changed_when: false
failed_when: false
when: ansible_facts['virtualization_type'] != 'lxc'
- name: Mark reboot required if a new kernel is installed
set_fact:
reboot_required: "yes"
changed_when: true
notify:
- Reboot system
when:
- ansible_facts['virtualization_type'] != 'lxc'
- (kernel_match.stdout | int) == 0
-5
View File
@@ -1,5 +0,0 @@
export PROXMOX_URL=https://192.168.212.10:8006/
export PROXMOX_USERNAME=ansible@pve
export PROXMOX_PASSWORD=u0H1GQMsAuSYZJ
export PROXMOX_INVALID_CERT=False
export EXCLUDE_LIST="docker.*;br-*;veth*"