Initial commit
This commit is contained in:
commit
420be44f56
11 changed files with 432 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
__pycache__
|
||||||
44
README.md
Normal file
44
README.md
Normal 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
20
defaults/main.yml
Normal 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
27
filter_plugins/filters.py
Executable 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
170
tasks/main.yml
Normal 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
6
templates/backup-all-vms.j2
Executable 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
15
templates/backup-cronjob.j2
Executable 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
13
templates/backup-full.j2
Executable 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
19
templates/backup-retention.j2
Executable 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
22
templates/backup-standalone.j2
Executable 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
95
templates/backup-vm.j2
Executable 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue