systemd: restic (#332)

Co-authored-by: Michael Grote <michael.grote@posteo.de>
Reviewed-on: mg/ansible#332
Co-authored-by: mg <michael.grote@posteo.de>
Co-committed-by: mg <michael.grote@posteo.de>
This commit is contained in:
Michael Grote 2022-02-21 16:42:22 +01:00
parent 6ee25e1e16
commit 16ebb0324e
22 changed files with 330 additions and 286 deletions

View file

@ -6,6 +6,26 @@
#----------------------------------------------------------------#
# This file is managed with ansible! #
#----------------------------------------------------------------#
### mgrote.restic
restic_user: root
restic_group: restic
restic_conf_dir: /etc/restic
restic_exclude: |
._*
desktop.ini
.Trash-*
**/**cache***/**
**/**Cache***/**
**/**AppData***/**
restic_mount_timeout: "10 min"
restic_failure_delay: "30 s"
restic_schedule: "0/6:*"
restic_folders_to_backup: "/usr/local /etc /root /var/www /home"
restic_repository: "//fileserver2.grote.lan/restic"
restic_repository_password: "{{ lookup('keepass', 'restic_repository_password', 'password') }}"
restic_mount_user: restic
restic_mount_password: "{{ lookup('keepass', 'fileserver_smb_user_restic', 'password') }}"
restic_fail_mail: "{{ empfaenger_mail }}"
### mgrote.user
users:
- username: mg
@ -87,22 +107,6 @@
postfix_smtp_use_tls: "yes"
### mgrote.apt_manage_sources
manage_sources_apt_proxy: "acng.grote.lan:9999"
### mgrote.restic
restic_enable_role: true
restic_folders_to_backup: "/usr/local /etc /root /home"
restic_cron_hours: "19"
restic_repository: "//fileserver2.grote.lan/restic"
restic_repository_password: "{{ lookup('keepass', 'restic_repository_password', 'password') }}"
restic_mount: "/mnt/restic"
restic_mount_user: restic
restic_mount_password: "{{ lookup('keepass', 'fileserver_smb_user_restic', 'password') }}"
restic_exclude: |
._*
desktop.ini
.Trash-*
**/**cache***/**
**/**Cache***/**
**/**AppData***/**
### mgrote.tmux
tmux_conf_destination: "/home/mg/.tmux.conf"
tmux_bashrc_destination: "/home/mg/.bashrc"

View file

@ -1,41 +0,0 @@
---
### geerlingguy.jenkins
jenkins_package_state: latest
jenkins_http_port: 8080
jenkins_admin_username: jadmin
jenkins_admin_password: "{{ lookup('keepass', 'jenkins_admin_password', 'password') }}"
jenkins_plugins_install_dependencies: true
jenkins_plugins_state: latest
jenkins_java_options: "-Djenkins.install.runSetupWizard=true"
### oefenweb.ufw
ufw_rules:
- rule: allow
to_port: 22
protocol: tcp
comment: 'ssh'
from_ip: 0.0.0.0/0
- rule: allow
to_port: 8080
comment: 'jenkins'
from_ip: 0.0.0.0/0
- rule: allow
to_port: 4949
protocol: tcp
comment: 'munin'
from_ip: 192.168.2.144/24
### mgrote.restic
restic_folders_to_backup: /usr/local /etc /root /home /var/lib/jenkins
### geerlingguy.pip
pip_package: python3-pip
pip_install_packages:
- name: pykeepass==3.2.1
- name: jinja2>=2.11.2
- name: markupsafe
- name: ansible-playbook-grapher
### geerlingguy.ansible
ansible_install_method: pip
ansible_install_version_pip: '2.10'
### mgrote.apt_manage_packages
apt_packages_extra:
- graphviz # für ansible-playbook-grapher
- sshpass

View file

@ -0,0 +1,71 @@
---
- hosts: all
become: yes
tasks:
- name: remove /etc/restic
become: yes
ansible.builtin.file:
path: /etc/restic
state: absent
- name: ensure user exists
become: true
ansible.builtin.user:
name: restic
state: absent
- name: add user to sudoers
become: true
ansible.builtin.blockinfile:
path: /etc/sudoers
state: absent
block: |
restic ALL=(ALL) NOPASSWD:ALL
validate: '/usr/sbin/visudo -cf %s'
backup: yes
marker_begin: restic-sudoers BEGIN
marker_end: restic-sudoers END
- name: copy smb_password.txt
become: yes
ansible.builtin.file:
dest: "/etc/restic/smb_password.txt"
state: absent
- name: copy restic_backup.sh
become: yes
ansible.builtin.file:
state: absent
dest: "/usr/local/bin/restic_backup.sh"
- name: remove exclude.txt
become: yes
ansible.builtin.file:
path: "/etc/restic/exclude.txt"
state: absent
- name: copy password.txt
become: yes
ansible.builtin.file:
state: absent
dest: "/etc/restic/password.txt"
- name: remove restic cronjob
become: yes
ansible.builtin.cron:
name: restic
state: absent
job: "/usr/local/bin/restic_backup.sh"
minute: "{{ 59|random(seed=inventory_hostname) }}"
- name: remove restic log
become: true
ansible.builtin.file:
path: /var/log/restic.log
state: absent
- name: copy logrotate config
become: yes
ansible.builtin.file:
state: absent
dest: /etc/logrotate.d/restic

View file

@ -2,12 +2,16 @@
### Beschreibung
Installiert und konfiguriert restic.
Die Konfigurationsdaten liegen unter /etc/restic.
Es wird ein Cronjob angelegt, bei dem die Minuten quasi-zufaellig auf Basis des Hostnamens generiert werden.
Das Repository wird über (auto)mount-Units gemountet.
Das Backup wird über Timer-Units geplant.
Im Fehlerfall wird eine Mail verschickt.
Die Konfigurationsdaten liegen unter "{{ restic_conf_dir }}.
### getestet auf
- [X] Ubuntu (>=18.04)
- [X] Debian
- [X] ProxMox 6.1
- [] Debian
- [X] ProxMox 7*
### Variablen + Defaults
see [defaults](./defaults/main.yml)

View file

@ -1,21 +1,33 @@
---
restic_anzahl_versuche_backup: "3" # wie oft soll restic versuchen ein backup zu starten
restic_wartezeit: "60" # wartezeit zwischen den versuchen
restic_folders_to_backup: "/usr/local /etc /root /var/www /home" # welche ordner sollen gesichert werden
restic_cron_hours: "19" # zu welcher stunde soll das script gestartet werden(minute wird aus dem hostnamen generiert)
restic_repository: "ANY.SMB.SHARE" # smb-share mit dem repository: z.B. "//fileserver2.grote.lan/restic"
restic_repository_password: XXXXX # password für das repo
restic_mount: "/mnt/restic" # wohin soll das repo gemountet werden
restic_mount_user: restic # nutzer für den share/mount
restic_mount_password: XXXXX # passwort für den mount
restic_exclude: | # was soll ausgeschlossen werden, siehe: https://github.com/restic/restic/issues/1005; https://forum.restic.net/t/exclude-syntax-confusion/1531/12
# restic user
restic_user: root
# restic group
restic_group: restic
# restic config directory
restic_conf_dir: /etc/restic
# was soll ausgeschlossen werden, siehe: https://github.com/restic/restic/issues/1005; https://forum.restic.net/t/exclude-syntax-confusion/1531/12
restic_exclude: |
._*
desktop.ini
.Trash-*
**/**cache***/**
**/**Cache***/**
**/**AppData***/**
restic_enable_role: true
### under which user the script is run
restic_user_group: "root"
restic_user: "restic"
# timeout for cifs mount; systemd notation
restic_mount_timeout: "10 min"
# delay for restartung task; systemd notation
restic_failure_delay: "30 s"
# when should restic run; systemd notation
restic_schedule: "*:0/2"
# welche ordner sollen gesichert werden
restic_folders_to_backup: "/usr/local /etc /root /var/www /home"
# smb-share mit dem repository: z.B. "//fileserver2.grote.lan/restic"
restic_repository: "//fileserver.domain/restic"
# password für das repo
restic_repository_password: "{{ lookup('keepass', 'restic_repository_password', 'password') }}"
# nutzer für den share
restic_mount_user: restic
# passwort für den mount
restic_mount_password: "unsafe_password"
# where to send in case of an error
restic_fail_mail: x@y.de

View file

@ -0,0 +1,28 @@
---
- name: systemctl daemon-reload
become: yes
ansible.builtin.systemd:
daemon_reload: yes
- name: systemctl enable units
become: yes
ansible.builtin.systemd:
name: "{{ item }}"
enabled: yes
masked: no
with_items:
- media-restic.automount
- media-restic.mount
- restic.service
- restic.timer
- restic_mail.service
- name: systemctl start units
become: yes
ansible.builtin.systemd:
name: "{{ item }}"
state: restarted
enabled: yes
with_items:
- restic.timer
notify: systemctl daemon-reload

View file

@ -1,66 +0,0 @@
---
- name: copy smb_password.txt
become: yes
ansible.builtin.template:
src: "smb_password.txt"
dest: "/etc/restic/smb_password.txt"
owner: "{{ restic_user }}"
group: "{{ restic_user_group }}"
mode: 0600
- name: copy restic_backup.sh
become: yes
ansible.builtin.template:
src: "restic_backup.sh"
dest: "/usr/local/bin/restic_backup.sh"
mode: 0744
owner: "{{ restic_user }}"
group: "{{ restic_user_group }}"
- name: create exclude.txt
become: yes
ansible.builtin.blockinfile:
path: "/etc/restic/exclude.txt"
create: yes
block: "{{ restic_exclude }}"
mode: 0644
- name: copy password.txt
become: yes
ansible.builtin.template:
src: "password.txt"
dest: "/etc/restic/password.txt"
owner: "{{ restic_user }}"
group: "{{ restic_user_group }}"
mode: 0600
- name: create restic cronjob
become: yes
ansible.builtin.cron:
name: restic
state: present
job: "/usr/local/bin/restic_backup.sh"
minute: "{{ 59|random(seed=inventory_hostname) }}"
hour: "{{ restic_cron_hours }}"
# siehe: https://stackoverflow.com/questions/33379378/idempotence-and-random-variables-in-ansible
user: "{{ restic_user }}"
- name: Create restic log
become: true
ansible.builtin.file:
path: /var/log/restic.log
state: touch
owner: "{{ restic_user }}"
group: "{{ restic_user_group }}"
mode: 0644
access_time: preserve
modification_time: preserve
- name: copy logrotate config
become: yes
ansible.builtin.template:
src: logrotate_restic
dest: /etc/logrotate.d/restic
owner: "{{ restic_user }}"
group: "{{ restic_user_group }}"
mode: 0644

View file

@ -1,15 +0,0 @@
---
- name: create /etc/restic
become: yes
ansible.builtin.file:
path: /etc/restic
state: directory
- name: create restic mount-directory
become: yes
ansible.builtin.file:
path: "{{ restic_mount }}"
state: directory
owner: "{{ restic_user }}"
group: "{{ restic_user_group }}"
mode: 0755

View file

@ -1,10 +0,0 @@
---
- name: install restic-packages
become: yes
ansible.builtin.package:
name:
- restic
- logrotate
- cifs-utils
- sudo
state: present

View file

@ -1,16 +1,124 @@
---
- name: include user tasks
include_tasks: user.yml
when: restic_enable_role
- name: ensure group exists
become: true
ansible.builtin.group:
name: "{{ restic_group }}"
state: present
- name: include install tasks
include_tasks: install.yml
when: restic_enable_role
- name: ensure user exists
become: true
ansible.builtin.user:
name: "{{ restic_user }}"
group: "{{ restic_group }}"
shell: /usr/sbin/nologin
- name: include directories tasks
include_tasks: dir.yml
when: restic_enable_role
- name: install restic-packages
become: yes
ansible.builtin.package:
name:
- restic
state: present
- name: include config tasks
include_tasks: config.yml
when: restic_enable_role
- name: create "{{ restic_conf_dir }}"
become: yes
ansible.builtin.file:
path: "{{ restic_conf_dir }}"
state: directory
owner: "{{ restic_user }}"
group: "{{ restic_group }}"
mode: 0755
- name: template smb.cred
become: yes
ansible.builtin.template:
src: "smb.cred.j2"
dest: "{{ restic_conf_dir }}/smb.cred"
owner: "{{ restic_user }}"
group: "{{ restic_group }}"
mode: 0600
no_log: true
- name: templates excludes
become: yes
ansible.builtin.blockinfile:
path: "{{ restic_conf_dir }}/excludes"
create: yes
block: "{{ restic_exclude }}"
mode: 0644
owner: "{{ restic_user }}"
group: "{{ restic_group }}"
- name: template restic.env
become: yes
ansible.builtin.template:
src: "restic.env.j2"
dest: "{{ restic_conf_dir }}/restic.env"
owner: root
group: root
mode: 0600
no_log: true
- name: template restic.mount
become: yes
ansible.builtin.template:
src: media-restic.mount.j2
dest: /etc/systemd/system/media-restic.mount # media-restic == /media/restic
owner: root
group: root
mode: 0644
notify:
- systemctl daemon-reload
- systemctl enable units
- name: template restic.automount
become: yes
ansible.builtin.template:
src: media-restic.automount.j2
dest: /etc/systemd/system/media-restic.automount
owner: root
group: root
mode: 0644
notify:
- systemctl daemon-reload
- systemctl enable units
- systemctl start units
- name: template restic.service
become: yes
ansible.builtin.template:
src: restic.service.j2
dest: /etc/systemd/system/restic.service
owner: root
group: root
mode: 0644
notify:
- systemctl daemon-reload
- name: template restic.timer
become: yes
ansible.builtin.template:
src: restic.timer.j2
dest: /etc/systemd/system/restic.timer
owner: root
group: root
mode: 0644
notify:
- systemctl daemon-reload
- name: template restic_mail.service
become: yes
ansible.builtin.template:
src: "restic_mail.service.j2"
dest: /etc/systemd/system/restic_mail.service
owner: root
group: root
mode: 0644
notify:
- systemctl daemon-reload
- name: systemctl start restic.timer
become: yes
ansible.builtin.systemd:
name: restic.timer
state: started
enabled: yes

View file

@ -1,33 +0,0 @@
---
- name: ensure group exists
become: true
ansible.builtin.group:
name: "{{ restic_user_group }}"
state: present
when:
- restic_user_group is defined
- name: ensure user exists
become: true
ansible.builtin.user:
name: "{{ restic_user }}"
group: "{{ restic_user_group }}"
shell: /usr/sbin/nologin
when:
- restic_user_group is defined
- restic_user is defined
- name: add user to sudoers
become: true
ansible.builtin.blockinfile:
path: /etc/sudoers
state: present
block: |
{{ restic_user }} ALL=(ALL) NOPASSWD:ALL
validate: '/usr/sbin/visudo -cf %s'
backup: yes
marker_begin: restic-sudoers BEGIN
marker_end: restic-sudoers END
when:
- restic_user_group is defined
- restic_user is defined

View file

@ -1,12 +0,0 @@
{{ file_header | default () }}
/var/log/restic.log {
su root root
create 0640 root root
rotate 4
weekly
compress
missingok
notifempty
dateext
dateyesterday
}

View file

@ -0,0 +1,11 @@
{{ file_header | default () }}
[Unit]
Description=Automounter for restic
Requires=network-online.target
[Automount]
Where=/media/restic
TimeoutIdleSec={{ restic_mount_timeout }}
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,9 @@
{{ file_header | default () }}
[Unit]
Description=Mount Unit for restic
[Mount]
What={{ restic_repository }}
Where=/media/restic
Options=vers=3.0,uid={{ restic_user }},credentials={{ restic_conf_dir }}/smb.cred
Type=cifs

View file

@ -1 +0,0 @@
{{ restic_repository_password }}

View file

@ -0,0 +1,3 @@
{{ file_header | default () }}
RESTIC_REPOSITORY="/media/restic"
RESTIC_PASSWORD="{{ restic_repository_password }}"

View file

@ -0,0 +1,14 @@
{{ file_header | default () }}
[Unit]
Description=Backup with restic
Requires=media-restic.mount
After=media-restic.mount
OnFailure=restic_mail.service
[Service]
Type=simple
EnvironmentFile={{ restic_conf_dir }}/restic.env
ExecStart=/usr/bin/restic backup --no-cache --exclude-file {{ restic_conf_dir }}/excludes {{ restic_folders_to_backup }}
User={{ restic_user }}
Group={{ restic_group }}
RestartSec={{ restic_failure_delay }}

View file

@ -0,0 +1,10 @@
{{ file_header | default () }}
[Unit]
Description=Timer for restic backups.
[Timer]
OnCalendar={{ restic_schedule }}
RandomizedDelaySec=30 min
[Install]
WantedBy=timers.target multi-user.target

View file

@ -1,62 +0,0 @@
#!/bin/bash
{{ file_header | default () }}
# source functions
if [[ -f "/usr/local/bin/functions.sh" ]]; then
source /usr/local/bin/functions.sh
else
echo "[ERROR] Could not find: /usr/local/bin/functions.sh"
exit 3
fi
# set lock
## call function
## lock gets set and released if the script terminates
set_lock
abbruch_restic=0 # set counter for error
sudo mount -t cifs -o credentials="/etc/restic/smb_password.txt",vers=3.0,uid=$UID {{ restic_repository }} {{ restic_mount }} # mount share
mount_return_value=$? # schreib Exit Code in Variable
if ( [ "$mount_return_value" -ne 0 ] ); then
{
echo "--------------------------------------------------" # Trenner logfile
echo $(date +%d.%m.%Y-%T) # Datum für logfile
echo "mount error"
} >> /var/log/restic.log 2>&1;
tail --lines=5 "/var/log/restic.log" | mail -s "Backup-Error - restic - $HOSTNAME" {{ empfaenger_mail }}
exit 1
else
{
echo "--------------------------------------------------" # Trenner logfile
echo $(date +%d.%m.%Y-%T) # Datum für logfile
echo "mount successful"
} >> /var/log/restic.log 2>&1;
fi
while [[ "$abbruch_restic" -le {{ restic_anzahl_versuche_backup }} ]] # Schleife für Abbruchbedingung; um die eckigen Klammern(Befehl "test") muss immer ein leerzeichen sein
do
{ # ist keine Subshell sondern Grouping; https://askubuntu.com/questions/662190/write-the-output-of-multiple-sequential-commands-to-a-text-file
echo "--------------------------------------------------" # Trenner logfile
echo $(date +%d.%m.%Y-%T) # Datum für logfile
restic -r {{ restic_mount }} --password-file /etc/restic/password.txt backup --exclude-file /etc/restic/exclude.txt {{ restic_folders_to_backup }} # execute Backup
restic_return_value=$? # schreib Exit Code in Variable
if ( [[ "$restic_return_value" -eq 0 ]] ); # Prüfung ob restic erfolgreich war(setze Abbruchbedingung), wenn nicht warte 1min und zähle die Abbruchbedingung hoch
then
abbruch_restic=99
else
sleep {{ restic_wartezeit }}
abbruch_restic=$(("$abbruch_restic" + 1))
fi
echo $(date +%d.%m.%Y-%T) # Datum für logfile
} >> /var/log/restic.log 2>&1; # leite die komplette Ausgabe in logfile um
done
sudo umount {{ restic_mount }} >> /var/log/restic.log 2>&1; # unmount
if ( [[ "$restic_return_value" -ne 0 ]] ); then # sende eMail wenn Restic Fehler ungleich 0, also Fehler; #https://stackoverflow.com/a/45817972
tail --lines=50 "/var/log/restic.log" | mail -s "Backup-Error - restic - $HOSTNAME" {{ empfaenger_mail }} # schreibe die letzten 50 Zeilen aus dem Logfile in den Body der Mail
fi

View file

@ -0,0 +1,8 @@
{{ file_header | default () }}
[Unit]
Description=Send a Mail in case of an error in restic.service.
[Service]
Type=oneshot
ExecStart=/bin/bash -c '/bin/systemctl status restic.service | mail -s "[ERROR] restic - %H" {{ empfaenger_mail }}'

View file

@ -1,6 +1,7 @@
{{ file_header | default () }}
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
@ -9,6 +10,7 @@
# Entries in this file show the compile time defaults.
# You can change settings by editing this file.
# Defaults can be restored by simply deleting this file.
#
# See timesyncd.conf(5) for details.