v1.0.0
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ssh_keys/
|
||||||
|
patchmon_inventory.yml
|
||||||
12
ansible.cfg
Normal file
12
ansible.cfg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[defaults]
|
||||||
|
inventory = patchmon_inventory.yml
|
||||||
|
localhost_warning = False
|
||||||
|
interpreter_python = auto_silent
|
||||||
|
timeout = 30
|
||||||
|
|
||||||
|
[ssh_connection]
|
||||||
|
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o ConnectTimeout=30 -o ConnectionAttempts=5
|
||||||
|
|
||||||
|
[inventory]
|
||||||
|
enable_plugins = patchmon.dynamic_inventory.dynamic_inventory
|
||||||
|
|
||||||
11
cron/ansible-autopilot.logrotate
Normal file
11
cron/ansible-autopilot.logrotate
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Przykładowa konfiguracja logrotate dla logu bezobsługowych przebiegów autopilot.
|
||||||
|
# Instalacja: sudo cp cron/ansible-autopilot.logrotate /etc/logrotate.d/ansible-autopilot
|
||||||
|
/var/log/ansible-autopilot.log {
|
||||||
|
weekly
|
||||||
|
rotate 8
|
||||||
|
compress
|
||||||
|
delaycompress
|
||||||
|
missingok
|
||||||
|
notifempty
|
||||||
|
copytruncate
|
||||||
|
}
|
||||||
16
cron/crontab.example
Normal file
16
cron/crontab.example
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Przykładowy wpis crontab dla bezobsługowego zarządzania flotą przez PatchMon + Ansible.
|
||||||
|
#
|
||||||
|
# Instalacja:
|
||||||
|
# crontab -e
|
||||||
|
# (wklej poniższy wiersz, dostosuj godzinę)
|
||||||
|
#
|
||||||
|
# albo jako plik w /etc/cron.d/ (wtedy trzeba dodać pole z użytkownikiem, np. "root"):
|
||||||
|
# sudo cp crontab.example /etc/cron.d/ansible-autopilot
|
||||||
|
#
|
||||||
|
# Cotygodniowo, w niedzielę o 03:30: aktualizacja pakietów + sprzątanie + raport floty.
|
||||||
|
# Log trafia do /var/log/ansible-autopilot.log (patrz cron/ansible-autopilot.logrotate).
|
||||||
|
30 3 * * 0 /root/ansible/cron/run-autopilot.sh
|
||||||
|
|
||||||
|
# Opcjonalnie: sam raport stanu floty (bez aktualizacji) codziennie o 07:00,
|
||||||
|
# żeby mieć świeże dane o dysku/reboot-required do porannego przeglądu.
|
||||||
|
# 0 7 * * * cd /root/ansible && /usr/bin/ansible-playbook -i patchmon_inventory.yml playbooks/playbook_fleet_report.yml --limit "Update-now,localhost" >> /var/log/ansible-autopilot.log 2>&1
|
||||||
32
cron/run-autopilot.sh
Executable file
32
cron/run-autopilot.sh
Executable file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Wrapper do bezobsługowego uruchamiania playbook_autopilot.yml z crona.
|
||||||
|
# - flock zapobiega nakładającym się uruchomieniom (np. jeśli poprzedni przebieg
|
||||||
|
# jeszcze trwa, gdy odpala się kolejny).
|
||||||
|
# - Wynik (stdout+stderr) trafia z timestampem do LOG_FILE.
|
||||||
|
# - Reboot NIE jest wykonywany automatycznie — playbook_fleet_report.yml tylko
|
||||||
|
# zgłasza, które hosty go wymagają (patrz reports/).
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
ANSIBLE_DIR="/root/ansible"
|
||||||
|
LOCK_FILE="/var/run/ansible-autopilot.lock"
|
||||||
|
LOG_FILE="/var/log/ansible-autopilot.log"
|
||||||
|
TARGET_GROUP="${TARGET_GROUP:-Update-now}"
|
||||||
|
|
||||||
|
exec 200>"${LOCK_FILE}"
|
||||||
|
if ! flock -n 200; then
|
||||||
|
echo "$(date '+%Y-%m-%d %H:%M:%S') [run-autopilot] Poprzedni przebieg wciąż trwa, pomijam." >>"${LOG_FILE}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "${ANSIBLE_DIR}"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "===== $(date '+%Y-%m-%d %H:%M:%S') [run-autopilot] START (target: ${TARGET_GROUP}) ====="
|
||||||
|
ansible-playbook -i patchmon_inventory.yml \
|
||||||
|
playbooks/playbook_autopilot.yml \
|
||||||
|
--limit "${TARGET_GROUP},localhost"
|
||||||
|
rc=$?
|
||||||
|
echo "===== $(date '+%Y-%m-%d %H:%M:%S') [run-autopilot] KONIEC (exit code: ${rc}) ====="
|
||||||
|
} >>"${LOG_FILE}" 2>&1
|
||||||
|
|
||||||
|
exit "${rc}"
|
||||||
15
group_vars/all.yml
Normal file
15
group_vars/all.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
# Wspólne ustawienia połączenia dla wszystkich hostów z inwentarza PatchMon.
|
||||||
|
# playbook_dir sprawia, że ścieżka do klucza działa niezależnie od katalogu,
|
||||||
|
# z którego wywołujemy ansible-playbook.
|
||||||
|
ansible_ssh_private_key_file: "{{ playbook_dir }}/../ssh_keys/id_rsa"
|
||||||
|
ansible_ssh_common_args: '-o StrictHostKeyChecking=no'
|
||||||
|
ansible_become: true
|
||||||
|
ansible_become_method: sudo
|
||||||
|
|
||||||
|
# Klucze publiczne wdrażane przez playbooks/playbook_ssh_keys.yml.
|
||||||
|
# Uzupełnij o właściwe klucze przed użyciem tego playbooka.
|
||||||
|
admin_ssh_keys: []
|
||||||
|
|
||||||
|
# Próg (%) zajętości dysku, po którym playbooks/playbook_cleanup.yml zgłasza ostrzeżenie.
|
||||||
|
disk_usage_warn_percent: 85
|
||||||
3
group_vars/mgmt.yml
Normal file
3
group_vars/mgmt.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
# Hosty w grupie PatchMon "mgmt" logują się kontem mgmt i podnoszą uprawnienia przez sudo.
|
||||||
|
ansible_user: mgmt
|
||||||
3
group_vars/root.yml
Normal file
3
group_vars/root.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
# Hosty w grupie PatchMon "root" logują się bezpośrednio jako root.
|
||||||
|
ansible_user: root
|
||||||
6
patchmon_inventory.yml.example
Normal file
6
patchmon_inventory.yml.example
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
plugin: patchmon.dynamic_inventory.dynamic_inventory
|
||||||
|
api_url: http://patchmon.example.local/api/v1/api/hosts
|
||||||
|
api_key: CHANGE_ME_API_KEY
|
||||||
|
api_secret: CHANGE_ME_API_SECRET
|
||||||
|
verify_ssl: false
|
||||||
22
playbooks/playbook_autopilot.yml
Normal file
22
playbooks/playbook_autopilot.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
# Playbook do bezobsługowego uruchamiania z crona (patrz cron/run-autopilot.sh).
|
||||||
|
# Kolejność: aktualizacja pakietów -> sprzątanie/kondycja -> raport floty.
|
||||||
|
#
|
||||||
|
# Świadomie NIE zawiera:
|
||||||
|
# - playbook_security_baseline.yml (zmiany w sshd/unattended-upgrades — uruchamiać ręcznie)
|
||||||
|
# - restartu hostów (reboot jest tylko raportowany w fleet_report, decyzję
|
||||||
|
# o restarcie podejmuje operator ręcznie)
|
||||||
|
#
|
||||||
|
# Użycie: ansible-playbook -i patchmon_inventory.yml playbooks/playbook_autopilot.yml --limit "Update-now,localhost"
|
||||||
|
#
|
||||||
|
# WAŻNE: ",localhost" w --limit jest wymagane — playbook_fleet_report.yml ma
|
||||||
|
# drugą fazę na hosts: localhost (renderowanie raportu), a --limit bez "localhost"
|
||||||
|
# w wzorcu powoduje jej pominięcie ("skipping: no hosts matched").
|
||||||
|
- name: Aktualizacja pakietów
|
||||||
|
import_playbook: playbook_upgrade.yml
|
||||||
|
|
||||||
|
- name: Sprzątanie i kondycja systemu
|
||||||
|
import_playbook: playbook_cleanup.yml
|
||||||
|
|
||||||
|
- name: Raport stanu floty
|
||||||
|
import_playbook: playbook_fleet_report.yml
|
||||||
46
playbooks/playbook_cleanup.yml
Normal file
46
playbooks/playbook_cleanup.yml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
# Rutynowe sprzątanie/kondycja systemu: apt autoremove/clean, czyszczenie journala,
|
||||||
|
# sprawdzenie zajętości dysku z progiem ostrzegawczym (domyślnie 85%, group_vars/all.yml).
|
||||||
|
- name: Sprzątanie i kondycja systemu
|
||||||
|
hosts: all
|
||||||
|
gather_facts: true
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Usuń nieużywane pakiety (apt autoremove)
|
||||||
|
ansible.builtin.apt:
|
||||||
|
autoremove: yes
|
||||||
|
purge: yes
|
||||||
|
when: ansible_os_family in ["Debian", "Ubuntu"]
|
||||||
|
|
||||||
|
- name: Wyczyść lokalny cache pobranych pakietów (apt clean)
|
||||||
|
ansible.builtin.apt:
|
||||||
|
autoclean: yes
|
||||||
|
when: ansible_os_family in ["Debian", "Ubuntu"]
|
||||||
|
|
||||||
|
- name: Wyczyść dziennik systemowy starszy niż 7 dni (journalctl --vacuum-time)
|
||||||
|
ansible.builtin.command: journalctl --vacuum-time=7d
|
||||||
|
register: journal_vacuum
|
||||||
|
changed_when: "'Vacuuming done' in journal_vacuum.stdout and 'freed' in journal_vacuum.stdout"
|
||||||
|
when: ansible_service_mgr == "systemd"
|
||||||
|
|
||||||
|
- name: Odśwież fakty o zajętości dysku (po sprzątaniu)
|
||||||
|
ansible.builtin.setup:
|
||||||
|
filter: ansible_mounts
|
||||||
|
gather_timeout: 30
|
||||||
|
|
||||||
|
- name: Sprawdź zajętość dysku na partycji root
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
root_disk_percent_used: >-
|
||||||
|
{{ (( (mount.size_total - mount.size_available) / mount.size_total * 100) | round(0, 'ceil')) | int }}
|
||||||
|
vars:
|
||||||
|
mount: "{{ ansible_mounts | selectattr('mount', 'equalto', '/') | first }}"
|
||||||
|
when: ansible_mounts | selectattr('mount', 'equalto', '/') | list | length > 0
|
||||||
|
|
||||||
|
- name: OSTRZEŻENIE — zajętość dysku powyżej progu
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
WARN [{{ inventory_hostname }}]: dysk / zajęty w {{ root_disk_percent_used }}%
|
||||||
|
(próg: {{ disk_usage_warn_percent }}%)
|
||||||
|
when:
|
||||||
|
- root_disk_percent_used is defined
|
||||||
|
- root_disk_percent_used | int >= disk_usage_warn_percent | int
|
||||||
56
playbooks/playbook_docker_cleanup.yml
Normal file
56
playbooks/playbook_docker_cleanup.yml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
# Czyszczenie nieużywanych obrazów Docker — tylko na hostach, na których Docker
|
||||||
|
# jest faktycznie zainstalowany (pozostałe hosty są bezpiecznie pomijane).
|
||||||
|
#
|
||||||
|
# Domyślnie usuwa wyłącznie obrazy "dangling" (bez tagu) — bezpieczne, nie rusza
|
||||||
|
# tagowanych obrazów, nawet jeśli akurat nie są używane przez żaden kontener.
|
||||||
|
#
|
||||||
|
# Aby usunąć WSZYSTKIE nieużywane obrazy (także tagowane, nieużywane przez żaden
|
||||||
|
# kontener) — więcej odzyskanego miejsca, ale bardziej agresywne — uruchom z:
|
||||||
|
# -e "docker_prune_all_images=true"
|
||||||
|
- name: Czyszczenie nieużywanych obrazów Docker
|
||||||
|
hosts: all
|
||||||
|
gather_facts: false
|
||||||
|
vars:
|
||||||
|
docker_prune_all_images: false
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Sprawdź, czy docker jest zainstalowany
|
||||||
|
ansible.builtin.command: which docker
|
||||||
|
register: docker_check
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Pomiń — Docker nie jest zainstalowany na tym hoście
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "Docker nie jest zainstalowany na {{ inventory_hostname }} — pomijam."
|
||||||
|
when: docker_check.rc != 0
|
||||||
|
|
||||||
|
- name: Zajętość miejsca przez Dockera przed czyszczeniem
|
||||||
|
ansible.builtin.command: docker system df
|
||||||
|
register: docker_df_before
|
||||||
|
changed_when: false
|
||||||
|
when: docker_check.rc == 0
|
||||||
|
|
||||||
|
- name: Usuń nieużywane obrazy Docker (dangling)
|
||||||
|
ansible.builtin.command: docker image prune -f
|
||||||
|
register: prune_result
|
||||||
|
changed_when: "'Total reclaimed space: 0B' not in prune_result.stdout"
|
||||||
|
when:
|
||||||
|
- docker_check.rc == 0
|
||||||
|
- not docker_prune_all_images
|
||||||
|
|
||||||
|
- name: Usuń WSZYSTKIE nieużywane obrazy Docker (także tagowane)
|
||||||
|
ansible.builtin.command: docker image prune -a -f
|
||||||
|
register: prune_result_all
|
||||||
|
changed_when: "'Total reclaimed space: 0B' not in prune_result_all.stdout"
|
||||||
|
when:
|
||||||
|
- docker_check.rc == 0
|
||||||
|
- docker_prune_all_images
|
||||||
|
|
||||||
|
- name: Wynik czyszczenia obrazów
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
{{ (prune_result_all if docker_prune_all_images else prune_result).stdout_lines
|
||||||
|
| default(['brak danych']) }}
|
||||||
|
when: docker_check.rc == 0
|
||||||
66
playbooks/playbook_fleet_report.yml
Normal file
66
playbooks/playbook_fleet_report.yml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
# Raport stanu floty (tylko odczyt, bez żadnych zmian na hostach):
|
||||||
|
# OS/wersja, uptime, zajętość dysku, liczba oczekujących aktualizacji
|
||||||
|
# oraz flaga reboot-required (decyzję o restarcie podejmuje operator ręcznie).
|
||||||
|
#
|
||||||
|
# WAŻNE: druga faza (renderowanie raportu) działa na hosts: localhost — jeśli
|
||||||
|
# używasz --limit, dopisz do niego ",localhost", inaczej ta faza zostanie pominięta,
|
||||||
|
# np.: --limit "Update-now,localhost"
|
||||||
|
- name: Zbierz stan floty
|
||||||
|
hosts: all
|
||||||
|
gather_facts: true
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Sprawdź czy wymagany jest restart (reboot-required)
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: /var/run/reboot-required
|
||||||
|
register: reboot_required_stat
|
||||||
|
|
||||||
|
- name: Policz pakiety oczekujące na aktualizację
|
||||||
|
ansible.builtin.command: apt list --upgradable
|
||||||
|
register: upgradable_raw
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
when: ansible_os_family in ["Debian", "Ubuntu"]
|
||||||
|
|
||||||
|
- name: Sprawdź zajętość dysku na partycji root
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
root_disk_percent_used: >-
|
||||||
|
{{ (( (mount.size_total - mount.size_available) / mount.size_total * 100) | round(0, 'ceil')) | int }}
|
||||||
|
vars:
|
||||||
|
mount: "{{ ansible_mounts | selectattr('mount', 'equalto', '/') | first }}"
|
||||||
|
when: ansible_mounts | selectattr('mount', 'equalto', '/') | list | length > 0
|
||||||
|
|
||||||
|
- name: Zbuduj podsumowanie hosta
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
host_report:
|
||||||
|
os: "{{ ansible_distribution }} {{ ansible_distribution_version }}"
|
||||||
|
uptime_days: "{{ (ansible_uptime_seconds / 86400) | round(1) }}"
|
||||||
|
disk_root_percent: "{{ root_disk_percent_used | default('n/a') }}"
|
||||||
|
upgradable_packages: >-
|
||||||
|
{{ (upgradable_raw.stdout_lines | default([]) | length - 1)
|
||||||
|
if (upgradable_raw is defined and upgradable_raw.stdout_lines is defined and upgradable_raw.stdout_lines | length > 0)
|
||||||
|
else 0 }}
|
||||||
|
reboot_required: "{{ reboot_required_stat.stat.exists }}"
|
||||||
|
|
||||||
|
- name: Wygeneruj raport floty
|
||||||
|
hosts: localhost
|
||||||
|
connection: local
|
||||||
|
gather_facts: true
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Upewnij się, że katalog reports/ istnieje
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ playbook_dir }}/../reports"
|
||||||
|
state: directory
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Renderuj raport floty
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: "{{ playbook_dir }}/../templates/fleet_report.j2"
|
||||||
|
dest: "{{ playbook_dir }}/../reports/fleet_report_{{ ansible_date_time.iso8601_basic_short }}.txt"
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Ścieżka wygenerowanego raportu
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "Raport zapisany w reports/fleet_report_{{ ansible_date_time.iso8601_basic_short }}.txt"
|
||||||
64
playbooks/playbook_security_baseline.yml
Normal file
64
playbooks/playbook_security_baseline.yml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
# Baza bezpieczeństwa: unattended-upgrades (tylko aktualizacje bezpieczeństwa,
|
||||||
|
# bez automatycznego restartu — patrz playbook_fleet_report.yml dla flagi reboot-required)
|
||||||
|
# oraz wyłączenie logowania hasłem po SSH.
|
||||||
|
- name: Baza bezpieczeństwa (unattended-upgrades + wyłączenie logowania hasłem)
|
||||||
|
hosts: all
|
||||||
|
gather_facts: true
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Zainstaluj unattended-upgrades (Debian/Ubuntu)
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name:
|
||||||
|
- unattended-upgrades
|
||||||
|
- apt-listchanges
|
||||||
|
state: present
|
||||||
|
update_cache: yes
|
||||||
|
cache_valid_time: 3600
|
||||||
|
when: ansible_os_family in ["Debian", "Ubuntu"]
|
||||||
|
|
||||||
|
- name: Skonfiguruj unattended-upgrades (tylko aktualizacje bezpieczeństwa, bez auto-reboot)
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: ../templates/50unattended-upgrades.j2
|
||||||
|
dest: /etc/apt/apt.conf.d/50unattended-upgrades
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
when: ansible_os_family in ["Debian", "Ubuntu"]
|
||||||
|
|
||||||
|
- name: Włącz okresowe aktualizacje APT (20auto-upgrades)
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /etc/apt/apt.conf.d/20auto-upgrades
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
content: |
|
||||||
|
APT::Periodic::Update-Package-Lists "1";
|
||||||
|
APT::Periodic::Unattended-Upgrade "1";
|
||||||
|
when: ansible_os_family in ["Debian", "Ubuntu"]
|
||||||
|
|
||||||
|
- name: Wyłącz logowanie hasłem (PasswordAuthentication no)
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/ssh/sshd_config
|
||||||
|
regexp: '(?i)^\s*#?\s*PasswordAuthentication'
|
||||||
|
line: 'PasswordAuthentication no'
|
||||||
|
state: present
|
||||||
|
backup: yes
|
||||||
|
validate: '/usr/sbin/sshd -t -f %s'
|
||||||
|
notify: Restart SSH
|
||||||
|
|
||||||
|
- name: Ogranicz logowanie root do klucza (PermitRootLogin prohibit-password)
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/ssh/sshd_config
|
||||||
|
regexp: '(?i)^\s*#?\s*PermitRootLogin'
|
||||||
|
line: 'PermitRootLogin prohibit-password'
|
||||||
|
state: present
|
||||||
|
backup: yes
|
||||||
|
validate: '/usr/sbin/sshd -t -f %s'
|
||||||
|
notify: Restart SSH
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
- name: Restart SSH
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: ssh
|
||||||
|
state: restarted
|
||||||
23
playbooks/playbook_ssh.yml
Normal file
23
playbooks/playbook_ssh.yml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
- name: Konfiguracja SSH (blokada pustych haseł)
|
||||||
|
hosts: all
|
||||||
|
gather_facts: true
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Zablokuj logowanie na puste hasła (PermitEmptyPasswords no)
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/ssh/sshd_config
|
||||||
|
# Szuka linii zaczynającej się od PermitEmptyPasswords (również zakomentowanej)
|
||||||
|
regexp: '(?i)^\s*#?\s*PermitEmptyPasswords'
|
||||||
|
line: 'PermitEmptyPasswords no'
|
||||||
|
state: present
|
||||||
|
backup: yes
|
||||||
|
# Sprawdza poprawność pliku przed zapisem, aby nie zepsuć SSH
|
||||||
|
validate: '/usr/sbin/sshd -t -f %s'
|
||||||
|
notify: Restart SSH
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
- name: Restart SSH
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: ssh
|
||||||
|
state: restarted
|
||||||
32
playbooks/playbook_ssh_keys.yml
Normal file
32
playbooks/playbook_ssh_keys.yml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
# Zarządzanie kluczami publicznymi SSH konta logowania (root lub mgmt, w zależności
|
||||||
|
# od grupy PatchMon hosta) na całej flocie jednym uruchomieniem.
|
||||||
|
#
|
||||||
|
# Lista kluczy: admin_ssh_keys w group_vars/all.yml (uzupełnij przed użyciem).
|
||||||
|
#
|
||||||
|
# Domyślnie tylko DODAJE brakujące klucze (bezpieczne). Aby wymusić, żeby
|
||||||
|
# authorized_keys zawierał WYŁĄCZNIE klucze z admin_ssh_keys (usuwając inne),
|
||||||
|
# uruchom z: -e "ssh_keys_exclusive=true" — rób to świadomie, może odciąć dostęp.
|
||||||
|
- name: Zarządzanie kluczami SSH konta logowania
|
||||||
|
hosts: all
|
||||||
|
gather_facts: false
|
||||||
|
vars:
|
||||||
|
ssh_keys_exclusive: false
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Sprawdź, czy admin_ssh_keys nie jest puste
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- admin_ssh_keys | length > 0
|
||||||
|
fail_msg: >-
|
||||||
|
admin_ssh_keys jest puste — uzupełnij listę kluczy w group_vars/all.yml
|
||||||
|
przed uruchomieniem tego playbooka.
|
||||||
|
run_once: true
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
- name: Zapewnij obecność kluczy publicznych w authorized_keys
|
||||||
|
ansible.posix.authorized_key:
|
||||||
|
user: "{{ ansible_user }}"
|
||||||
|
key: "{{ admin_ssh_keys | join('\n') }}"
|
||||||
|
state: present
|
||||||
|
exclusive: "{{ ssh_keys_exclusive }}"
|
||||||
16
playbooks/playbook_upgrade.yml
Normal file
16
playbooks/playbook_upgrade.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
- name: Aktualizacja pakietów (Debian/Ubuntu)
|
||||||
|
hosts: all
|
||||||
|
gather_facts: true
|
||||||
|
tasks:
|
||||||
|
- name: Aktualizacja cache pakietów (Debian/Ubuntu)
|
||||||
|
ansible.builtin.apt:
|
||||||
|
update_cache: yes
|
||||||
|
cache_valid_time: 3600
|
||||||
|
when: ansible_os_family in ["Debian", "Ubuntu"]
|
||||||
|
|
||||||
|
- name: Aktualizacja wszystkich pakietów (Debian/Ubuntu)
|
||||||
|
ansible.builtin.apt:
|
||||||
|
upgrade: dist
|
||||||
|
when: ansible_os_family in ["Debian", "Ubuntu"]
|
||||||
|
|
||||||
139
readme.md
Normal file
139
readme.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# Ansible + PatchMon — zarządzanie flotą serwerów
|
||||||
|
|
||||||
|
Inwentarz jest pobierany dynamicznie z PatchMon (`patchmon_inventory.yml`, plugin
|
||||||
|
`patchmon.dynamic_inventory`). Grupy hostów pochodzą wprost z grup przypisanych
|
||||||
|
w PatchMon — filtrowanie na potrzeby playbooków (`--limit`) odbywa się przez te grupy,
|
||||||
|
nie przez ręczne listy hostów.
|
||||||
|
|
||||||
|
## Pierwsze uruchomienie (świeży klon repozytorium)
|
||||||
|
|
||||||
|
Sekrety nie są trzymane w gicie — trzeba je uzupełnić lokalnie:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp patchmon_inventory.yml.example patchmon_inventory.yml
|
||||||
|
# uzupełnij api_url / api_key / api_secret w patchmon_inventory.yml
|
||||||
|
|
||||||
|
mkdir -p ssh_keys && chmod 700 ssh_keys
|
||||||
|
cp /ścieżka/do/twojego/klucza ssh_keys/id_rsa && chmod 600 ssh_keys/id_rsa
|
||||||
|
```
|
||||||
|
|
||||||
|
## Grupy PatchMon używane w tym projekcie
|
||||||
|
|
||||||
|
| Grupa | Znaczenie |
|
||||||
|
|---|---|
|
||||||
|
| `root` | Hosty logowane bezpośrednio na konto `root`. |
|
||||||
|
| `mgmt` | Hosty logowane na konto `mgmt` (z `sudo`). |
|
||||||
|
| `Update-now` | Wszystkie hosty dopuszczone do automatycznej aktualizacji (`root` ∪ `mgmt`). Domyślny cel dla crona/autopilota. |
|
||||||
|
| `Type-HOST` | Fizyczne hosty Proxmox — traktuj ostrożniej (restart hosta restartuje też jego VM/CT). |
|
||||||
|
| `Type-VM` / `Type-CT` | Maszyny wirtualne / kontenery LXC. |
|
||||||
|
| `Site-nni` / `Site-on` | Lokalizacje. |
|
||||||
|
|
||||||
|
Konto logowania (`ansible_user`) **nie** jest już podawane ręcznie przez `-e` — jest
|
||||||
|
ustawiane automatycznie przez `group_vars/root.yml` i `group_vars/mgmt.yml` na
|
||||||
|
podstawie grupy PatchMon danego hosta. Dzięki temu jedno uruchomienie playbooka na
|
||||||
|
grupie mieszanej (np. `Update-now`) obsłuży poprawnie oba typy kont.
|
||||||
|
|
||||||
|
## Struktura projektu
|
||||||
|
|
||||||
|
```
|
||||||
|
ssh_keys/
|
||||||
|
id_rsa # klucz prywatny SSH (poza gitem, patrz sekcja "Sekrety")
|
||||||
|
group_vars/
|
||||||
|
all.yml # klucz SSH, become, próg dysku, lista kluczy admina
|
||||||
|
root.yml / mgmt.yml # ansible_user per grupa PatchMon
|
||||||
|
playbooks/
|
||||||
|
playbook_upgrade.yml # apt update + dist-upgrade
|
||||||
|
playbook_cleanup.yml # autoremove/autoclean, czyszczenie journala, alarm dysku
|
||||||
|
playbook_docker_cleanup.yml # czyszczenie nieużywanych obrazów Docker (tylko hosty z Dockerem)
|
||||||
|
playbook_fleet_report.yml # raport: OS, uptime, dysk, liczba aktualizacji, reboot-required
|
||||||
|
playbook_security_baseline.yml # unattended-upgrades + wyłączenie logowania hasłem SSH
|
||||||
|
playbook_ssh.yml # blokada pustych haseł SSH
|
||||||
|
playbook_ssh_keys.yml # zarządzanie kluczami publicznymi na koncie logowania
|
||||||
|
playbook_autopilot.yml # orkiestracja pod cron: upgrade -> cleanup -> fleet_report
|
||||||
|
templates/
|
||||||
|
50unattended-upgrades.j2
|
||||||
|
fleet_report.j2
|
||||||
|
cron/
|
||||||
|
run-autopilot.sh # wrapper do crona (flock + log)
|
||||||
|
crontab.example
|
||||||
|
ansible-autopilot.logrotate
|
||||||
|
reports/ # tu trafiają wygenerowane raporty floty
|
||||||
|
```
|
||||||
|
|
||||||
|
## Użycie
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Aktualizacja pakietów na całej flocie dopuszczonej do auto-update
|
||||||
|
ansible-playbook -i patchmon_inventory.yml playbooks/playbook_upgrade.yml --limit "Update-now"
|
||||||
|
|
||||||
|
# Sprzątanie/kondycja systemu (autoremove, journal, alarm zajętości dysku)
|
||||||
|
ansible-playbook -i patchmon_inventory.yml playbooks/playbook_cleanup.yml --limit "Update-now"
|
||||||
|
|
||||||
|
# Czyszczenie nieużywanych obrazów Docker (tylko hosty, na których jest Docker)
|
||||||
|
ansible-playbook -i patchmon_inventory.yml playbooks/playbook_docker_cleanup.yml --limit "Update-now"
|
||||||
|
|
||||||
|
# Raport stanu floty -> plik w reports/
|
||||||
|
# WAŻNE: dopisz ",localhost" do --limit, inaczej faza renderowania raportu
|
||||||
|
# zostanie pominięta ("skipping: no hosts matched").
|
||||||
|
ansible-playbook -i patchmon_inventory.yml playbooks/playbook_fleet_report.yml --limit "Update-now,localhost"
|
||||||
|
|
||||||
|
# Baza bezpieczeństwa: unattended-upgrades + wyłączenie logowania hasłem SSH
|
||||||
|
# (dotyka sshd_config — uruchamiaj świadomie, ręcznie, nie z crona)
|
||||||
|
ansible-playbook -i patchmon_inventory.yml playbooks/playbook_security_baseline.yml --limit "Update-now"
|
||||||
|
|
||||||
|
# Blokada logowania na puste hasła
|
||||||
|
ansible-playbook -i patchmon_inventory.yml playbooks/playbook_ssh.yml --limit "Update-now"
|
||||||
|
|
||||||
|
# Zarządzanie kluczami SSH konta logowania (uzupełnij admin_ssh_keys w group_vars/all.yml)
|
||||||
|
ansible-playbook -i patchmon_inventory.yml playbooks/playbook_ssh_keys.yml --limit "Update-now"
|
||||||
|
|
||||||
|
# Pełny przebieg autopilot ręcznie (to samo co robi cron)
|
||||||
|
ansible-playbook -i patchmon_inventory.yml playbooks/playbook_autopilot.yml --limit "Update-now,localhost"
|
||||||
|
```
|
||||||
|
|
||||||
|
Można też celować w węższe grupy, np. `--limit "Site-on"`, `--limit "Type-CT"`,
|
||||||
|
albo pojedynczy host: `--limit "pve"`.
|
||||||
|
|
||||||
|
## Reboot — tylko raportowanie, nigdy automatycznie
|
||||||
|
|
||||||
|
`playbook_fleet_report.yml` sprawdza `/var/run/reboot-required` na każdym hoście
|
||||||
|
i wypisuje listę hostów wymagających restartu w raporcie (`reports/fleet_report_*.txt`).
|
||||||
|
**Żaden playbook w tym projekcie nie restartuje hostów automatycznie** — decyzję
|
||||||
|
i moment restartu (szczególnie dla `Type-HOST`, czyli fizycznego Proxmoksa) podejmuje
|
||||||
|
operator ręcznie, po przejrzeniu raportu.
|
||||||
|
|
||||||
|
## Bezobsługowe uruchamianie (cron)
|
||||||
|
|
||||||
|
`cron/run-autopilot.sh` to wrapper na `playbook_autopilot.yml`:
|
||||||
|
- blokuje się przez `flock`, żeby nie nakładały się dwa równoległe przebiegi,
|
||||||
|
- loguje przebieg (start/koniec/kod wyjścia) do `/var/log/ansible-autopilot.log`,
|
||||||
|
- domyślnie celuje w grupę `Update-now` (nadpisywalne zmienną `TARGET_GROUP`).
|
||||||
|
|
||||||
|
Instalacja:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x cron/run-autopilot.sh
|
||||||
|
crontab -e # wklej zawartość cron/crontab.example
|
||||||
|
sudo cp cron/ansible-autopilot.logrotate /etc/logrotate.d/ansible-autopilot
|
||||||
|
```
|
||||||
|
|
||||||
|
Każdy przebieg `playbook_fleet_report.yml` (w tym z crona) dokłada nowy plik do
|
||||||
|
`reports/` — warto od czasu do czasu posprzątać stare raporty, np.
|
||||||
|
`find reports/ -name 'fleet_report_*.txt' -mtime +30 -delete`.
|
||||||
|
|
||||||
|
## Sekrety
|
||||||
|
|
||||||
|
- **Klucz prywatny SSH** leży w `ssh_keys/id_rsa` (katalog `chmod 700`, plik `chmod 600`).
|
||||||
|
Katalog `ssh_keys/` jest w `.gitignore` — klucz nigdy nie trafia do repozytorium.
|
||||||
|
Ścieżka do niego jest ustawiona centralnie w `group_vars/all.yml`
|
||||||
|
(`ansible_ssh_private_key_file: "{{ playbook_dir }}/../ssh_keys/id_rsa"`), więc
|
||||||
|
wszystkie playbooki i `cron/run-autopilot.sh` korzystają z niej automatycznie —
|
||||||
|
nie trzeba nic dodatkowo konfigurować.
|
||||||
|
- **`api_key`/`api_secret`/`api_url` PatchMon** leżą w `patchmon_inventory.yml`, który
|
||||||
|
jest w `.gitignore` — nigdy nie trafia do repozytorium. Wersja do repo to
|
||||||
|
`patchmon_inventory.yml.example` (same placeholdery, bez sekretów). Przy zakładaniu
|
||||||
|
projektu od zera: `cp patchmon_inventory.yml.example patchmon_inventory.yml` i
|
||||||
|
uzupełnij `api_url`/`api_key`/`api_secret`.
|
||||||
|
(Plugin `patchmon.dynamic_inventory` wczytuje ten plik bezpośrednio, bez
|
||||||
|
templatowania Jinja2/Vault, dlatego to rozdzielenie na plik realny + `.example`,
|
||||||
|
a nie np. `{{ lookup('env', ...) }}`, jest tu jedynym niezawodnym sposobem.)
|
||||||
0
reports/.gitkeep
Normal file
0
reports/.gitkeep
Normal file
13
templates/50unattended-upgrades.j2
Normal file
13
templates/50unattended-upgrades.j2
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// Zarządzane przez Ansible (playbook_security_baseline.yml) — nie edytuj ręcznie.
|
||||||
|
// Instaluje wyłącznie aktualizacje bezpieczeństwa, bez automatycznego restartu:
|
||||||
|
// decyzję o reboot podejmuje operator na podstawie playbook_fleet_report.yml.
|
||||||
|
|
||||||
|
Unattended-Upgrade::Origins-Pattern {
|
||||||
|
"origin=Debian,codename=${distro_codename},label=Debian-Security";
|
||||||
|
"origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
|
||||||
|
"origin=Ubuntu,archive=${distro_codename}-security";
|
||||||
|
};
|
||||||
|
|
||||||
|
Unattended-Upgrade::Remove-Unused-Dependencies "true";
|
||||||
|
Unattended-Upgrade::Automatic-Reboot "false";
|
||||||
|
Unattended-Upgrade::Automatic-Reboot-WithUsers "false";
|
||||||
33
templates/fleet_report.j2
Normal file
33
templates/fleet_report.j2
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
Raport stanu floty — {{ ansible_date_time.iso8601 }}
|
||||||
|
{{ '=' * 60 }}
|
||||||
|
|
||||||
|
{% set reported_hosts = hostvars | dict2items | selectattr('value.host_report', 'defined') | map(attribute='key') | sort %}
|
||||||
|
{% set reboot_hosts = [] %}
|
||||||
|
{% for h in reported_hosts %}
|
||||||
|
{% if hostvars[h].host_report.reboot_required %}
|
||||||
|
{% set _ = reboot_hosts.append(h) %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
Hosty ({{ reported_hosts | length }}):
|
||||||
|
{% for h in reported_hosts %}
|
||||||
|
{% set r = hostvars[h].host_report %}
|
||||||
|
- {{ h }}
|
||||||
|
OS: {{ r.os }}
|
||||||
|
Uptime: {{ r.uptime_days }} dni
|
||||||
|
Dysk / zajęty: {{ r.disk_root_percent }}%
|
||||||
|
Aktualizacje: {{ r.upgradable_packages }} oczekujących
|
||||||
|
Reboot wymagany: {{ 'TAK' if r.reboot_required else 'nie' }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{{ '=' * 60 }}
|
||||||
|
{% if reboot_hosts | length > 0 %}
|
||||||
|
UWAGA — hosty wymagające ręcznego restartu ({{ reboot_hosts | length }}):
|
||||||
|
{% for h in reboot_hosts %}
|
||||||
|
- {{ h }}
|
||||||
|
{% endfor %}
|
||||||
|
Reboot NIE jest wykonywany automatycznie — zrestartuj te hosty ręcznie,
|
||||||
|
gdy będzie to bezpieczne (patrz grupy Type-HOST / Type-VM / Type-CT).
|
||||||
|
{% else %}
|
||||||
|
Żaden host nie wymaga restartu.
|
||||||
|
{% endif %}
|
||||||
Reference in New Issue
Block a user