Initial commit

This commit is contained in:
nd 2021-03-12 03:35:14 +01:00
commit 420be44f56
No known key found for this signature in database
GPG key ID: 21B5CD4DEE3670E9
11 changed files with 432 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
__pycache__

44
README.md Normal file
View file

@ -0,0 +1,44 @@
# Backup client
## Parameters and defaults
All configuration is to be placed inside the `backups` dict.
```
# backend specific settings
backends:
# restic specific settings
restic:
# url of the restic repository
url: '/var/backup-client/restic'
# repository type musst be 'local'
repo_type: 'local'
# Mode in which the backup is taken. One of the following:
#
# vm-via-hypervisor: backup a vm via restic on the hypervisor. Saves config on the host
# hypervisor-restic: backup its vms via restic
# standalone-restic: use restic on the target itself to save a backup to a backup location (TODO)
mode: vm-via-hypervisor
# Allows backups to be skipped
enabled: True
# How many copies per time intervall should be kept?
# Note that this is ignored in vm-via-hypervisor mode because the vm host settings are used for all vms
retention:
hours: 12
days: 14
weeks: 16
months: 12
years: 3
# keys are strings with glob patterns of files to be excluded. Value musst be true to enable the exclude, false to disable it
# Only supportet in restic based backups
exclude_files: {}
# Keys are strings with glob patterns of files to be included. Value musst be true to enable the include, false to disable it
# Only supportet in restic based backups
# Ignored in vm-via-hypervisor mode
include_files: {}
```

20
defaults/main.yml Normal file
View file

@ -0,0 +1,20 @@
backups:
backends:
restic:
url: /var/backup-client/restic
repo_type: local
mode: vm-via-hypervisor
enabled: True
retention:
hours: 12
days: 14
weeks: 16
months: 12
years: 3
exclude_files:
'/tmp': true
'/var/tmp': true
'/var/cache': true
'/root/.ansible/': true
include_files:
'/': true

27
filter_plugins/filters.py Executable file
View file

@ -0,0 +1,27 @@
#!/usr/bin/env python3
import os
def filterEnabled(inputdict):
output = []
for i in inputdict.keys():
if inputdict.get(i):
output.append(i)
return output
def vmpath2hostpath(inputfiles, mountpoint):
output = []
for line in inputfiles:
line = os.path.normpath(line)
if line.startswith('/'):
disk = '*'
output.append(os.path.join(mountpoint, disk, line[1:]))
else:
output.append(line)
return output
class FilterModule(object):
def filters(self):
return {
'vmpath2hostpath': vmpath2hostpath,
'filterEnabled': filterEnabled,
}

170
tasks/main.yml Normal file
View file

@ -0,0 +1,170 @@
- name: parse config
set_fact:
backup_backend: "{% if backups.mode in ['standalone-restic', 'hypervisor-restic'] %}restic{% else %}False{% endif %}"
backup_executor: "{% if backups.mode in ['vm-via-hypervisor'] %}False{% else %}True{% endif %}"
- debug:
var: backup_backend
- name: create config folder
file:
path: /etc/backup-client/
state: directory
owner: root
group: root
mode: 0700
- name: setup hosts that actualy run backup code (not vms for example)
when: backup_executor
block:
- name: create retention file
copy:
dest: /etc/backup-client/retention.env
owner: root
group: root
mode: 0700
content: |
export BACKUP_RETENTION_HOURS={{ backups.retention.hours }}
export BACKUP_RETENTION_DAYS={{ backups.retention.days }}
export BACKUP_RETENTION_WEEKS={{ backups.retention.weeks }}
export BACKUP_RETENTION_MONTHS={{ backups.retention.months }}
export BACKUP_RETENTION_YEARS={{ backups.retention.years }}
- name: copy backup config
loop:
- name: 'enabled'
flag: '{{ backups.enabled }}'
file:
path: /etc/backup-client/{{ item.name }}
owner: root
group: root
mode: 0600
state: "{% if item.flag %}touch{% else %}absent{% endif %}"
- name: copy scripts
loop:
- backup-retention
- backup-standalone
- backup-vm
- backup-all-vms
- backup-full
- backup-cronjob
template:
src: "{{ item }}.j2"
dest: "/usr/local/bin/{{ item }}"
owner: root
group: root
mode: 0700
validate: /bin/bash -n %s
- name: create data folder
file:
path: /var/backup-client/
state: directory
owner: root
group: root
mode: 0700
- name: handle common restic based setup tasks
when: backup_backend == 'restic'
block:
- name: install backend tools (restic)
apt:
pkg:
- restic
- name: copy exclude file
copy:
dest: /etc/backup-client/exclude_files
owner: root
group: root
mode: 0600
content: "{{ backups.exclude_files|filterEnabled|join('\n') }}"
- name: copy include file
copy:
dest: /etc/backup-client/include_files
owner: root
group: root
mode: 0600
content: "{{ backups.include_files|filterEnabled|join('\n') }}"
- name: create repo key for restic
command: "dd if=/dev/urandom of=/etc/backup-client/restic.key bs=1k count=16"
args:
creates: "/etc/backup-client/restic.key"
- name: create restic env file
copy:
dest: /etc/backup-client/restic.env
owner: root
group: root
mode: 0700
content: |
export RESTIC_REPOSITORY="{{ backups.backends.restic.url }}"
export RESTIC_PASSWORD_FILE="/etc/backup-client/restic.key"
- name: create restic repository folder
when: backups.backends.restic.repo_type == 'local'
file:
path: "{{ backups.backends.restic.url }}"
state: directory
owner: root
group: root
mode: 0700
- name: create repo for restic
when: backups.backends.restic.repo_type == 'local'
shell: 'source /etc/backup-client/restic.env; restic init'
args:
executable: /bin/bash
creates: "{{ backups.backends.restic.url }}/config"
- name: handle hypervisor mode
when: backups.mode == 'hypervisor-restic'
block:
- name: create vms config folder
file:
path: /etc/backup-client/vms/
state: directory
owner: root
group: root
mode: 0700
- name: create vm mount point
file:
path: /var/backup-client/vm-mountpoint/
state: directory
owner: root
group: root
mode: 0700
- name: handle vm-via-hypervisor mode
when: backups.mode == 'vm-via-hypervisor'
block:
- name: create config folder on vm host
delegate_to: "{{ vm['host'] }}"
file:
dest: /etc/backup-client/vms/{{ vm['name'] }}
state: directory
owner: root
group: root
mode: 0700
- name: copy exclude file to vm host
delegate_to: "{{ vm['host'] }}"
copy:
dest: /etc/backup-client/vms/{{ vm['name'] }}/exclude_files
owner: root
group: root
mode: 0600
content: "{{ backups.exclude_files|filterEnabled|vmpath2hostpath(mountpoint='/var/backup-client/vm-mountpoint')|join('\n') }}"
- name: copy include file to vm host
delegate_to: "{{ vm['host'] }}"
copy:
dest: /etc/backup-client/vms/{{ vm['name'] }}/include_files
owner: root
group: root
mode: 0600
content: "{{ backups.include_files|filterEnabled|vmpath2hostpath(mountpoint='/var/backup-client/vm-mountpoint')|join('\n') }}"
- name: copy vm backup config to vm host
delegate_to: "{{ vm['host'] }}"
loop:
- name: 'enabled'
flag: '{{ backups.enabled }}'
file:
path: /etc/backup-client/vms/{{ vm['name'] }}/{{ item.name }}
owner: root
group: root
mode: 0600
state: "{% if item.flag %}touch{% else %}absent{% endif %}"

6
templates/backup-all-vms.j2 Executable file
View file

@ -0,0 +1,6 @@
#!/bin/bash
set -euo pipefail
for d in `virsh list --all --name`; do
backup-vm "$d"
done

15
templates/backup-cronjob.j2 Executable file
View file

@ -0,0 +1,15 @@
#!/bin/bash
set -euo pipefail
(
set -euo pipefail
flock -x -w 10 200 || exit 1
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
echo started backup cronjob
ionice -c 3 -p$$
nice -n 19 backup-full
echo finished backup cronjob
) 200>/var/lock/backup-cronjob.lock | logger -t "backup-cronjob"

13
templates/backup-full.j2 Executable file
View file

@ -0,0 +1,13 @@
#!/bin/bash
set -euo pipefail
{% if backup_executor %}
backup-standalone
{% endif %}
{% if backups.mode in ['hypervisor-restic'] %}
backup-all-vms
{% endif %}
{% if backup_executor %}
backup-retention
{% endif %}

19
templates/backup-retention.j2 Executable file
View file

@ -0,0 +1,19 @@
#!/bin/bash
set -euo pipefail
source /etc/backup-client/retention.env
{% if backup_backend == 'restic' %}
# restic backend
source /etc/backup-client/restic.env
restic forget \
--verbose \
--prune \
--group-by "host,paths,tags" \
--keep-hourly ${BACKUP_RETENTION_HOURS} \
--keep-daily ${BACKUP_RETENTION_DAYS} \
--keep-weekly ${BACKUP_RETENTION_WEEKS} \
--keep-monthly ${BACKUP_RETENTION_MONTHS} \
--keep-yearly ${BACKUP_RETENTION_YEARS}
{% endif %}

22
templates/backup-standalone.j2 Executable file
View file

@ -0,0 +1,22 @@
#!/bin/bash
set -euo pipefail
test -f "/etc/backup-client/enabled" || { echo "Standalone backup is disabled"; exit 0; }
{% if backup_backend == 'restic' %}
# restic backend
source /etc/backup-client/restic.env
restic backup \
--verbose \
--exclude-caches \
--one-file-system \
--exclude "${RESTIC_REPOSITORY}" \
--exclude-file "/etc/backup-client/exclude_files" \
--files-from "/etc/backup-client/include_files"
{% endif %}
{% if not backup_backend %}
echo "Noop, backup is handled external"
{% endif %}

95
templates/backup-vm.j2 Executable file
View file

@ -0,0 +1,95 @@
#!/bin/bash
set -euo pipefail
(
set -euo pipefail
flock -x -w 10 200 || exit 1
export LVM_SUPPRESS_FD_WARNINGS=1
DOMAIN=${1:?VM name musst be passed!}
DOMAIN_MOUNTBASE="/var/backup-client/vm-mountpoint"
function unfreeze_vm {
virsh resume "$DOMAIN" > /dev/null || true
}
function delete_snapshots {
cleanup_vmmount > /dev/null 2>&1
for i in $DISKS; do
extract_lvm
SNAPSHOT="/dev/$VG/backup-$DEVICE"
lvremove -f "$SNAPSHOT" > /dev/null 2>&1 || true
done
}
function cleanup_vmmount {
umount -l $DOMAIN_MOUNTBASE/* || true
rmdir ${DOMAIN_MOUNTBASE:?}/* || true
rm "$DOMAIN_MOUNTBASE/config.xml" || true
}
function extract_lvm {
DISK=`echo $i | awk -F, '{print $1}'`
DEVICE=`echo $i | awk -F, '{print $2}'`
VG=`lvs --noheadings -o vg_name "$DISK" | tr -d ' '`
LV=`lvs --noheadings -o lv_name "$DISK" | tr -d ' '`
}
function backup_vm {
# get a list of disks
DISKS=`virsh domblklist "$DOMAIN" --details \
| sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' \
| grep ^block \
| grep -v swap \
| awk '{printf "%s,%s ",$4, $3}'`
echo "backing up $DOMAIN: $DISKS"
trap "unfreeze_vm || true; delete_snapshots" INT TERM EXIT
# in case an earlier script crashed we have to clear snapshots
delete_snapshots
# freez vm
virsh suspend "$DOMAIN" > /dev/null || true
# create disk snapshots
for i in $DISKS; do
extract_lvm
lvcreate -L16G -s -n "backup-$DEVICE" "$DISK" > /dev/null
done
# dump vm config
virsh dumpxml "$DOMAIN" > "$DOMAIN_MOUNTBASE/config.xml"
unfreeze_vm
trap "delete_snapshots" INT TERM EXIT
# mount disks snapshots
for i in $DISKS; do
extract_lvm
SNAPSHOT="/dev/$VG/backup-$DEVICE"
fsck -y "$SNAPSHOT" > /dev/null 2> /dev/null
(
mkdir "$DOMAIN_MOUNTBASE/$DEVICE"
mount -o ro "$SNAPSHOT" "$DOMAIN_MOUNTBASE/$DEVICE"
) 2> /dev/null
done
{% if backup_backend == 'restic' %}
# restic backend
source /etc/backup-client/restic.env
restic backup \
--verbose \
--host "$DOMAIN" \
--exclude-file "/etc/backup-client/vms/$DOMAIN/exclude_files" \
"$DOMAIN_MOUNTBASE"
{% endif %}
# delete snapshot
delete_snapshots;
trap - INT TERM EXIT
}
test -f "/etc/backup-client/vms/$DOMAIN/enabled" && backup_vm || echo "Backup for $DOMAIN is disabled"
) 200>/var/lock/backup-vm.lock