diff --git a/.github/workflows/molecule.yml b/.github/workflows/molecule.yml index 5fcb13d..2fde508 100644 --- a/.github/workflows/molecule.yml +++ b/.github/workflows/molecule.yml @@ -78,14 +78,6 @@ jobs: run: ansible-galaxy install -r requirements.yml shell: micromamba-shell {0} - - name: "Install community.general collection" - run: ansible-galaxy collection install community.general - shell: micromamba-shell {0} - - - name: "Install community.grafana collection" - run: ansible-galaxy collection install community.grafana - shell: micromamba-shell {0} - - name: "Run Molecule tests" if: ${{ matrix.role != '__no_role__' }} working-directory: ${{ matrix.role }} diff --git a/README.md b/README.md index 5bd9d0d..e9151cd 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ ## Roles - [alertmanager](roles/alertmanager/README.md) +- [borgmatic](roles/borgmatic/README.md) - [dnsmasq](roles/dnsmasq/README.md) - [grafana](roles/grafana/README.md) - [ipmi_exporter](roles/ipmi_exporter/README.md) diff --git a/galaxy.yml b/galaxy.yml index c68bddb..409ec6a 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,7 +1,7 @@ --- namespace: genlab name: common -version: 0.9.0 +version: 0.10.0 readme: README.md authors: - Alexander Gorelyshev (corvus-migratorius@proton.me) diff --git a/requirements.yml b/requirements.yml index a0cd255..093988a 100644 --- a/requirements.yml +++ b/requirements.yml @@ -1,3 +1,8 @@ --- collections: - name: ansible.posix + - name: community.general + - name: community.grafana + - name: community.docker + - name: community.crypto + - name: maxhoesel.borgbackup diff --git a/roles/borgmatic/README.md b/roles/borgmatic/README.md new file mode 100644 index 0000000..ce5f940 --- /dev/null +++ b/roles/borgmatic/README.md @@ -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 diff --git a/roles/borgmatic/defaults/main.yml b/roles/borgmatic/defaults/main.yml new file mode 100644 index 0000000..c96f1e8 --- /dev/null +++ b/roles/borgmatic/defaults/main.yml @@ -0,0 +1,18 @@ +--- +borgmatic_version: "1.4.0" +borgmatic_glibc_version: "2.36" +borgmatic_pipx_version: "1.7.1" +borgmatic_binary_url: "\ + https://github.com/borgbackup/borg/releases/download/{{ borgmatic_version }}/borg-linux-glibc{{ borgmatic_glibc_version | replace('.', '') }}.tgz" +borgmatic_pipx_bin_dir: "/opt/borgmatic/bin" +borgmatic_schedule_oncalendar: "daily" +borgmatic_push_pubkey: true +borgmatic_sshkey_path: "/root/borgmatic/id_ed25519" +borgmatic_compression: "lz4" +borgmatic_keep_hourly: 0 +borgmatic_keep_daily: 3 +borgmatic_keep_weekly: 3 +borgmatic_keep_monthly: 1 +borgmatic_keep_yearly: 0 +borgmatic_uptime_kuma: +borgmatic_loki: diff --git a/roles/borgmatic/handlers/main.yml b/roles/borgmatic/handlers/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/borgmatic/handlers/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/borgmatic/meta/main.yml b/roles/borgmatic/meta/main.yml new file mode 100644 index 0000000..868031b --- /dev/null +++ b/roles/borgmatic/meta/main.yml @@ -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: [] diff --git a/roles/borgmatic/molecule/default/converge.yml b/roles/borgmatic/molecule/default/converge.yml new file mode 100644 index 0000000..cb3fdb1 --- /dev/null +++ b/roles/borgmatic/molecule/default/converge.yml @@ -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.common.borgmatic + borgmatic_source_directories: + - "/tmp/data" + borgmatic_repo_path: "ssh://borg@localhost/./test-repo" + borgmatic_repo_label: "test-repo" + borgmatic_encryption_passphrase: "secret" + repo_server_inventory_hostname: ubuntu # in production this should be an Ansible inventory hostname + repo_server_user: borg diff --git a/roles/borgmatic/molecule/default/molecule.yml b/roles/borgmatic/molecule/default/molecule.yml new file mode 100644 index 0000000..2910ca3 --- /dev/null +++ b/roles/borgmatic/molecule/default/molecule.yml @@ -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 diff --git a/roles/borgmatic/molecule/default/verify.yml b/roles/borgmatic/molecule/default/verify.yml new file mode 100644 index 0000000..6edcb47 --- /dev/null +++ b/roles/borgmatic/molecule/default/verify.yml @@ -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: borgmatic_installed_version + ansible.builtin.command: "/usr/bin/borg --version" + + - name: "Check Borg version" + ansible.builtin.assert: + that: borgmatic_installed_version.stdout.find(borgmatic_version) + success_msg: "borg version {{ borgmatic_version }} is installed and working" + fail_msg: "borg version {{ borgmatic_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: borgmatic_test_repo_readme + failed_when: borgmatic_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: borgmatic_timer + ansible.builtin.systemd: + name: borgmatic.timer + + - name: "Assert that the timer is running" + ansible.builtin.assert: + that: borgmatic_timer.status.ActiveState == "active" + success_msg: "Timer is running" + fail_msg: "Unexpected timer state: '{{ borgmatic_timer.status.ActiveState }}'" diff --git a/roles/borgmatic/tasks/config.yml b/roles/borgmatic/tasks/config.yml new file mode 100644 index 0000000..78dc67d --- /dev/null +++ b/roles/borgmatic/tasks/config.yml @@ -0,0 +1,34 @@ +--- +- name: "Compose basic configuration for Borgmatic" + ansible.builtin.set_fact: + borgmatic_composite_config: + source_directories: "{{ borgmatic_source_directories }}" + repositories: + - path: "{{ borgmatic_repo_path }}" + label: "{{ borgmatic_repo_label }}" + encryption_passphrase: "{{ borgmatic_encryption_passphrase }}" + compression: "{{ borgmatic_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: "{{ borgmatic_keep_hourly }}" + keep_daily: "{{ borgmatic_keep_daily }}" + keep_weekly: "{{ borgmatic_keep_weekly }}" + keep_monthly: "{{ borgmatic_keep_monthly }}" + keep_yearly: "{{ borgmatic_keep_yearly }}" + +- name: "Add Uptime Kuma configuration" + when: borgmatic_uptime_kuma + ansible.builtin.set_fact: + borgmatic_composite_config: "{{ borgmatic_composite_config | combine({'uptime_kuma': borgmatic_uptime_kuma}) }}" + +- name: "Add Loki configuration" + when: borgmatic_loki + ansible.builtin.set_fact: + borgmatic_composite_config: "{{ borgmatic_composite_config | combine({'loki': borgmatic_loki}) }}" diff --git a/roles/borgmatic/tasks/handle-ssh-keys.yml b/roles/borgmatic/tasks/handle-ssh-keys.yml new file mode 100644 index 0000000..07e9c07 --- /dev/null +++ b/roles/borgmatic/tasks/handle-ssh-keys.yml @@ -0,0 +1,25 @@ +--- +- name: "Ensure the path for SSH keys exists" + ansible.builtin.file: + path: "{{ borgmatic_sshkey_path | dirname }}" + state: directory + owner: root + group: root + mode: "0700" + +- name: "Generate an ed25519 SSH key pair with 100 KDF rounds" + register: borgmatic_ssh_key_pair + community.crypto.openssh_keypair: + type: ed25519 + path: "{{ borgmatic_sshkey_path }}" + comment: "Generated by Ansible for Borgmatic" + force: false + mode: '0600' + +- name: "Push the SSH key pair to the Borg repo host" + when: borgmatic_push_pubkey + delegate_to: "{{ repo_server_inventory_hostname }}" + ansible.posix.authorized_key: + user: "{{ repo_server_user }}" + key: "{{ borgmatic_ssh_key_pair.public_key }}" + state: present diff --git a/roles/borgmatic/tasks/install.yml b/roles/borgmatic/tasks/install.yml new file mode 100644 index 0000000..46f5203 --- /dev/null +++ b/roles/borgmatic/tasks/install.yml @@ -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=={{ borgmatic_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: borgmatic_version_installed + ansible.builtin.command: + cmd: /usr/bin/borg --version + + - name: "Check that the correct version of Borg is installed" + ansible.builtin.assert: + that: borgmatic_version_installed.stdout.find(borgmatic_version) + fail_msg: "The expected Borg version was not found: {{ borgmatic_version_installed }}" + success_msg: "Found the expected Borg version ({{ borgmatic_version }})" + + rescue: + - name: "Download Borg from a custom URL: '{{ borgmatic_binary_url }}'" + retries: 3 + delay: 1 + ansible.builtin.unarchive: + src: "{{ borgmatic_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" diff --git a/roles/borgmatic/tasks/integrations.yml b/roles/borgmatic/tasks/integrations.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/borgmatic/tasks/integrations.yml @@ -0,0 +1 @@ +--- diff --git a/roles/borgmatic/tasks/main.yml b/roles/borgmatic/tasks/main.yml new file mode 100644 index 0000000..0207ec6 --- /dev/null +++ b/roles/borgmatic/tasks/main.yml @@ -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" diff --git a/roles/borgmatic/tasks/run.yml b/roles/borgmatic/tasks/run.yml new file mode 100644 index 0000000..28051f5 --- /dev/null +++ b/roles/borgmatic/tasks/run.yml @@ -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: "{{ borgmatic_sshkey_path }}" + borgmatic_schedule_on: "{{ borgmatic_schedule_oncalendar }}" + borgmatic_schedule_max_random_delay: 600 + borgmatic_config: "{{ borgmatic_composite_config }}" diff --git a/roles/borgmatic/vars/main.yml b/roles/borgmatic/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/borgmatic/vars/main.yml @@ -0,0 +1 @@ +---