Compare commits

...
12 Commits
Author SHA1 Message Date
lhahnandClaude Opus 5.5 2a19c705be Backup server: skip self-pruning repos in remote-prune
backup_server_remote_skip lists apps whose client prunes the repository itself; remote-prune without arguments leaves them alone (an explicit app list still works).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 22:26:36 +02:00
lhahnandClaude Opus 5.5 b09d1af493 Backup server: use backup_server_clients for keys and ssh config
Borg passphrase files are named by app; looping over all backup_clients let public-cloud-frontend overwrite the key of the live 'webserver' repo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:58:21 +02:00
lhahnandClaude Opus 5.5 c0155e3396 Backup server: incremental mirror, client subset, configurable compression
- local_backup rsynced every repository into an empty folder each night (full transfer); keep a persistent mirror (backup_server_mirror) and rsync --delete into it
- backup_server_clients: clients handled by the server (app names must be unique, e.g. two 'webserver' apps collided)
- backup_server_local_skip: apps that are pruned remotely but not mirrored
- backup_server_compression: borg repos are encrypted already, allow 'none'

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:44:47 +02:00
lhahn 1d99257ead Typos. 2024-11-27 22:37:12 +01:00
lhahn 3508f62085 Setup backup server configuration for backup replication. 2024-08-16 21:30:11 +02:00
lhahn 768da9cf45 Merge pull request 'Finish Borg script' (#2) from borg into main
Reviewed-on: #2
2024-07-25 21:55:46 +02:00
lhahn 0a47163789 Refactor borg to be backup and restore script. 2024-07-25 21:54:36 +02:00
lhahn 2e113e7219 templates/usr/local/bin/authentik-backup.sh.j2 aktualisiert 2024-07-24 13:49:50 +02:00
lhahn 7bc0a29fa0 templates/usr/local/bin/authentik-backup.sh.j2 aktualisiert
Storing idea about having nice backup script
2024-07-24 13:47:46 +02:00
lhahn 7ca52f7374 Save Authentik Backup and Restore Sketch 2024-07-23 21:23:56 +02:00
lhahn a295036f94 Merge pull request 'Setup basic Borg functions' (#1) from borg into main
Reviewed-on: #1
2024-06-23 18:58:20 +02:00
lhahn 4c6cc1e6da init commit borg 2024-06-23 18:51:36 +02:00
14 changed files with 778 additions and 289 deletions

No files matched your search

+43 -56
View File
@@ -3,67 +3,54 @@ cloud_apps: /opt
cloud_storage: /opt/storage
cloud_stage: prod
cloud_update: false
ssh_port: 1802
# clients push only daily backup
# server (!= client) will then pull daily backups
backup_client: true
# Server should pull 1 hour later the backups.
backup_hour_shift: 1
backup_times_weekday: "*"
backup_times_hour:
- 6
- 10
- 14
- 18
- 22
backup_times_minute: 0
backup_keep_days: 7
backup_owner: backup
backup_owner_ssh_key: "ssh-rsa THEREISSUCHRSAKEYYET"
backup_group: "{{ backup_owner }}"
backup_permission: 0770
backup_owner_ssh_private_key: "ssh-rsa THEREISNOSUCHRSAKEYYET"
backup_owner_ssh_public_key: "ssh-rsa THEREISNOSUCHRSAKEYYET"
backup_hosts:
- name: server1-name
domain: host1.my-domain.tld
ip: 001.112.223.123
- name: server2-name
domain: host2.my-domain.tld
backup_location: "/borg"
backup_folder: "{{ cloud_apps }}/backup"
backup_root: "{{ cloud_storage }}/backup"
backup_run_folder: "{{ cloud_storage }}"
#backup_client_scripts:
# - /path/to/script -param
backup_client_scripts: []
#backup_client_script_envs:
# - var: VAR_NAME
# path: /some/path
# - var: VAR_NAME
# value: some_value
backup_client_script_envs: []
backup_storage_key: "MyStorageKey"
backup_lock: "{{ backup_folder }}/backup.lock"
backup_client: true
backup_app: app
backup_cron_specialtime: "daily"
backup_cron_owner: "{{ backup_owner }}"
backup_server_remote_prune_time: 6
backup_server_remote_backup_time: 8
backup_server_local_prune_time: 10
#server: clients to prune/mirror (defaults to backup_clients), apps not mirrored locally, local compression
backup_server_clients: "{{ backup_clients }}"
backup_server_local_skip: []
#apps whose client prunes its own repository; remote-prune (without arguments) skips them
backup_server_remote_skip: []
backup_server_compression: lzma
#local mirror of the remote repositories (defaults to <backup_root>/storage/remote-backups)
#backup_server_mirror: /mnt/nas/backup/mirror
#backup_targets:
# db:
# - type: mariadb
# name: db_name_mdb
# - type: pgsql
# name: db_name_pg
# mail:
# - type: file
# path: /path/to/some/file
# - type: mariadb
# name: db_name_mdb_mail
# file:
# - /path/to/some/file
# - /path/to/some/dir
backup_targets:
db: []
mail: []
file: []
backup_script:
prework_backup: |
echo "This is executed before borg backup. Please collect data for backup in path: {{ backup_storage }}"
postwork_restore: |
echo "This is executed after borg restore. Please collect data during restore from path: {{ backup_storage }}"
backup_ssh_port: 22
backup_host: my-borg-repo.tld
backup_clients: []
# app1:
# owner: user1
# app: app1
# borgkey: BorgRepoKey1
# sshpubkey: SshPubKey1
# sshprivkey: SshPrivKey1
# app2:
# owner: user2
# app: app2
# borgkey: BorgRepoKey2
# sshpubkey: SshPubKey2
# sshprivkey: SshPrivKey2
+117
View File
@@ -0,0 +1,117 @@
from datetime import datetime
from dateutil.relativedelta import relativedelta
from argparse import ArgumentParser, FileType
def filter_backups_timeslot(offset, backups, timeslot='days', keep=7, mode='max', printy=True):
keep_backups = []
for timeslot_off in range(1, keep):
indices = [
idx
for idx,value in enumerate(backups)
if (
value[1] >= (offset - relativedelta(**{timeslot:timeslot_off}))
)
]
if not indices:
continue
if mode == "min":
#removed not considered backups for this slot!
for idx in reversed(indices[1:]):
backups.pop(idx)
keep_backups.append(backups.pop(indices[0]))
elif mode == "max":
keep_backups.append(backups.pop(indices[-1]))
#removed not considered backups for this slot!
for idx in reversed(indices[:-1]):
backups.pop(idx)
else:
for idx in sorted(indices, reverse=True):
keep_backups.append(backups.pop(idx))
return keep_backups, backups
def filter_backups(backuplist, show_keep=True, keep_hours=24, keep_days=7, keep_weeks=4, keep_months=12, keep_years=10):
backups = sorted(
[
(backup, datetime.fromtimestamp(int(backup.split('-')[1])))
for backup in backuplist
], key=lambda v: v[1], reverse=True
)
offset = backups[0][1]
keep_backups = [backups.pop(0)]
part_keep_backups, backups = filter_backups_timeslot(offset, backups, timeslot='hours',keep=keep_hours,mode='all')
keep_backups.extend(part_keep_backups)
part_keep_backups, backups = filter_backups_timeslot(offset, backups, timeslot='days',keep=keep_days,mode='max')
keep_backups.extend(part_keep_backups)
part_keep_backups, backups = filter_backups_timeslot(offset, backups, timeslot='weeks',keep=keep_weeks,mode='max')
keep_backups.extend(part_keep_backups)
part_keep_backups, backups = filter_backups_timeslot(offset, backups, timeslot='months',keep=keep_months,mode='max')
keep_backups.extend(part_keep_backups)
part_keep_backups, backups = filter_backups_timeslot(offset, backups, timeslot='years',keep=keep_years,mode='max')
keep_backups.extend(part_keep_backups)
keeps = [backup[0] for backup in keep_backups]
if not show_keep:
return [backup for backup in backuplist if backup not in keeps]
return keeps
def main():
parser = ArgumentParser(description="Program to print backups to keep or delete based on time stamps.")
parser.add_argument(
"-f", "--file", metavar="FILE", type=FileType('r'), required=True,
help="Path to file with list of backups to filter. Expects per line one backup name in format <appname>-<timestamp>")
parser.add_argument(
"-d", "--print-delete", action="store_true", required=False,
help="Instead of objects to kept, print out objects to be deleted."
)
parser.add_argument(
"-H", "--hours-keep", metavar="KEEP", type=int, required=False,
default=24, help="Number of objects to keep on hourly basis."
)
parser.add_argument(
"-D", "--days-keep", metavar="KEEP", type=int, required=False,
default=7, help="Number of objects to keep on daily basis."
)
parser.add_argument(
"-W", "--weeks-keep", metavar="KEEP", type=int, required=False,
default=4, help="Number of objects to keep on weekly basis."
)
parser.add_argument(
"-M", "--months-keep", metavar="KEEP", type=int, required=False,
default=12, help="Number of objects to keep on monthly basis."
)
parser.add_argument(
"-Y", "--years-keep", metavar="KEEP", type=int, required=False,
default=10, help="Number of objects to keep on yearly basis."
)
args = parser.parse_args()
show_keep = not args.print_delete
keep_hours = args.hours_keep
keep_days = args.days_keep
keep_weeks = args.weeks_keep
keep_months = args.months_keep
keep_years = args.years_keep
backuplist = [
line.strip()
for line in args.file
if len(line.strip()) > 0
]
fail = False
for idx,backup in enumerate(backuplist):
if len(backup) < 10 or not backup[-10:].isnumeric():
print(f"WARNING! Line {idx+1} has no timestamp suffix")
fail = True
if fail:
exit(1)
considered_backups = filter_backups(
backuplist, show_keep,
keep_hours, keep_days, keep_weeks, keep_months, keep_years
)
for backup in considered_backups:
print(backup)
if __name__ == '__main__':
main()
Executable → Regular
+21 -29
View File
@@ -1,38 +1,30 @@
---
- name: generate backup ssh user
user:
name: "{{ backup_owner }}"
- name: Setup ssh keys
copy:
content: "{{ item.content }}"
dest: "{{ backup_home }}/.ssh/{{ item.file }}"
owner: "{{ backup_owner }}"
group: "{{ backup_group }}"
comment: Backup user
shell: /bin/sh
- name: Setup User ssh_key
authorized_key:
user: "{{ backup_owner }}"
key: "{{ backup_owner_ssh_key }}"
- name: generate config lists
template:
src: backup/conf/{{ conf }}.list.j2
dest: "{{ backup_conf }}/{{ conf }}.list"
mode: "0600"
loop:
- filedir
- db
- mail
- content: "{{ backup_owner_ssh_private_key }}"
file: "id_rsa"
- content: "{{ backup_owner_ssh_public_key }}"
file: "id_rsa.pub"
loop_control:
loop_var: conf
label: "{{ conf }}.list"
label: "{{ item.file }}"
- name: setup backup script
template:
src: backup/backup_on_client.sh.j2
dest: "{{ backup_folder }}/backup.sh"
mode: 0740
src: "./usr/local/bin/cloud_backup.j2"
dest: "/usr/local/bin/cloud_backup"
owner: "{{ backup_owner }}"
group: "{{ backup_group }}"
mode: "0750"
- name: setup cronjob for client backup generation
- name: setup cron backup job
cron:
name: "backup"
weekday: "{{ backup_times_weekday }}"
hour: "{{ backup_times_hour | join(',') }}"
minute: "{{ backup_times_minute }}"
job: "{{ backup_folder }}/backup.sh"
name: "cloud backup"
user: "{{ backup_cron_owner }}"
job: "/usr/local/bin/cloud_backup backup"
special_time: "{{ backup_cron_specialtime }}"
+42 -17
View File
@@ -1,34 +1,59 @@
---
# Basics
- name: install requirements for backup system
apt:
update_cache: yes
state: "{% if cloud_update | bool %}latest{% else %}present{% endif %}"
install_recommends: yes
pkg:
- rsync
- borgbackup
- name: setup storage folders for backup
- name: setup backup location
file:
state: directory
path: "{{ dir.value.path }}"
path: "{{ backup_root }}"
owner: "root"
group: "root"
mode: "0755"
- name: Setup required groups of users
group:
name: "{{ backup_group }}"
state: present
- name: generate backup user
user:
name: "{{ backup_owner }}"
group: "{{ backup_group }}"
comment: backup user
shell: /bin/bash
home: "{{ backup_home }}"
- name: Setup backup storage password
copy:
content: "{{ backup_storage_key }}"
dest: "{{ backup_home }}/.borg.key"
owner: "{{ backup_owner }}"
group: "{{ backup_group }}"
mode: "{{ backup_permission }}"
with_dict: "{{ backup_storage }}"
loop_control:
loop_var: dir
label: "{{ dir.key }}"
mode: "0600"
- name: setup conf folder
- name: Setup ssh folder
file:
state: directory
path: "{{ backup_folder }}/conf"
path: "{{ backup_home }}/.ssh"
owner: "{{ backup_owner }}"
group: "{{ backup_group }}"
mode: "0700"
- name: setup client backup structure
include_tasks: client.yml
- name: setup backup storage
file:
state: directory
path: "{{ backup_storage }}"
owner: "{{ backup_owner }}"
group: "{{ backup_group }}"
mode: "0777"
- include_tasks: server.yml
when: not backup_client
- include_tasks: client.yml
when: backup_client
- name: setup server backup structure
include_tasks: server.yml
when: not backup_client
Executable → Regular
+112 -21
View File
@@ -1,27 +1,118 @@
---
- name: analyse backup hours for pulling server
set_fact:
backup_times_server_hour: "{{ backup_times_server_hour | default([]) + [item + backup_hour_shift] }}"
loop: "{{ backup_times_hour }}"
- name: setup backup installation folder
file:
state: directory
path: "{{ backup_inst }}"
owner: "{{ backup_owner }}"
group: "{{ backup_group }}"
mode: "0750"
- name: generate backup user ssh key
user:
name: "{{ backup_owner }}"
generate_ssh_key: yes
ssh_key_type: rsa
ssh_key_bits: 4096
home: "{{ backup_folder }}"
- name: install dateutil library service
apt:
update_cache: yes
state: "{% if cloud_update | bool %}latest{% else %}present{% endif %}"
install_recommends: yes
pkg:
- python3-dateutil
- rsync
- name: template main server backup pull script
- name: setup filter program for backup pruning
copy:
src: opt/backup/filter_backups.py
dest: "{{ backup_inst }}/filter_backups.py"
owner: "{{ backup_owner }}"
group: "{{ backup_group }}"
mode: "0750"
- name: setup ssh public keys of clients
copy:
content: "{{ backup_server_clients[item].sshpubkey }}"
dest: "{{ backup_home }}/.ssh/{{ item }}.pub"
owner: "{{ backup_owner }}"
group: "{{ backup_group }}"
mode: "0600"
loop: "{{ backup_server_clients.keys() }}"
- name: setup ssh private keys of clients
copy:
content: "{{ backup_server_clients[item].sshprivkey }}"
dest: "{{ backup_home }}/.ssh/{{ item }}"
owner: "{{ backup_owner }}"
group: "{{ backup_group }}"
mode: "0600"
loop: "{{ backup_server_clients.keys() }}"
- name: setup ssh config of clients
template:
src: backup/backup_from_clients.sh.j2
dest: "{{ backup_folder }}/pull-backup.sh"
mode: 0740
src: "home/backup/.ssh/config.j2"
dest: "{{ backup_home }}/.ssh/config"
owner: "{{ backup_owner }}"
group: "{{ backup_group }}"
mode: "0600"
- name: setup cronjob for server backup pull
- name: setup backup key folder
file:
state: directory
path: "{{ backup_key_folder }}"
owner: "{{ backup_owner }}"
group: "{{ backup_group }}"
mode: "0700"
- name: setup borg repository keys
copy:
content: "{{ backup_server_clients[item].borgkey }}"
dest: "{{ backup_key_folder }}/{{ backup_server_clients[item].app }}"
owner: "{{ backup_owner }}"
group: "{{ backup_group }}"
mode: "0600"
loop: "{{ backup_server_clients.keys() }}"
- name: setup local backup storage folder
file:
state: directory
path: "{{ item }}"
owner: "{{ backup_owner }}"
group: "{{ backup_group }}"
mode: "0750"
loop:
- "{{ backup_server_mirror | default(backup_remotes_folder) }}"
- "{{ backup_location }}/{{ backup_app }}"
- name: setup borg local repository
shell:
cmd: "BORG_PASSPHRASE=$(cat {{ backup_home }}/.borg.key) borg init --encryption repokey {{ backup_location }}/{{ backup_app }}"
creates: "{{ backup_location }}/{{ backup_app }}/config"
become_user: "{{ backup_owner }}"
become: yes
- name: setup borg server script
template:
src: "opt/backup/inst/cloud-server-backup.j2"
dest: "{{ backup_inst }}/cloud-server-backup"
owner: "{{ backup_owner }}"
group: "{{ backup_group }}"
mode: "0750"
- name: setup remote prune cron backup server job
cron:
name: "pull backups"
weekday: "{{ backup_times_weekday }}"
hour: "{{ backup_times_server_hour | join(',') }}"
minute: "{{ backup_times_minute }}"
job: "{{ backup_folder }}/pull-backup.sh"
name: "cloud server remote prune"
user: "{{ backup_cron_owner }}"
job: "{{ backup_inst }}/cloud-server-backup remote-prune"
hour: "{{ backup_server_remote_prune_time }}"
minute: 0
- name: setup remote backup cron backup server job
cron:
name: "cloud server remote backup"
user: "{{ backup_cron_owner }}"
job: "{{ backup_inst }}/cloud-server-backup backup"
hour: "{{ backup_server_remote_backup_time }}"
minute: 0
- name: setup local prune cron backup server job
cron:
name: "cloud server local prune"
user: "{{ backup_cron_owner }}"
job: "{{ backup_inst }}/cloud-server-backup prune"
hour: "{{ backup_server_local_prune_time }}"
minute: 0
@@ -1,68 +0,0 @@
#!/bin/bash
set -e -o pipefail
cd {{ backup_folder }}
if [ -f {{ backup_lock }} ]; then
echo "Backup lock! Cannot procceed as lock '{{ backup_lock }}' exist."
echo "Associated PID is $(cat {{ backup_lock }})"
exit 1
fi
echo "$$" > {{ backup_lock }}
BKPTIMESTAMP=$(date +"%y-%m-%d.%H")
BKP=$BKPTIMESTAMP"_backup"
# generate rolling folder
{% for target in backup_targets %}
{% for host in backup_hosts %}
{% if 'domain' in host %}
mkdir -p {{ target }}/{{ host.name + '_' + host.domain }}/$BKP
{% elif 'ip' in host %}
mkdir -p {{ target }}/{{ host.name + '_' + host.ip }}/$BKP
{% endif %}
{% endfor %}
{% endfor %}
# pull backups
{% for target in backup_targets %}
{% for host in backup_hosts %}
{% if 'domain' in host %}
rsync --rsh='ssh -p{{ ssh_port }} -i ~{{ backup_owner }}/.ssh/id_rsa' -chav --stats --ignore-missing-args {{ backup_owner }}@{{ host.domain }}:{{ backup_folder }}/{{ target }}.tgz {{ target }}/{{ host.name + '_' + host.domain }}/$BKP
{% elif 'ip' in host %}
rsync --rsh='ssh -p{{ ssh_port }} -i ~{{ backup_owner }}/.ssh/id_rsa' -chav --stats --ignore-missing-args {{ backup_owner }}@{{ host.ip }}:{{ backup_folder }}/{{ target }}.tgz {{ target }}/{{ host.name + '_' + host.ip }}/$BKP
{% endif %}
{% endfor %}
{% endfor %}
# link to latest
{% for target in backup_targets %}
{% for host in backup_hosts %}
{% if 'domain' in host %}
ln -sf {{ backup_folder }}/{{ target }}/{{ host.name + '_' + host.domain }}/$BKP {{ backup_folder }}/{{ target }}/{{ host.name + '_' + host.domain }}/LATEST
{% elif 'ip' in host %}
ln -sf {{ backup_folder }}/{{ target }}/{{ host.name + '_' + host.ip }}/$BKP {{ backup_folder }}/{{ target }}/{{ host.name + '_' + host.ip }}/LATEST
{% endif %}
{% endfor %}
{% endfor %}
# keep only last {{ backup_keep_days * ( backup_times_hour | length ) }}
{% for target in backup_targets %}
{% for host in backup_hosts %}
{% if 'domain' in host %}
DELETE_LIST=$(ls -1dtr {{ target }}/{{ host.name + '_' + host.domain }}/* | head -n -{{ backup_keep_days * ( backup_times_hour | length ) }})
{% elif 'ip' in host %}
DELETE_LIST=$(ls -1dtr {{ target }}/{{ host.name + '_' + host.ip }}/* | head -n -{{ backup_keep_days * ( backup_times_hour | length ) }})
{% endif %}
if (( $( echo $DELETE_LIST | wc -w ) > 0 )); then
rm -rf $DELETE_LIST
fi
{% endfor %}
{% endfor %}
rm {{ backup_lock }}
-72
View File
@@ -1,72 +0,0 @@
#!/bin/bash
set -e -o pipefail
if [ -f {{ backup_lock }} ]; then
echo "Backup lock! Cannot procceed as lock '{{ backup_lock }}' exist."
echo "Associated PID is $(cat {{ backup_lock }})"
exit 1
fi
echo "$$" > {{ backup_lock }}
cd {{ backup_folder }}
# Load script envs
echo "Load script environment variables"
{% for scr_env in backup_client_script_envs %}
{% if 'path' in scr_env %}
export {{ scr_env.var }}={{ scr_env.path }}:${{ scr_env.var }}
{% else %}
export {{ scr_env.var }}={{ scr_env.value }}
{% endif %}
{% endfor %}
# Perform client pre backup scripts for
echo "Perform pre-backup scripts"
{% for cmd in backup_client_scripts %}
{{ cmd }};
{% endfor %}
# Backup files and directories
echo "Copy files and directories"
while read DIRFILE; do
echo " - Copy $DIRFILE"
BACKUP_SUBDIR=$(dirname $DIRFILE)
mkdir -p "{{ backup_storage.file.path }}/$BACKUP_SUBDIR"
cp -r -u $DIRFILE "{{ backup_storage.file.path }}/$DIRFILE"
done < {{ backup_conf }}/filedir.list
rm -f file.tgz #remove old backup
tar czfv file.tgz file
rm -rf file/*
# Backup databases
echo "Generate DB dumps"
while read DBS; do
DBTYPE=$(echo $DBS | cut -f 1 -d "{{ backup_conf_db_sep }}" );
DBNAME=$(echo $DBS | cut -f 2 -d "{{ backup_conf_db_sep }}" );
if [ $DBTYPE == "mariadb" ] || [ $DBTYPE == "mysql" ]; then
echo "- dump to {{ backup_storage.db.path }}/$DBTYPE/$DBNAME.sql"
mkdir -p {{ backup_storage.db.path }}/$DBTYPE;
mysqldump $DBNAME | gzip -c > {{ backup_storage.db.path }}/$DBTYPE/$DBNAME.sql.gz;
elif [ $DBTYPE == "pgsql" ]; then
echo "- dump to {{ backup_storage.db.path }}/$DBTYPE/$DBNAME.sql"
mkdir -p {{ backup_storage.db.path }}/$DBTYPE;
sudo -u postgres bash -c "pg_dump $DBNAME | gzip -c > /tmp/$DBNAME.sql.gz";
mv /tmp/$DBNAME.sql.gz /backup/db/$DBTYPE/$DBNAME.sql.gz;
else
echo "- ERROR! DB type '$DBTYPE' for '$DBNAME' is unknown to backup."
fi
done < {{ backup_conf }}/db.list
rm -f db.tgz #remove old backup
tar czfv db.tgz db
rm -rf db/*
#Mail
rm -f mail.tgz #remove old backup
tar czfv mail.tgz mail
rm -rf mail/*
rm {{ backup_lock }}
chown {{ backup_owner }}:{{ backup_group }} {{ backup_folder }}/*.tgz
-5
View File
@@ -1,5 +0,0 @@
{% if 'db' in backup_targets %}
{% for db in backup_targets.db %}
{{ db.type }}{{ backup_conf_db_sep }}{{ db.name }}
{% endfor %}
{% endif %}
-5
View File
@@ -1,5 +0,0 @@
{% if 'file' in backup_targets %}
{% for filedir in backup_targets.file %}
{{ filedir }}
{% endfor %}
{% endif %}
-5
View File
@@ -1,5 +0,0 @@
{% if 'mail' in backup_targets %}
{% for mail in backup_targets.mail %}
{{ mail }}
{% endfor %}
{% endif %}
+7
View File
@@ -0,0 +1,7 @@
{% for client in backup_server_clients %}
Host {{ client }}
HostName {{ backup_host }}
Port {{ backup_ssh_port }}
User {{ backup_server_clients[client].owner }}
IdentityFile {{ backup_home }}/.ssh/{{ client }}
{% endfor %}
@@ -0,0 +1,285 @@
{% set server_clients = backup_server_clients | default(backup_clients) %}
#!/bin/bash
BORGUSER="{{ backup_owner }}";
TARGETFOLDER="{{ backup_remotes_folder }}";
LOCALREPOLOCATION="{{ backup_location }}/{{ backup_app }}";
APPLIST="{{ server_clients | json_query('*.app') | join(' ') }}";
# repos that are only pruned remotely, not mirrored locally
LOCALAPPLIST="{{ server_clients | json_query('*.app') | difference(backup_server_local_skip) | join(' ') }}";
# repos pruned by their own client (e.g. a NAS borg plugin), skipped by remote-prune without arguments
REMOTEPRUNEAPPLIST="{{ server_clients | json_query('*.app') | difference(backup_server_remote_skip) | join(' ') }}";
MIRRORFOLDER="{{ backup_server_mirror | default(backup_remotes_folder) }}";
declare -A BORG_CLIENT_MAP
{% for client in server_clients %}
BORG_CLIENT_MAP[{{ server_clients[client].app }}]="{{ client }}";
{% endfor %}
declare -A BORG_USER_MAP
{% for client in server_clients %}
BORG_USER_MAP[{{ server_clients[client].app }}]="{{ server_clients[client].owner }}";
{% endfor %}
help (){
echo "cloud-server-backup - backup and restore script on cloud backup with borg target v1.0 by L.Hahn.
Usage: $0 COMMAND [APPS]
COMMAND:
- remote-apps-list List available apps in remote borg repositories.
- remote-list [APPS] List available archives for apps in remote borg repositories.
- remote-prune [APPS] Prune remote app (or all) borg repositories.
- list List available archives of remote repositories on local repository.
- backup Perform backup of remote borg repositories and create a new archive in local borg repository.
- prune Prune local borg backup of remote repositories.
- restore ARCHIVENAME [APPS] Upload backup to borg repository to restore older file states.
May turn off your application if still running.
If latest is provided as ARCHIVENAME, the latest one based on timestamp is taken.
IF ARCHIVENAME is provided, will try to download it; throws error if not found.
If APP is provided, only the mentioned app repositories are restored.
APPS: A single app or comma-separated (sub-)set of below listed apps to be considered.
{% for client in server_clients %}
- {{ server_clients[client].app }}
{% endfor %}
";
}
###############################################################################
#=== HELP FUNCTIONS ==========================================================#
###############################################################################
remote_delete_single_backup () {
REPOLOCATION=$1;
ARCHIVENAME=$2;
BORG_PASSPHRASE=$3;
KEYFILE=$4;
echo "Remove archive $ARCHIVENAME";
sudo -H -u $BORGUSER bash -c '
ARCHIVENAME='$ARCHIVENAME';
export BORG_PASSPHRASE='$BORG_PASSPHRASE';
KEYFILE='$KEYFILE';
REPOLOCATION='$REPOLOCATION';
borg delete $REPOLOCATION::$ARCHIVENAME --rsh "/usr/bin/ssh -i $KEYFILE"';
}
remote_free_space () {
REPOLOCATION=$1;
KEYFILE=$2;
APP=$3;
echo "Free space on repository for app '$APP'.";
sudo -H -u $BORGUSER bash -c '
KEYFILE='$KEYFILE';
REPOLOCATION='$REPOLOCATION';
borg compact $REPOLOCATION --rsh "/usr/bin/ssh -i $KEYFILE"';
}
has_argument () {
if [[ -z "$1" ]];
then
echo "Missing required paramter!";
exit 1;
fi
}
validate_apps_or_default () {
VALID=1;
if [[ -z "$1" ]];
then
echo $APPLIST;
else
CHECKLIST=$(echo $1 | tr ',' ' ');
for APP in $CHECKLIST;
do
if [[ ! ${APPLIST[@]} =~ $APP ]]
then
VALID=0;
echo "$APP is not a valid app.";
fi
done
if [[ ! $VALID == 1 ]]
then
echo "Aborting.";
exit 1;
else
echo $CHECKLIST;
fi
fi
}
###############################################################################
#=== USECASE FUNCTIONS =======================================================#
###############################################################################
remote_list_app () {
for APP in $APPLIST;
do
echo $APP;
done
}
remote_list () {
REMOTELIST=$1;
for APP in $REMOTELIST;
do
REPOSITORYCLIENT=${BORG_CLIENT_MAP[$APP]}
BORG_REPO_USER=${BORG_USER_MAP[$APP]}
REPOLOCATION="ssh://$BORG_REPO_USER@{{ backup_host }}:{{ backup_ssh_port }}/./$APP"
BORG_PASSPHRASE=$(cat {{ backup_key_folder }}/$APP)
KEYFILE="{{ backup_home }}/.ssh/$REPOSITORYCLIENT"
ARCHIVEIDS=$(
sudo -H -u $BORGUSER bash -c '
export BORG_PASSPHRASE='$BORG_PASSPHRASE';
KEYFILE='$KEYFILE';
REPOLOCATION='$REPOLOCATION';
borg list $REPOLOCATION --rsh "/usr/bin/ssh -i $KEYFILE"' | sort -r
);
echo "$ARCHIVEIDS";
done
}
remote_prune () {
REMOTEPRUNELIST=$1;
for APP in $REMOTEPRUNELIST;
do
BORG_REPO_USER=${BORG_USER_MAP[$APP]}
BORG_PASSPHRASE=$(cat {{ backup_key_folder }}/$APP)
REPOLOCATION="ssh://$BORG_REPO_USER@{{ backup_host }}:{{ backup_ssh_port }}/./$APP"
KEYFILE="{{ backup_home }}/.ssh/${BORG_CLIENT_MAP[$APP]}"
sudo -H -u $BORGUSER bash -c '
KEYFILE='$KEYFILE';
REPOLOCATION='$REPOLOCATION';
export BORG_PASSPHRASE='$BORG_PASSPHRASE';
borg prune -H 24 -d 7 -w 4 -m -1 -y 100 $REPOLOCATION --rsh "/usr/bin/ssh -i $KEYFILE"';
remote_free_space $REPOLOCATION $KEYFILE $APP;
done
}
local_list () {
ARCHIVEIDS=$(
sudo -H -u $BORGUSER bash -c '
REPOLOCATION='$LOCALREPOLOCATION';
export BORG_PASSPHRASE=$(cat {{ backup_home }}/.borg.key);
borg list $REPOLOCATION' | grep {{ backup_app }} | sort -r);
echo "$ARCHIVEIDS";
}
local_prune () {
sudo -H -u $BORGUSER bash -c '
KEYFILE='$KEYFILE';
REPOLOCATION='$LOCALREPOLOCATION';
export BORG_PASSPHRASE=$(cat {{ backup_home }}/.borg.key);
borg prune -H 24 -d 7 -w 4 -m -1 -y 100 $REPOLOCATION';
echo "Free space on local repository.";
sudo -H -u $BORGUSER bash -c '
REPOLOCATION='$LOCALREPOLOCATION';
borg compact $REPOLOCATION';
}
local_backup () {
ARCHIVENAME="{{ backup_app }}-$(date '+%s')";
mkdir -p $MIRRORFOLDER;
chown $BORGUSER: $MIRRORFOLDER;
# keep a persistent mirror so rsync only transfers what changed
for APP in $LOCALAPPLIST;
do
sudo -H -u $BORGUSER bash -c '
MIRRORFOLDER='$MIRRORFOLDER';
REPOLOCATION='${BORG_CLIENT_MAP[$APP]}':'$APP';
rsync -ar --delete $REPOLOCATION $MIRRORFOLDER/;';
done
# borg repositories are encrypted already, compression rarely helps
sudo -H -u $BORGUSER bash -c '
MIRRORFOLDER='$MIRRORFOLDER';
REPOLOCATION='$LOCALREPOLOCATION';
ARCHIVENAME='$ARCHIVENAME';
export BORG_PASSPHRASE=$(cat {{ backup_home }}/.borg.key);
borg create -C {{ backup_server_compression }} $REPOLOCATION::$ARCHIVENAME $MIRRORFOLDER;';
}
local_delete () {
ARCHIVENAME=$1;
ARCHIVEIDS=$(local_list | cut -f 1 -d ' ');
if [[ "$ARCHIVEIDS" != *$ARCHIVENAME* ]];
then
echo "ERROR! Provided archivename '$ARCHIVENAME' is not part of the available archives! Aborting.";
exit 1;
fi
echo "Remove archive $ARCHIVENAME";
sudo -H -u $BORGUSER bash -c '
ARCHIVENAME='$ARCHIVENAME';
export BORG_PASSPHRASE='$(cat {{ backup_home }}/.borg.key)';
REPOLOCATION='$LOCALREPOLOCATION';
borg delete $REPOLOCATION::$ARCHIVENAME';
}
###############################################################################
#=== MAIN FUNCTIONS ==========================================================#
###############################################################################
(
flock -n 9 || {
echo "BACKUP ALREADY RUNNING! ABORTING.";
exit 1;
}
if [ $# = 0 ]; then
help;
exit 0;
fi
action=$1
case $action in
"remote-apps-list")
remote_list_app;
;;
"remote-list")
USERLIST=$(validate_apps_or_default "$2");
remote_list "$USERLIST";
;;
"remote-prune")
if [[ -z "$2" ]]; then
USERLIST=$REMOTEPRUNEAPPLIST;
else
USERLIST=$(validate_apps_or_default "$2");
fi
remote_prune "$USERLIST";
;;
"list")
local_list;
;;
"prune")
local_prune;
;;
"backup")
local_backup;
;;
"restore")
has_argument $2;
USERLIST=$(validate_apps_or_default $3);
#local_restore $2;
;;
"delete")
has_argument $2;
local_delete $2;
;;
*)
help;
exit 0;
;;
esac
) 9>/var/run/lock/cloud-server-backup.lock;
+146
View File
@@ -0,0 +1,146 @@
#!/bin/bash
BORGUSER="{{ backup_owner }}";
RUNFOLDER="{{ backup_run_folder }}";
TARGETFOLDER="{{ backup_storage }}";
REPOLOCATION="{{ backup_location }}";
ARCHIVENAME="{{ backup_app }}-$(date '+%s')";
help (){
echo "cloud_backup - backup and restore script on cloud with borg target v1.0 by L.Hahn.
Usage: $0 COMMAND [ARCHIVENAME]
COMMAND:
- list List available archives in remote borg repository for your host.
- backup Perform backup of your host and create a new archive in borg repository.
- restore [ARCHIVENAME] Download backup from borg repository to your host and restore files.
May turn off your application if still running.
If no ARCHIVENAME is provided, the latest one based on timestamp is taken.
IF ARCHIVENAME is provided, will try to download it; throws error if not found.
";
}
### INDIVIDUAL TEMPLATE PART ###
prework_backup () {
{{ backup_script.prework_backup | indent( width=4) }}
}
postwork_restore () {
{{ backup_script.postwork_restore | indent( width=4) }}
}
### SHARED TEMPLATE PART ###
get_archives () {
ARCHIVEIDS=$(
sudo -H -u $BORGUSER bash -c '
REPOLOCATION='$REPOLOCATION';
export BORG_PASSPHRASE=$(cat {{ backup_home }}/.borg.key);
borg list $REPOLOCATION' | grep {{ backup_app }} | sort -r);
echo "$ARCHIVEIDS";
}
cloud_backup () {
mkdir -p $TARGETFOLDER/$ARCHIVENAME;
prework_backup;
cd /;
sudo -H -u $BORGUSER bash -c '
TARGETFOLDER='$TARGETFOLDER';
REPOLOCATION='$REPOLOCATION';
ARCHIVENAME='$ARCHIVENAME';
export BORG_PASSPHRASE=$(cat {{ backup_home }}/.borg.key);
borg create -C lzma $REPOLOCATION::$ARCHIVENAME $TARGETFOLDER/$ARCHIVENAME';
rm -rf $TARGETFOLDER/$ARCHIVENAME;
}
cloud_restore () {
ARCHIVENAME=$1;
ARCHIVEIDS=$(get_archives | cut -f 1 -d ' ');
if [[ "${ARCHIVENAME,,}" == "latest" ]];
then
ARCHIVENAME=$(echo "$ARCHIVEIDS" | head -n 1);
else
if [[ "$ARCHIVEIDS" != *$ARCHIVENAME* ]];
then
echo "ERROR! Provided archivename '$ARCHIVENAME' is not part of the available archives! Aborting.";
exit 1;
fi
fi
mkdir -p $TARGETFOLDER/$ARCHIVENAME;
chown -R $BORGUSER: $TARGETFOLDER
cd /;
sudo -H -u $BORGUSER bash -c '
REPOLOCATION='$REPOLOCATION';
ARCHIVENAME='$ARCHIVENAME';
export BORG_PASSPHRASE=$(cat {{ backup_home }}/.borg.key);
borg extract $REPOLOCATION::$ARCHIVENAME --list';
postwork_restore;
rm -rf $TARGETFOLDER/$ARCHIVENAME;
}
cloud_backup_delete () {
ARCHIVENAME=$1;
ARCHIVEIDS=$(get_archives | cut -f 1 -d ' ');
if [[ "$ARCHIVEIDS" != *$ARCHIVENAME* ]];
then
echo "ERROR! Provided archivename '$ARCHIVENAME' is not part of the available archives! Aborting.";
exit 1;
fi
sudo -H -u $BORGUSER bash -c '
REPOLOCATION='$REPOLOCATION';
ARCHIVENAME='$ARCHIVENAME';
export BORG_PASSPHRASE=$(cat {{ backup_home }}/.borg.key);
borg delete $REPOLOCATION::$ARCHIVENAME';
}
has_argument () {
if [[ -z "$1" ]];
then
echo "Missing required paramter!";
exit 1;
fi
}
(
flock -n 9 || {
echo "BACKUP ALREADY RUNNING! ABORTING.";
exit 1;
}
if [ $# = 0 ]; then
help;
exit 0;
fi
action=$1
case $action in
"list")
get_archives;
;;
"backup")
cloud_backup;
;;
"restore")
has_argument $2;
cloud_restore $2;
;;
"delete")
has_argument $2;
cloud_backup_delete $2;
;;
*)
help;
exit 0;
;;
esac
) 9>/var/run/lock/cloud-backup.lock;
+5 -11
View File
@@ -1,12 +1,6 @@
---
backup_conf: "{{ backup_folder }}/conf"
backup_conf_db_sep: ":"
backup_storage:
mail:
path: "{{ backup_folder }}/mail"
db:
path: "{{ backup_folder }}/db"
file:
path: "{{ backup_folder }}/file"
backup_home: "{{ backup_root }}/home"
backup_inst: "{{ backup_root }}/inst"
backup_storage: "{{ backup_root }}/storage"
backup_key_folder: "{{ backup_home }}/.keys"
backup_remotes_folder: "{{ backup_storage }}/remote-backups"