From 04d221778b6403966e7053a06e9cc99ead7097ed Mon Sep 17 00:00:00 2001 From: Bertrand Benjamin Date: Tue, 8 Mar 2022 14:42:14 +0100 Subject: [PATCH] Feat: basic borgmatic role --- roles/borgmatic/defaults/main.yml | 30 +++ roles/borgmatic/files/borgmatic.timer | 10 + roles/borgmatic/handlers/main.yml | 3 + roles/borgmatic/molecule/default/converge.yml | 20 +- roles/borgmatic/molecule/default/molecule.yml | 1 + .../molecule/default/tests/test_default.py | 6 + roles/borgmatic/tasks/main.yml | 51 ++++- .../templates/borgmatic.config.yaml.j2 | 187 ++++++++++++++++++ .../borgmatic/templates/borgmatic.service.j2 | 54 +++++ 9 files changed, 358 insertions(+), 4 deletions(-) create mode 100644 roles/borgmatic/files/borgmatic.timer create mode 100644 roles/borgmatic/templates/borgmatic.config.yaml.j2 create mode 100644 roles/borgmatic/templates/borgmatic.service.j2 diff --git a/roles/borgmatic/defaults/main.yml b/roles/borgmatic/defaults/main.yml index eff2217..935012d 100644 --- a/roles/borgmatic/defaults/main.yml +++ b/roles/borgmatic/defaults/main.yml @@ -1,2 +1,32 @@ --- # defaults file for borgmatic +borg_encryption_passphrase: '' +borg_exclude_patterns: [] +borgmatic_config_name: config.yaml +borgmatic_large_repo: true +borgmatic_hooks: + on_error: + - echo "`date` - Error while creating a backup." + before_backup: + - echo "`date` - Starting backup." + after_backup: + - echo "`date` - Finished backup." +borgmatic_checks: + - repository +borgmatic_check_last: 3 +borgmatic_store_atime: true +borgmatic_store_ctime: true +borgmatic_relocated_repo_access_is_ok: false +borg_one_file_system: true +borg_exclude_from: [] +borg_encryption_passcommand: false +borg_lock_wait_time: 5 +borg_ssh_command: false +borg_remote_path: false +borg_remote_rate_limit: 0 +borg_retention_policy: + keep_hourly: 3 + keep_daily: 7 + keep_weekly: 4 + keep_monthly: 6 +create_repo: False diff --git a/roles/borgmatic/files/borgmatic.timer b/roles/borgmatic/files/borgmatic.timer new file mode 100644 index 0000000..d6d6dd2 --- /dev/null +++ b/roles/borgmatic/files/borgmatic.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Run borgmatic backup + +[Timer] +OnBootSec=2min +OnUnitActiveSec=1d +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/roles/borgmatic/handlers/main.yml b/roles/borgmatic/handlers/main.yml index 6314fb8..3111f70 100644 --- a/roles/borgmatic/handlers/main.yml +++ b/roles/borgmatic/handlers/main.yml @@ -1,2 +1,5 @@ --- # handlers file for borgmatic +- name: reload systemd + systemd: + daemon_reload: yes diff --git a/roles/borgmatic/molecule/default/converge.yml b/roles/borgmatic/molecule/default/converge.yml index e3e84d0..7e3a920 100644 --- a/roles/borgmatic/molecule/default/converge.yml +++ b/roles/borgmatic/molecule/default/converge.yml @@ -2,10 +2,24 @@ - name: Converge hosts: all become: yes + vars: + - borg_source_directories: + - /media/test1/ + - /media/test2/ + - borg_repository: + - /backup/ + - borgmatic_config_name: config.yaml + pre_tasks: - - name: Update apt cache - apt: - update_cache: yes + #- name: Update apt cache +# apt: +# update_cache: yes + + - name: Create sources directories + file: + path: "{{ item }}" + state: directory + with_items: "{{ borg_source_directories + borg_repository}}" tasks: - name: "Include borgmatic" diff --git a/roles/borgmatic/molecule/default/molecule.yml b/roles/borgmatic/molecule/default/molecule.yml index 2e06323..d5eed35 100644 --- a/roles/borgmatic/molecule/default/molecule.yml +++ b/roles/borgmatic/molecule/default/molecule.yml @@ -9,6 +9,7 @@ platforms: - name: Debian box: "debian/bullseye64" pre_build_image: true + provisioner: name: ansible verifier: diff --git a/roles/borgmatic/molecule/default/tests/test_default.py b/roles/borgmatic/molecule/default/tests/test_default.py index d4a27bd..edf962b 100644 --- a/roles/borgmatic/molecule/default/tests/test_default.py +++ b/roles/borgmatic/molecule/default/tests/test_default.py @@ -6,3 +6,9 @@ def test_installed_packages(host): assert borgbackup.is_installed borgmatic = host.package("borgmatic") assert borgmatic.is_installed + +def test_borgmatic_config(host): + config = host.file("/etc/borgmatic/config.yaml") + assert config.exists + valid_config = host.run("sudo validate-borgmatic-config") + assert valid_config.succeeded diff --git a/roles/borgmatic/tasks/main.yml b/roles/borgmatic/tasks/main.yml index f186934..c7b3e95 100644 --- a/roles/borgmatic/tasks/main.yml +++ b/roles/borgmatic/tasks/main.yml @@ -1,9 +1,58 @@ --- # tasks file for borgmatic +- name: stat on sources + stat: + path: "{{ item }}" + with_items: "{{ borg_source_directories }}" + register: sources_exists + +- name: Assert sources exists + assert: + that: "{{ item }}.stat.exists" + with_items: "{{ sources_exists.results }}" + - name: Install borg and borgmatic apt: name: "{{ item }}" state: present with_items: - - borg + - borgbackup - borgmatic + +- name: Make dir for borgmatic in etc + file: + path: "/etc/borgmatic" + state: directory + +- name: Borgmatic config + template: + src: borgmatic.config.yaml.j2 + dest: "/etc/borgmatic/{{ borgmatic_config_name }}" + mode: 644 + +- name: Check borgmatic config + command: + cmd: validate-borgmatic-config + register: validate_borgmatic_config + +- name: Assert that config is validate + assert: + that: validate_borgmatic_config.failed != true + +- name: copy systemd service + template: + src: borgmatic.service.j2 + dest: /etc/systemd/system/borgmatic.service + mode: 644 + notify: reload systemd + +- name: copy systemd timer + copy: + src: borgmatic.timer + dest: /etc/systemd/system/borgmatic.timer + notify: "reload systemd" + +- name: enable timer + systemd: + name: borgmatic + enabled: yes diff --git a/roles/borgmatic/templates/borgmatic.config.yaml.j2 b/roles/borgmatic/templates/borgmatic.config.yaml.j2 new file mode 100644 index 0000000..5f2b21d --- /dev/null +++ b/roles/borgmatic/templates/borgmatic.config.yaml.j2 @@ -0,0 +1,187 @@ +#jinja2: lstrip_blocks: "True", trim_blocks: "True" +--- +# From borgbase/ansible-role-borgbackup + +{{ ansible_managed | comment }} + +# Full config: https://torsion.org/borgmatic/docs/reference/config.yaml +location: + source_directories: +{% for dir in borg_source_directories %} + - {{ dir }} +{% endfor %} + + # Stay in same file system (do not cross mount points). + one_file_system: {{ borg_one_file_system }} + repositories: +{% if borg_repository is iterable and (borg_repository is not string and borg_repository is not mapping) %} + {% for repo in borg_repository %} + - {{ repo }} + {% endfor %} +{% elif borg_repository is defined and borg_repository is string %} + - {{ borg_repository }} +{% endif %} + + # Store atime into archive. + atime: {{ borgmatic_store_atime }} + + # Store ctime into archive. + ctime: {{ borgmatic_store_ctime }} + +{% if borg_exclude_patterns %} + # Any paths matching these patterns are excluded from backups. Globs and tildes + # are expanded. See the output of "borg help patterns" for more details. + exclude_patterns: +{% for dir in borg_exclude_patterns %} + - '{{ dir }}' +{% endfor %} +{% endif %} +{% if borg_exclude_from %} + # Read exclude patterns from one or more separate named files, one pattern per + # line. See the output of "borg help patterns" for more details. + exclude_from: +{% for dir in borg_exclude_from %} + - {{ dir }} +{% endfor %} +{% endif %} + + # Exclude directories that contain a CACHEDIR.TAG file. See + # http://www.brynosaurus.com/cachedir/spec.html for details. + exclude_caches: true + + # Exclude directories that contain a file with the given filename. + exclude_if_present: .nobackup + + # Alternate Borg remote executable. Defaults to "borg". + # remote_path: borg1 +{% if borg_remote_path %} + remote_path: {{ borg_remote_path }} +{% endif %} + +# Repository storage options. See +# https://borgbackup.readthedocs.io/en/stable/usage.html#borg-create and +# https://borgbackup.readthedocs.io/en/stable/usage/general.html#environment-variables for +# details. +storage: + encryption_passphrase: {{ borg_encryption_passphrase }} + + # The standard output of this command is used to unlock the encryption key. Only + # use on repositories that were initialized with passcommand/repokey encryption. + # Note that if both encryption_passcommand and encryption_passphrase are set, + # then encryption_passphrase takes precedence. + # encryption_passcommand: secret-tool lookup borg-repository repo-name +{% if borg_encryption_passcommand %} + encryption_passcommand: {{ borg_encryption_passcommand }} +{% endif %} + + # Type of compression to use when creating archives. See + # https://borgbackup.readthedocs.org/en/stable/usage.html#borg-create for details. + # Defaults to no compression. + compression: auto,zstd + + # Remote network upload rate limit in kiBytes/second. +{% if borg_remote_rate_limit %} + remote_rate_limit: {{ borg_remote_rate_limit }} +{% endif %} + + # Command to use instead of just "ssh". This can be used to specify ssh options. + # ssh_command: ssh -i ~/.ssh/id_ed25519 +{% if borg_ssh_command %} + ssh_command: {{ borg_ssh_command }} +{% endif %} + + # Umask to be used for borg create. + umask: 0077 + + # Maximum seconds to wait for acquiring a repository/cache lock. + lock_wait: {{ borg_lock_wait_time }} + + # Name of the archive. Borg placeholders can be used. See the output of + # "borg help placeholders" for details. Default is + # "{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}". If you specify this option, you must + # also specify a prefix in the retention section to avoid accidental pruning of + # archives with a different archive name format. And you should also specify a + # prefix in the consistency section as well. + archive_name_format: '{hostname}-{now:%Y-%m-%d-%H%M%S}' + + # Bypass Borg error about a repository that has been moved. + relocated_repo_access_is_ok: {{ borgmatic_relocated_repo_access_is_ok }} + +# Retention policy for how many backups to keep in each category. See +# https://borgbackup.readthedocs.org/en/stable/usage.html#borg-prune for details. +# At least one of the "keep" options is required for pruning to work. +retention: +{% if borg_retention_policy.keep_secondly is defined %} + # Number of secondly archives to keep. + keep_secondly: {{ borg_retention_policy.keep_secondly }} +{% endif %} + +{% if borg_retention_policy.keep_minutely is defined %} + # Number of minutely archives to keep. + keep_minutely: {{ borg_retention_policy.keep_minutely }} +{% endif %} + +{% if borg_retention_policy.keep_hourly is defined %} + # Number of hourly archives to keep. + keep_hourly: {{ borg_retention_policy.keep_hourly }} +{% endif %} + +{% if borg_retention_policy.keep_daily is defined %} + # Number of daily archives to keep. + keep_daily: {{ borg_retention_policy.keep_daily }} +{% endif %} + +{% if borg_retention_policy.keep_weekly is defined %} + # Number of weekly archives to keep. + keep_weekly: {{ borg_retention_policy.keep_weekly }} +{% endif %} + +{% if borg_retention_policy.keep_monthly is defined %} + # Number of monthly archives to keep. + keep_monthly: {{ borg_retention_policy.keep_monthly }} +{% endif %} + +{% if borg_retention_policy.keep_yearly is defined %} + # Number of yearly archives to keep. + keep_yearly: {{ borg_retention_policy.keep_yearly }} +{% endif %} + + # When pruning, only consider archive names starting with this prefix. + # Borg placeholders can be used. See the output of "borg help placeholders" for + # details. Default is "{hostname}-". + prefix: '{hostname}-' + +# Consistency checks to run after backups. See +# https://borgbackup.readthedocs.org/en/stable/usage.html#borg-check and +# https://borgbackup.readthedocs.org/en/stable/usage.html#borg-extract for details. +consistency: + # List of one or more consistency checks to run: "repository", + # "archives", "data", and/or "extract". Defaults to + # "repository" and "archives". Set to "disabled" to disable + # all consistency checks. "repository" checks the consistency + # of the repository, "archives" checks all of the archives, + # "data" verifies the integrity of the data within the + # archives, and "extract" does an extraction dry-run of the + # most recent archive. Note that "data" implies "archives". + checks: + {% for checks in borgmatic_checks %} + - {{ checks }} + {% endfor %} + + # Restrict the number of checked archives to the last n. Applies only to the "archives" check. + check_last: {{ borgmatic_check_last }} + + # When performing the "archives" check, only consider archive names starting with + # this prefix. Borg placeholders can be used. See the output of + # "borg help placeholders" for details. Default is "{hostname}-". + prefix: '{hostname}-' + +# Shell commands or scripts to execute before and after a backup or if an error has occurred. +# IMPORTANT: All provided commands and scripts are executed with user permissions of borgmatic. +# Do not forget to set secure permissions on this file as well as on any script listed (chmod 0700) to +# prevent potential shell injection or privilege escalation. +hooks: +{% for hook in borgmatic_hooks %} + {{ hook }}: + {{ borgmatic_hooks[hook] | to_nice_yaml(indent=2) | trim | indent(8) }} +{% endfor %} diff --git a/roles/borgmatic/templates/borgmatic.service.j2 b/roles/borgmatic/templates/borgmatic.service.j2 new file mode 100644 index 0000000..419002b --- /dev/null +++ b/roles/borgmatic/templates/borgmatic.service.j2 @@ -0,0 +1,54 @@ +[Unit] +Description=borgmatic backup +Wants=network-online.target +After=network-online.target + +[Service] +Type=oneshot + +# Security settings for systemd running as root, optional but recommended to improve security. You +# can disable individual settings if they cause problems for your use case. For more details, see +# the systemd manual: https://www.freedesktop.org/software/systemd/man/systemd.exec.html +LockPersonality=true +# Certain borgmatic features like Healthchecks integration need MemoryDenyWriteExecute to be off. +# But you can try setting it to "yes" for improved security if you don't use those features. +MemoryDenyWriteExecute=no +NoNewPrivileges=yes +PrivateDevices=yes +PrivateTmp=yes +ProtectClock=yes +ProtectControlGroups=yes +ProtectHostname=yes +ProtectKernelLogs=yes +ProtectKernelModules=yes +ProtectKernelTunables=yes +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK +RestrictNamespaces=yes +RestrictRealtime=yes +RestrictSUIDSGID=yes +SystemCallArchitectures=native +SystemCallFilter=@system-service +SystemCallErrorNumber=EPERM +# To restrict write access further, change "ProtectSystem" to "strict" and uncomment +# "ReadWritePaths", "ReadOnlyPaths", "ProtectHome", and "BindPaths". Then add any local repository +# paths to the list of "ReadWritePaths" and local backup source paths to "ReadOnlyPaths". This +# leaves most of the filesystem read-only to borgmatic. +ProtectSystem=full +# ReadWritePaths=-/mnt/my_backup_drive +# ReadOnlyPaths=-/var/lib/my_backup_source +# This will mount a tmpfs on top of /root and pass through needed paths +# ProtectHome=tmpfs +# BindPaths=-/root/.cache/borg -/root/.cache/borg -/root/.borgmatic + +# May interfere with running external programs within borgmatic hooks. +CapabilityBoundingSet=CAP_DAC_READ_SEARCH CAP_NET_RAW + +Restart=no +# Prevent rate limiting of borgmatic log events. If you are using an older version of systemd that +# doesn't support this (pre-240 or so), you may have to remove this option. +LogRateLimitIntervalSec=0 + +# Delay start to prevent backups running during boot. Note that systemd-inhibit requires dbus and +# dbus-user-session to be installed. +ExecStartPre=sleep 1m +ExecStart=systemd-inhibit --who="borgmatic" --why="Prevent interrupting scheduled backup" /usr/bin/borgmatic -c /etc/borgmatic/{{ borgmatic_config_name }} --verbosity -1 --syslog-verbosity 1