add borgmatic role

This commit is contained in:
Sergey Malyuk
2025-12-12 17:22:02 +03:00
parent c715157a7c
commit e16cc72ab2
23 changed files with 432 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
---
profile: production
strict: true
# Enable checking of loop variable prefixes in roles
loop_var_prefix: "^(__|{role}_)"
skip_list:
- var-naming[no-role-prefix]
warn_list:
- role-name[path]
- var-naming[no-role-prefix]
exclude_paths:
- .github/

3
roles/borgmatic/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.vscode
.idea
.lock

View File

@@ -0,0 +1,8 @@
---
rules:
brackets:
forbid: false
min-spaces-inside: 0
max-spaces-inside: 2
min-spaces-inside-empty: -1
max-spaces-inside-empty: 2

40
roles/borgmatic/README.md Normal file
View File

@@ -0,0 +1,40 @@
ansible-borgmatic
=========
This is a wrapper around the `borgmatic` role from the `maxhoesel.borgbackup` collection.
The wrapper solve the most outstading issue with the current implementation of the original role: inability to install latest (or arbitrary) versions of `borgmatic` and `borg`.
In the case of Borg we are fetching a release from Github.
In the case of Borgmatic we are installing it via `pipx`, as recommended by their official documentation found here: https://torsion.org/borgmatic/docs/how-to/set-up-backups/.
Requirements
------------
- `maxhoesel.borgbackup` collection installed (see `requirements.yml`);
Role Variables
--------------
None
Dependencies
------------
None
Example Playbook
----------------
See `molecule/default/converge.yml`
License
-------
BSD
Author Information
------------------
corvus-migratorius@proton.me

View File

@@ -0,0 +1,11 @@
---
name: ansible-borgmatic
channels:
- conda-forge
dependencies:
- python~=3.12.0
- pip>=24.2
- actionlint
- pip:
- -r requirements.txt
- -r requirements.ci.txt

View File

@@ -0,0 +1,9 @@
---
name: ansible-borgmatic
channels:
- conda-forge
dependencies:
- python~=3.12.0
- pip>=24.2
- pip:
- -r requirements.txt

View File

@@ -0,0 +1,17 @@
---
borg_version: "1.4.0"
glibc_version: "2.36"
pipx_version: "1.7.1"
borg_binary_url: "https://github.com/borgbackup/borg/releases/download/{{ borg_version }}/borg-linux-glibc{{ glibc_version | replace('.', '') }}.tgz"
borgmatic_pipx_bin_dir: "/opt/borgmatic/bin"
borgmatic_schedule_oncalendar: "daily"
push_pubkey: true
ssh_key_path: "/root/borgmatic/id_ed25519"
borg_compression: "lz4"
borg_keep_hourly: 0
borg_keep_daily: 3
borg_keep_weekly: 3
borg_keep_monthly: 1
borg_keep_yearly: 0
borg_uptime_kuma:
borg_loki:

View File

@@ -0,0 +1 @@
---

View File

@@ -0,0 +1,17 @@
---
galaxy_info:
role_name: "borgmatic"
namespace: genlab
author: "Alexander Gorelyshev"
company: "Genlab, LLC"
description: ""
license: "MIT"
min_ansible_version: "2.1"
platforms:
- name: "Ubuntu"
versions: [ "focal", "jammy", "noble" ]
galaxy_tags: [ ]
dependencies: []

View File

@@ -0,0 +1,48 @@
---
- name: Converge
hosts: all
vars:
repo_path: "/home/borg/test-repo"
pre_tasks:
- name: "Create a user for borg"
ansible.builtin.user:
name: borg
shell: /bin/bash
create_home: true
- name: "Generate test data file"
ansible.builtin.copy:
dest: "/tmp/data"
content: "This is a test file!"
owner: "{{ ansible_user_id }}"
group: "{{ ansible_user_id }}"
mode: "0644"
- name: "Ensure the repo path exists"
ansible.builtin.file:
path: "{{ repo_path }}"
state: directory
owner: "borg"
mode: "0700"
- name: "Install openssh-server"
ansible.builtin.apt:
name: openssh-server
state: present
update_cache: true
- name: "Start an SSH openssh-server"
ansible.builtin.systemd:
name: ssh
state: started
roles:
- role: genlab.borgmatic
borg_source_directories:
- "/tmp/data"
borg_repo_path: "ssh://borg@localhost/./test-repo"
borg_repo_label: "test-repo"
borg_encryption_passphrase: "secret"
repo_server_inventory_hostname: ubuntu # in production this should be an Ansible inventory hostname
repo_server_user: borg

View File

@@ -0,0 +1,31 @@
---
dependency:
name: galaxy
driver:
name: docker
platforms:
- name: ubuntu
image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2404}-ansible:latest
pre_build_image: true
command: ${MOLECULE_DOCKER_COMMAND:-""}
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
cgroupns_mode: host
privileged: true
provisioner:
name: ansible
verifier:
name: ansible
scenario:
name: default
test_sequence:
- destroy
- create
- converge
# - idempotence
- verify

View File

@@ -0,0 +1,48 @@
---
- name: Verify
hosts: all
gather_facts: false
any_errors_fatal: true
vars:
repo_path: "/home/borg/test-repo"
tasks:
- name: "Include default vars"
ansible.builtin.include_vars:
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/"
extensions: [ 'yml' ]
- name: "Check if Borg is installed"
changed_when: false
register: borg_installed_version
ansible.builtin.command: "/usr/bin/borg --version"
- name: "Check Borg version"
ansible.builtin.assert:
that: borg_installed_version.stdout.find(borg_version)
success_msg: "borg version {{ borg_version }} is installed and working"
fail_msg: "borg version {{ borg_version }} is not installed or not working correctly"
- name: "Check if Borgmatic is installed"
changed_when: false
register: borgmatic_installed_version
ansible.builtin.command:
cmd: "/usr/bin/borgmatic --version"
- name: "Check that the test repo was created"
register: test_repo_readme
failed_when: test_repo_readme.stat.exists is false
ansible.builtin.stat:
path: "{{ repo_path }}"
- name: "Check that the systemd timer for Borgmatic is up and running"
register: timer
ansible.builtin.systemd:
name: borgmatic.timer
- name: "Assert that the timer is running"
ansible.builtin.assert:
that: timer.status.ActiveState == "active"
success_msg: "Timer is running"
fail_msg: "Unexpected timer state: '{{ timer.status.ActiveState }}'"

View File

@@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}

View File

@@ -0,0 +1,6 @@
ansible-lint
molecule==24.12.0
molecule-plugins[docker]
docker~=7.1.0
requests==2.31.0 # pinned to the latest version not breaking Docker SDK
yamllint

View File

@@ -0,0 +1 @@
ansible~=11.3.0

View File

@@ -0,0 +1,6 @@
# requirements file
---
collections:
- name: https://github.com/maxhoesel/ansible-collection-borgbackup
type: git
version: "v2.0.1"

View File

@@ -0,0 +1,34 @@
---
- name: "Compose basic configuration for Borgmatic"
ansible.builtin.set_fact:
borgmatic_composite_config:
source_directories: "{{ borg_source_directories }}"
repositories:
- path: "{{ borg_repo_path }}"
label: "{{ borg_repo_label }}"
encryption_passphrase: "{{ borg_encryption_passphrase }}"
compression: "{{ borg_compression }}"
# CLI output configuration
list_details: true
statistics: true
exclude_caches: true
# logging verbosity:
verbosity: 1
syslog_verbosity: 1
monitoring_verbosity: 1
# backup depth
keep_hourly: "{{ borg_keep_hourly }}"
keep_daily: "{{ borg_keep_daily }}"
keep_weekly: "{{ borg_keep_weekly }}"
keep_monthly: "{{ borg_keep_monthly }}"
keep_yearly: "{{ borg_keep_yearly }}"
- name: "Add Uptime Kuma configuration"
when: borg_uptime_kuma
ansible.builtin.set_fact:
borgmatic_composite_config: "{{ borgmatic_composite_config | combine({'uptime_kuma': borg_uptime_kuma}) }}"
- name: "Add Loki configuration"
when: borg_loki
ansible.builtin.set_fact:
borgmatic_composite_config: "{{ borgmatic_composite_config | combine({'loki': borg_loki}) }}"

View File

@@ -0,0 +1,25 @@
---
- name: "Ensure the path for SSH keys exists"
ansible.builtin.file:
path: "{{ ssh_key_path | dirname }}"
state: directory
owner: root
group: root
mode: "0700"
- name: "Generate an ed25519 SSH key pair with 100 KDF rounds"
register: ssh_key_pair
community.crypto.openssh_keypair:
type: ed25519
path: "{{ ssh_key_path }}"
comment: "Generated by Ansible for Borgmatic"
force: false
mode: '0600'
- name: "Push the SSH key pair to the Borg repo host"
when: push_pubkey
delegate_to: "{{ repo_server_inventory_hostname }}"
ansible.posix.authorized_key:
user: "{{ repo_server_user }}"
key: "{{ ssh_key_pair.public_key }}"
state: present

View File

@@ -0,0 +1,66 @@
---
- name: "Ensure that system dependencies are installed"
ansible.builtin.apt:
name:
- openssh-client
- python3-pip
- python3-venv
state: present
update_cache: true
cache_valid_time: 3600
- name: "Install pipx"
retries: 3
delay: 1
ansible.builtin.pip:
name: "pipx=={{ pipx_version }}"
executable: pip3
break_system_packages: true
- name: "Ensure pipx binary is available in PATH"
changed_when: false
ansible.builtin.command:
cmd: pipx ensurepath
- name: "Install borgmatic via pipx"
retries: 3
delay: 1
environment:
PIPX_BIN_DIR: "{{ borgmatic_pipx_bin_dir }}"
community.general.pipx:
name: borgmatic
state: present
install_deps: true
- name: "Install Borg if the correct version is not available"
block:
# we are looking for Borg installed in a directory that Max Hoesel's role exects to find it
- name: "Get the currently installed version of Borg"
changed_when: false
register: borg_version_installed
ansible.builtin.command:
cmd: /usr/bin/borg --version
- name: "Check that the correct version of Borg is installed"
ansible.builtin.assert:
that: borg_version_installed.stdout.find(borg_version)
fail_msg: "The expected Borg version was not found: {{ borg_version_installed }}"
success_msg: "Found the expected Borg version ({{ borg_version }})"
rescue:
- name: "Download Borg from a custom URL: '{{ borg_binary_url }}'"
retries: 3
delay: 1
ansible.builtin.unarchive:
src: "{{ borg_binary_url }}"
dest: "/opt/"
remote_src: true
owner: root
group: root
mode: "0755"
- name: "Create a symbolic link for Borg"
ansible.builtin.file:
state: link
src: "/opt/borg-dir/borg.exe"
dest: "/usr/bin/borg"

View File

@@ -0,0 +1 @@
---

View File

@@ -0,0 +1,15 @@
---
- name: "Include tool installation tasks"
ansible.builtin.include_tasks: "install.yml"
- name: "Include SSH key handling tasks"
ansible.builtin.include_tasks: "handle-ssh-keys.yml"
- name: "Include configuration tasks"
ansible.builtin.include_tasks: "config.yml"
- name: "Include tasks for third-party integrations"
ansible.builtin.include_tasks: "integrations.yml"
- name: "Include tasks for running borgmatic"
ansible.builtin.include_tasks: "run.yml"

View File

@@ -0,0 +1,22 @@
---
# A workaround for maxhoesel.borgbackup.borgmatic that does not support custom paths
- name: "Create symbolic links for Borgmatic executables"
loop:
- borgmatic
- generate-borgmatic-config
- validate-borgmatic-config
ansible.builtin.file:
state: link
src: "{{ borgmatic_pipx_bin_dir }}/{{ item }}"
dest: /usr/bin/{{ item }}
- name: "Configure and run Borgmatic"
ansible.builtin.include_role:
name: maxhoesel.borgbackup.borgmatic
vars:
borgmatic_install: false # we handle installation separately to get the recent version
# borgmatic_ssh_key_gen_options: "-t ed25519 -a 100"
borgmatic_ssh_key_path: "{{ ssh_key_path }}"
borgmatic_schedule_on: "{{ borgmatic_schedule_oncalendar }}"
borgmatic_schedule_max_random_delay: 600
borgmatic_config: "{{ borgmatic_composite_config }}"

View File

@@ -0,0 +1 @@
---