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