commit e4097afe2bb78dd91890f90f1a05da6a73021540 Author: Luc Stroobant Date: Thu Sep 7 20:35:01 2017 +0200 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..9f8b2c0 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# Borg backup role +This role installs Borg backup on backupservers and clients. The role contains a wrapper-script 'borg-backup' to ease the usage on the client. Supported options include borg-backup info | init | list | backup | mount. Automysqlbackup will run as pre-backup command if it's installed. +The role supports both self hosted and rsync.net as Borg server. + + +## Required variables +Define a group backupservers in your inventory with one or multiple hosts. The default location where the backups will be saved is /var/backup/repos/. +``` +infra: +[backupservers] +backup1.fiaas.co +``` + +group\_vars/all.yml: +``` +backupservers: + - fqdn: backup1.fiaas.co + user: borgbackup + type: normal + home: /backup/ + pool: repos + options: "" + - fqdn: yourhost.rsync.net + user: userid + type: rsync.net + home: "" + pool: repos + options: "--remote-path=borg1" +``` +Contains the list of server you want to use on a certain client. +Allows to override backup servers on group or host level. +*WARNING: the trailing / in item.home is required.* + +Define a borg\_passphrase for every host. +host\_vars\client1: +``` +borg\_passphrase: Ahl9EiNohr5koosh1Wohs3Shoo3ooZ6p +``` + +*Make sure to check the configured defaults for this role, which contains the list of default locations being backed up in backup_include.* Override this in your inventory where required. + +## Usage + +Configure Borg on the server and on a client: +``` +ansible-playbook -i inventory/test playbooks/backup.yml -l backup1.fiaas.co +ansible-playbook -i inventory/test playbooks/backup.yml -l client1.fiaas.co +``` + +## Further reading +https://borgbackup.readthedocs.io/en/stable/ diff --git a/defaults/main.yml b/defaults/main.yml new file mode 100644 index 0000000..b4b4d58 --- /dev/null +++ b/defaults/main.yml @@ -0,0 +1,25 @@ +--- +backup_required: True +restore: False + +borg_version: "1.0.10" +borg_checksum: "sha256:99f889e57630e64a67d0c1a54c056f8d82aac1ccc2cc56c9fb5f5f2710f29950" +borg_download_url: "https://github.com/borgbackup/borg/releases/download/{{ borg_version }}/borg-linux64" + +backup_pre_commands: + - '[[ ! -f "/usr/sbin/automysqlbackup" ]] || /usr/sbin/automysqlbackup' + - "dpkg --get-selections > /root/.installed-software.log" + +backup_include: + - "/etc" + - "/home" + - "/root" + - "/var/www" + - "/var/log" + +retention: + hourly: 12 + daily: 7 + weekly: 4 + monthly: 6 + yearly: 1 diff --git a/tasks/borg-client.yml b/tasks/borg-client.yml new file mode 100644 index 0000000..c5642f8 --- /dev/null +++ b/tasks/borg-client.yml @@ -0,0 +1,106 @@ +--- +- name: client | generate ssh key for this machine + shell: if [ -f ~/.ssh/id_rsa ]; then rm -f ~/.ssh/id_rsa; fi && ssh-keygen -q -t rsa -b 4096 -f ~/.ssh/id_rsa -N "" creates=~/.ssh/id_rsa.pub + +- name: client | fetch ssh-key + shell: cat /root/.ssh/id_rsa.pub + register: sshkey + changed_when: False + +- name: client | write passphrase + lineinfile: + dest: "/root/.borg.passphrase" + state: "present" + line: 'export BORG_PASSPHRASE="{{ borg_passphrase }}"' + create: "yes" + +- name: client | template sshconfig for backup-hosts (no strict key checking) + template: + src: "ssh.config.j2" + dest: "/root/.ssh/config" + owner: "root" + group: "root" + +- name: client | put sshpubkey on the normal backupserver + authorized_key: + user: "{{ item.user }}" + key: "{{ sshkey.stdout }}" + key_options: 'command="cd {{ item.home }}{{ item.pool }}/{{ inventory_hostname }};borg serve --restrict-to-path {{ item.home }}/{{ item.pool }}/{{ inventory_hostname }}",no-port-forwarding,no-X11-forwarding,no-pty,no-agent-forwarding,no-user-rc' + delegate_to: "{{ item.fqdn }}" + when: item.type == 'normal' + with_items: "{{ backupservers }}" + +# rsync.net has no python, so we can only use raw to manage ssh keys - workaround with local tmp file +- name: client | get rsync.net authorized_keys file + raw: scp {{ item.user }}@{{ item.fqdn }}:.ssh/authorized_keys /tmp/rsync.net-{{ item.fqdn }}-authkeys + delegate_to: localhost + become: no + when: item.type == 'rsync.net' + with_items: "{{ backupservers }}" + changed_when: false + +- name: client | modify local rsync.net authorized_keys + authorized_key: + user: "{{ ansible_user_id }}" + key: "{{ sshkey.stdout }}" + key_options: 'command="cd {{ item.home }}{{ item.pool }}/{{ inventory_hostname }};borg serve --restrict-to-path {{ item.home }}/{{ item.pool }}/{{ inventory_hostname }}",no-port-forwarding,no-X11-forwarding,no-pty,no-agent-forwarding,no-user-rc' + path: "/tmp/rsync.net-{{ item.fqdn }}-authkeys" + manage_dir: no + delegate_to: localhost + become: no + when: item.type == 'rsync.net' + with_items: "{{ backupservers }}" + register: authkeys + +- name: client | upload local authorized_keys to rsync.net + raw: scp /tmp/rsync.net-{{ item.fqdn }}-authkeys {{ item.user }}@{{ item.fqdn }}:.ssh/authorized_keys + delegate_to: localhost + become: no + when: item.type == 'rsync.net' and authkeys.changed + with_items: "{{ backupservers }}" + +- name: client | remove tmp authorized_keys files + file: + path: /tmp/rsync.net-{{ item.fqdn }}-authkeys + state: absent + delegate_to: localhost + become: no + with_items: "{{ backupservers }}" + when: authkeys.changed + changed_when: false + +- name: client | check for mysql + stat: path=/var/lib/automysqlbackup + register: automysql + +- name: client | put wrapper script + template: + src: "borg-backup.sh.j2" + dest: "/usr/local/bin/borg-backup" + owner: "root" + group: "root" + mode: "0744" + +- name: client | create backup-directory on backup server + shell: /usr/local/bin/borg-backup init + register: backup_init + changed_when: "'Remember your passphrase' in backup_init.stderr" + +- name: client | create backup cronjob + cron: + cron_file: "borg-backup" + user: "root" + name: "borg-backup" + minute: "{{ 59|random }}" + hour: "{{ 5|random }}" + job: "/usr/local/bin/borg-backup backup" + +- name: client | disable automysqlbackup cronjob, it's in our pre-backup-tasks + lineinfile: + dest: "/etc/cron.daily/automysqlbackup" + regexp: "^/usr/sbin/automysqlbackup$" + line: "#/usr/sbin/automysqlbackup" + state: "present" + backrefs: "yes" + create: "no" + when: automysql.stat.isdir is defined and automysql.stat.isdir == True diff --git a/tasks/borg-server.yml b/tasks/borg-server.yml new file mode 100644 index 0000000..d540e3a --- /dev/null +++ b/tasks/borg-server.yml @@ -0,0 +1,30 @@ +--- +- name: server | install borg backup + get_url: + dest: "/usr/local/bin/borg" + checksum: "{{ borg_checksum }}" + owner: "root" + group: "root" + mode: "0755" + url: "{{ borg_download_url }}" + delegate_to: "{{ item.fqdn }}" + with_items: "{{ backupservers }}" + when: item.type == 'normal' + +- name: server | create user + user: + name: "{{ item.user }}" + shell: "/bin/bash" + home: "{{ item.home }}" + createhome: "yes" + delegate_to: "{{ item.fqdn }}" + with_items: "{{ backupservers }}" + when: item.type == 'normal' + +- name: server | create directories + file: + path: "{{ item.pool }}" + state: "directory" + owner: "{{ item.user }}" + group: "{{ item.user }}" + mode: "0770" diff --git a/tasks/install.yml b/tasks/install.yml new file mode 100644 index 0000000..f534fcd --- /dev/null +++ b/tasks/install.yml @@ -0,0 +1,10 @@ +--- +- name: install borg backup + get_url: + dest: "/usr/local/bin/borg" + checksum: "{{ borg_checksum }}" + owner: "root" + group: "root" + mode: "0755" + url: "{{ borg_download_url }}" + tags: borginstall diff --git a/tasks/main.yml b/tasks/main.yml new file mode 100644 index 0000000..076a41c --- /dev/null +++ b/tasks/main.yml @@ -0,0 +1,13 @@ +--- + +- include: install.yml + when: backup_required == True or inventory_hostname in groups.backupservers or restore == True + +- include: borg-server.yml + when: inventory_hostname in groups.backupservers + +- include: borg-client.yml + when: backup_required == True and inventory_hostname not in groups.backupservers + +- include: restore.yml + when: restore == True and inventory_hostname not in groups.backupservers diff --git a/tasks/restore.yml b/tasks/restore.yml new file mode 100644 index 0000000..64c10a3 --- /dev/null +++ b/tasks/restore.yml @@ -0,0 +1,42 @@ +--- +- name: client | generate ssh key for this machine + shell: if [ -f ~/.ssh/id_rsa ]; then rm -f ~/.ssh/id_rsa; fi && ssh-keygen -q -t rsa -b 4096 -f ~/.ssh/id_rsa -N "" creates=~/.ssh/id_rsa.pub + +- name: client | fetch ssh-key + shell: cat /root/.ssh/id_rsa.pub + register: sshkey + changed_when: False + +- name: client | write passphrase + lineinfile: + dest: "/root/.borg.passphrase" + state: "present" + line: 'export BORG_PASSPHRASE="{{ borg_passphrase }}"' + create: "yes" + +- name: client | template sshconfig for backup-hosts (no strict key checking) + template: + src: "ssh.config.j2" + dest: "/root/.ssh/config" + owner: "root" + group: "root" + +- name: client | place sshpubkey on the backupserver + authorized_key: + user: "{{ borg_user }}" + key: "{{ sshkey.stdout }}" + key_options: 'command="cd {{ borg_pool }}/{{ restore_from_vm }};borg serve --restrict-to-path {{ borg_pool }}/{{ restore_from_vm }}",no-port-forwarding,no-X11-forwarding,no-pty,no-agent-forwarding,no-user-rc' + delegate_to: "{{ item }}" + with_items: "{{ groups.backupservers }}" + +- name: client | check for mysql + stat: path=/var/lib/automysqlbackup + register: automysql + +- name: client | put wrapper script + template: + src: "borg-restore-from.sh.j2" + dest: "/usr/local/bin/borg-restore-from" + owner: "root" + group: "root" + mode: "0744" diff --git a/templates/borg-backup.sh.j2 b/templates/borg-backup.sh.j2 new file mode 100644 index 0000000..f88c336 --- /dev/null +++ b/templates/borg-backup.sh.j2 @@ -0,0 +1,78 @@ +#!/bin/bash + +if [ -z "$1" ] || [ ! -z "$2" ] + then + printf "Possible: info | init | list | backup | mount \n\n" +fi + +# Sourcing the backup-passphrase + +. /root/.borg.passphrase + +# Small helper commands, like listing backups, will help us in the future :) + +if [ "$1" = "info" ] + then + if [ -z "$2" ]; then printf "run $0 with list and use the backup-tag to request information\n"; exit 1; fi +{% for b in backupservers %} + REPOSITORY={{ b.user }}@{{ b.fqdn }}:{{ b.home }}{{ b.pool }}/{{ inventory_hostname }} + /usr/local/bin/borg info $REPOSITORY::$2 {{ b.options }} +{% endfor %} + exit 0 +fi + +if [ "$1" = "mount" ] + then + if [ -z "$2" ]; then printf "Select the backup-server\n"; exit 1; fi + if [ -z "$3" ]; then printf "Select the backup to mount\n"; exit 1; fi + if [ -z "$4" ]; then printf "Select the path to mount the backup on\n"; exit 1; fi +{% for b in backupservers %} + REPOSITORY={{ b.user }}@{{ b.fqdn }}:{{ b.home }}{{ b.pool }}/{{ inventory_hostname }} + /usr/local/bin/borg mount $REPOSITORY::$3 $4 {{ b.options }} + if [ "$?" = "0" ]; then printf "Backup mounted on $4, do not forget to unmount!\n"; fi + exit 0 +{% endfor %} +fi + +if [ "$1" = "list" ] + then +{% for b in backupservers %} + REPOSITORY={{ b.user }}@{{ b.fqdn }}:{{ b.home }}{{ b.pool }}/{{ inventory_hostname }} + printf "Archives on {{ b.fqdn }} :\n" + /usr/local/bin/borg list -v $REPOSITORY {{ b.options }} +{% endfor %} + exit 0 +fi + +if [ "$1" = "init" ] + then +{% for b in backupservers %} + REPOSITORY={{ b.user }}@{{ b.fqdn }}:{{ b.home }}{{ b.pool }}/{{ inventory_hostname }} + borg init $REPOSITORY {{ b.options }} +{% endfor %} + exit 0 +fi + +if [ "$1" = "backup" ] + then + date=`date +%Y%m%d-%H%M` + + # Running some commands pre-backup +{% for precommand in backup_pre_commands %} + {{ precommand }} +{% endfor %} + +{% for b in backupservers %} + printf "Backing up to {{ b.fqdn }} :\n" + REPOSITORY={{ b.user }}@{{ b.fqdn }}:{{ b.home }}{{ b.pool }}/{{ inventory_hostname }} + + /usr/local/bin/borg create --compression zlib,6 --stats $REPOSITORY::$date {{ b.options }} {% for dir in backup_include %}{{ dir }} {% endfor %}{% if automysql.stat.isdir is defined and automysql.stat.isdir == True %}/var/lib/automysqlbackup{% endif %} + + if [ "$?" -eq "0" ]; then printf "Backup succeeded on $date\n" >> /var/log/borg-backup.log; fi + + # Use the `prune` subcommand to maintain 7 daily, 4 weekly + # and 6 monthly archives. + /usr/local/bin/borg prune -v $REPOSITORY {{ b.options }} -H {{ retention.hourly }} -d {{ retention.daily }} -w {{ retention.weekly }} -m {{ retention.monthly }} -y {{ retention.yearly }} +{% endfor %} +fi + diff --git a/templates/borg-restore-from.sh.j2 b/templates/borg-restore-from.sh.j2 new file mode 100644 index 0000000..ce00649 --- /dev/null +++ b/templates/borg-restore-from.sh.j2 @@ -0,0 +1,45 @@ +#!/bin/bash + +if [ -z "$1" ] || [ ! -z "$2" ] + then + printf "Possible: info | list | mount \n\n" +fi + +# Sourcing the backup-passphrase + +. /root/.borg.passphrase + +# Small helper commands, like listing backups, will help us in the future :) + +if [ "$1" = "info" ] + then + if [ -z "$2" ]; then printf "run $0 with list and use the backup-tag to request information\n"; exit 1; fi +{% for host in groups.backupservers %} + REPOSITORY={{ borg_user }}@{{ restore_backup_server }}:{{ borg_pool }}/{{ restore_from_vm }} + /usr/local/bin/borg info $REPOSITORY::$2 +{% endfor %} + exit 0 +fi + +if [ "$1" = "mount" ] + then + if [ -z "$2" ]; then printf "Select the backup-server\n"; exit 1; fi + if [ -z "$3" ]; then printf "Select the backup to mount\n"; exit 1; fi + if [ -z "$4" ]; then printf "Select the path to mount the backup on\n"; exit 1; fi +{% for host in groups.backupservers %} + REPOSITORY={{ borg_user }}@{{ restore_backup_server }}:{{ borg_pool }}/{{ restore_from_vm }} + /usr/local/bin/borg mount $REPOSITORY::$3 $4 + if [ "$?" = "0" ]; then printf "Backup mounted on $4, do not forget to unmount!\n"; fi + exit 0 +{% endfor %} +fi + +if [ "$1" = "list" ] + then +{% for host in groups.backupservers %} + REPOSITORY={{ borg_user }}@{{ restore_backup_server }}:{{ borg_pool }}/{{ restore_from_vm }} + printf "{{ restore_from_vm }} archives to restore on {{ host }} ( {{ restore_backup_server }} ):\n" + /usr/local/bin/borg list -v $REPOSITORY +{% endfor %} + exit 0 +fi diff --git a/templates/ssh.config.j2 b/templates/ssh.config.j2 new file mode 100644 index 0000000..ac4f8a1 --- /dev/null +++ b/templates/ssh.config.j2 @@ -0,0 +1,8 @@ +# backup hosts +{% for b in backupservers %} +Host {{ b.fqdn }} + StrictHostKeyChecking no + {% if b.port is defined %} + Port {{ b.port }} + {% endif %} +{% endfor %}