diff --git a/roles/restic/defaults/main.yml b/roles/restic/defaults/main/config.yml similarity index 74% rename from roles/restic/defaults/main.yml rename to roles/restic/defaults/main/config.yml index c36c501..304aeac 100644 --- a/roles/restic/defaults/main.yml +++ b/roles/restic/defaults/main/config.yml @@ -1,5 +1,4 @@ --- - restic_repo_url: ~ restic_repo_password: ~ restic_s3_key_id: ~ @@ -8,6 +7,8 @@ restic_s3_access_key: ~ restic_backup_paths: [] restic_backup_stdin_command: ~ restic_backup_stdin_command_filename: ~ +restic_backup_generate_metrics_command: >-2 + {{ restic_script_generate_snapshot_metrics }} restic_policy_keep_all_within: 1d restic_policy_keep_hourly: 12 @@ -18,6 +19,8 @@ restic_policy_keep_yearly: 5 restic_policy_backup_frequency: hourly restic_base_environment: + RESTIC_REPOSITORY: "{{ restic_repo_url }}" + RESTIC_PASSWORD: "{{ restic_password }}" RESTIC_JOBNAME: "{{ restic_job_name | default('unknown') }}" RESTIC_FORGET_KEEP_WITHIN: "{{ restic_policy_keep_all_within }}" RESTIC_FORGET_KEEP_HOURLY: "{{ restic_policy_keep_hourly }}" @@ -25,6 +28,7 @@ restic_base_environment: RESTIC_FORGET_KEEP_WEEKLY: "{{ restic_policy_keep_weekly }}" RESTIC_FORGET_KEEP_MONTHLY: "{{ restic_policy_keep_monthly }}" RESTIC_FORGET_KEEP_YEARLY: "{{ restic_policy_keep_yearly }}" + RESTIC_GENERATE_SNAPSHOT_METRICS_COMMAND: "{{ restic_backup_generate_metrics_command }}" restic_s3_environment: AWS_ACCESS_KEY_ID: "{{ restic_s3_key_id }}" @@ -33,8 +37,8 @@ restic_s3_environment: restic_complete_environment: >- {{ restic_base_environment - | combine((restic_s3_environment - if (restic_s3_key_id and restic_s3_access_key) else {}) | default({})) + | combine((restic_s3_environment | default({})) + if (restic_s3_key_id and restic_s3_access_key) else {}) | combine(restic_environment | default({})) }} @@ -46,15 +50,3 @@ restic_policy: monthly: "{{ restic_policy_keep_monthly }}" yearly: "{{ restic_policy_keep_yearly }}" frequency: "{{ restic_policy_backup_frequency }}" - -restic_user: root -restic_create_user: false -restic_start_job_on_unit_change: false - -restic_job_name: ~ -restic_job_description: "Restic backup job for {{ restic_job_name }}" -restic_systemd_unit_naming_scheme: "restic.{{ restic_job_name }}" -restic_systemd_working_directory: /tmp -restic_systemd_syslog_identifier: "restic-{{ restic_job_name }}" - -restic_package_name: restic diff --git a/roles/restic/defaults/main/main.yml b/roles/restic/defaults/main/main.yml new file mode 100644 index 0000000..8635af5 --- /dev/null +++ b/roles/restic/defaults/main/main.yml @@ -0,0 +1,14 @@ +--- +restic_user: root +restic_user_create: false +restic_create_user: "{{ restic_user_create }}" +restic_user_create_home: false +restic_user_system: false + +restic_state: present +restic_version: "0.17.3" +restic_job_name: default +restic_job_directory: "/etc/restic" + +restic_package_name: restic +restic_script_generate_snapshot_metrics: "/opt/restic-generate-snapshot-metrics.sh" diff --git a/roles/restic/defaults/main/systemd.yml b/roles/restic/defaults/main/systemd.yml new file mode 100644 index 0000000..ce62d1e --- /dev/null +++ b/roles/restic/defaults/main/systemd.yml @@ -0,0 +1,11 @@ +--- +restic_systemd_job_name: ~ +restic_systemd_job_description: "Restic backup job for {{ restic_job_name }}" + +restic_systemd_working_directory: /tmp +restic_systemd_install_wanted_by: "basic.target" +restic_systemd_install_default_instance: "default" + +restic_systemd_start_job_on_unit_change: false +restic_systemd_service_exec_start: "/opt/restic-backup.sh" + diff --git a/roles/restic/files/restic-backup-directories.sh b/roles/restic/files/restic-backup.sh similarity index 69% rename from roles/restic/files/restic-backup-directories.sh rename to roles/restic/files/restic-backup.sh index a39ff3b..51d40f4 100644 --- a/roles/restic/files/restic-backup-directories.sh +++ b/roles/restic/files/restic-backup.sh @@ -8,15 +8,19 @@ fi echo "List existing snapshots or initialize repository" restic snapshots || restic init -sleep 2; +sleep 1; echo "Attempting to remove lock if present" restic unlock -sleep 2 +sleep 1; echo "Start backup on ${@:1}" restic --verbose --retry-lock=${RESTIC_RETRY_LOCK:-5m} backup "${@:1}" -sleep 2 +sleep 1; + +if [[ -n ${RESTIC_POIST_BACKUP_HOOK-} ]]; then + /bin/bash -c "$RESTIC_POST_BACKUP_HOOK" +fi echo "Forget and prune old snapshots" restic forget --prune --retry-lock=${RESTIC_RETRY_LOCK:-5m} \ @@ -29,9 +33,11 @@ restic forget --prune --retry-lock=${RESTIC_RETRY_LOCK:-5m} \ sleep 2 echo "Generate snapshot metrics" -restic --json snapshots | /opt/restic-generate-snapshot-metrics.sh \ - > /var/lib/node_exporter/restic-snapshots-${RESTIC_JOBNAME:-unknown}.prom-src -sleep 2 +if [[ -n ${RESTIC_GENERATE_SNAPSHOT_METRICS_COMMAND-} ]]; then + restic --json snapshots | ${RESTIC_GENERATE_SNAPSHOT_METRICS_COMMAND} \ + > /var/lib/node_exporter/restic-snapshots-${RESTIC_JOBNAME:-unknown}.prom-src + sleep 2; +fi echo "Check repository" restic check diff --git a/roles/restic/files/restic-generate-snapshot-metrics.sh b/roles/restic/files/restic-snapshot-metrics.sh similarity index 100% rename from roles/restic/files/restic-generate-snapshot-metrics.sh rename to roles/restic/files/restic-snapshot-metrics.sh diff --git a/roles/restic/tasks/check.yml b/roles/restic/tasks/check.yml new file mode 100644 index 0000000..94e28da --- /dev/null +++ b/roles/restic/tasks/check.yml @@ -0,0 +1,35 @@ +--- +- name: Check if 'restic_state' is valid + ansible.builtin.fail: + msg: >-2 + Unknown value '{{ restic_state }}' for 'restic_state'! + Supported values are {{ restic_states | join(', ') }} + when: restic_state not in restic_states + +- name: Ensure 'restic_job_name' is properly populated + ansible.builtin.fail: + msg: >-2 + Unsupported restic_job_name '{{ restic_job_name | string }}'! + when: + - not (restic_job_name | string | length > 0) + +- name: Ensure either backup_paths or backup_stdin_command is populated + ansible.builtin.fail: + msg: >-2 + Setting both `restic_backup_paths` and `restic_backup_stdin_command` + is not supported! + when: restic_backup_paths|length > 0 and restic_backup_stdin_command and false + +- name: Ensure a filename for stdin_command backup is given + ansible.builtin.fail: + msg: >-2 + `restic_backup_stdin_command` was set but no filename for the resulting + output was supplied in `restic_backup_stdin_command_filename`. + when: restic_backup_stdin_command and not restic_backup_stdin_command_filename + +- name: Ensure backup frequency adheres to systemd's OnCalender syntax + command: + cmd: "systemd-analyze calendar {{ restic_policy.frequency }}" + register: systemd_calender_parse_res + failed_when: systemd_calender_parse_res.rc != 0 + changed_when: false diff --git a/roles/restic/tasks/install.yml b/roles/restic/tasks/install.yml new file mode 100644 index 0000000..74e5964 --- /dev/null +++ b/roles/restic/tasks/install.yml @@ -0,0 +1,20 @@ +--- +- name: Ensure restic is installed + block: + - name: Ensure restic is installed via apt + apt: + package: restic + state: latest + when: ansible_os_family == 'Debian' + - name: Ensure restic is installed via dnf + dnf: + name: restic + state: latest + when: ansible_os_family == 'RedHat' + - name: Ensure restic is installed using the auto-detected package-manager + package: + name: "{{ restic_package_name }}" + state: present + when: ansible_os_family not in ['RedHat', 'Debian'] + + diff --git a/roles/restic/tasks/main.yml b/roles/restic/tasks/main.yml index a4d5eba..2453c86 100644 --- a/roles/restic/tasks/main.yml +++ b/roles/restic/tasks/main.yml @@ -1,46 +1,35 @@ --- +- name: Check if role input is valid + ansible.builtin.include_tasks: + file: check.yml -- name: Ensure {{ restic_user }} system user exists - user: +- name: Ensure restic is {{ restic_state }} + ansible.builtin.include_tasks: + file: install.yml + +- name: Ensure restic user '{{ restic_user }}' is {{ restic_state }} + ansible.builtin.user: name: "{{ restic_user }}" - state: present - system: true + state: "{{ restic_state }}" + system: "{{ restic_user_system }}" + create_home: "{{ restic_user_create_home }}" when: restic_create_user -- name: Ensure either backup_paths or backup_stdin_command is populated - when: restic_backup_paths|length > 0 and restic_backup_stdin_command and false - fail: - msg: "Setting both `restic_backup_paths` and `restic_backup_stdin_command` is not supported" - -- name: Ensure a filename for stdin_command backup is given - when: restic_backup_stdin_command and not restic_backup_stdin_command_filename - fail: - msg: "`restic_backup_stdin_command` was set but no filename for the resulting output was supplied in `restic_backup_stdin_command_filename`" - -- name: Ensure backup frequency adheres to systemd's OnCalender syntax - command: - cmd: "systemd-analyze calendar {{ restic_policy.frequency }}" - register: systemd_calender_parse_res - failed_when: systemd_calender_parse_res.rc != 0 - changed_when: false - -- name: Ensure restic is installed - block: - - name: Ensure restic is installed via apt - apt: - package: restic - state: latest - when: ansible_os_family == 'Debian' - - name: Ensure restic is installed via dnf - dnf: - name: restic - state: latest - when: ansible_os_family == 'RedHat' - - name: Ensure restic is installed using the auto-detected package-manager - package: - name: "{{ restic_package_name }}" - state: present - when: ansible_os_family not in ['RedHat', 'Debian'] +- name: Ensure restic backup scripts are {{ restic_state }} + ansible.builtin.copy: + src: "{{ script.source }}" + dest: "{{ script.destination }}" + mode: "{{ script.mode }}" + loop: + - source: restic-backup.sh + destination: "{{ restic_systemd_service_exec_start }}" + mode: "0510" + - source: restic-snapshot-metrics.sh + destination: "{{ restic_script_generate_snapshot_metrics }}" + mode: "0510" + loop_control: + loop_var: script + label: "{{ script.source }}" - name: Ensure systemd service file for '{{ restic_job_name }}' is templated template: diff --git a/roles/restic/templates/restic.conf.j2 b/roles/restic/templates/restic.conf.j2 new file mode 100644 index 0000000..8ec246e --- /dev/null +++ b/roles/restic/templates/restic.conf.j2 @@ -0,0 +1,3 @@ +{% for kv in restic_complete_environment | dict2items %} +{{ kv.key }}={{ kv.value }} +{% endfor %} diff --git a/roles/restic/templates/restic.service.j2 b/roles/restic/templates/restic.service.j2 index 7169689..f4a3e23 100644 --- a/roles/restic/templates/restic.service.j2 +++ b/roles/restic/templates/restic.service.j2 @@ -1,51 +1,15 @@ [Unit] -Description={{ restic_job_description }} +Description={{ restic_systemd_job_description }} [Service] Type=simple +EnvironmentFile={{ restic_job_directory }}/%i.conf User={{ restic_user }} WorkingDirectory={{ restic_systemd_working_directory }} SyslogIdentifier={{ restic_systemd_syslog_identifier }} -Environment=RESTIC_REPOSITORY={{ restic_repo_url }} -Environment=RESTIC_PASSWORD={{ restic_repo_password }} -{% for kv in restic_complete_environment | dict2items %} -Environment={{ kv.key }}={{ kv.value }} -{% endfor %} +ExecStart={{ restic_systemd_service_exec_start }} -{% if restic_init | default(true) %} -ExecStartPre=-/bin/sh -c '/usr/bin/restic snapshots || /usr/bin/restic init' -{% endif %} -{% if restic_unlock_before_backup | default(false) %} -ExecStartPre=-/bin/sh -c 'sleep 3 && /usr/bin/restic unlock' -{% endif %} -{% if restic_backup_pre_hook | default(false) %} -ExecStartPre=-{{ restic_backup_pre_hook }} -{% endif %} -{% if restic_backup_stdin_command %} -ExecStart=/bin/sh -c '{{ restic_backup_stdin_command }} | /usr/bin/restic backup \ - --retry-lock {{ restic_retry_lock | default('5m') }} \ - --verbose --stdin \ - --stdin-filename {{ restic_backup_stdin_command_filename }}' -{% else %} -ExecStart=/opt/restic-backup-directories.sh {{ restic_backup_paths | join(' ') }} -{% endif %} -{% if restic_forget_prune | default(true) %} -ExecStartPost=/usr/bin/restic forget --prune \ - --retry-lock {{ restic_retry_lock | default('5m') }} \ - --keep-within={{ restic_policy.keep_within }} \ - --keep-hourly={{ restic_policy.hourly }} \ - --keep-daily={{ restic_policy.daily }} \ - --keep-weekly={{ restic_policy.weekly }} \ - --keep-monthly={{ restic_policy.monthly }} \ - --keep-yearly={{ restic_policy.yearly }} -{% endif %} -{% if restic_list_snapshots | default(true) %} -ExecStartPost=-/usr/bin/restic snapshots --retry-lock {{ restic_retry_lock | default('5m') }} -{% endif %} -{% if restic_backup_post_hook | default(false) %} -ExecStartPost=-{{ restic_backup_post_hook }} -{% endif %} -{% if restic_check | default(true) %} -ExecStartPost=/usr/bin/restic check --retry-lock {{ restic_retry_lock | default('5m') }} -{% endif %} +[Install] +WantedBy={{ restic_systemd_install_wanted_by }} +DefaultInstance={{ restic_systemd_install_default_instance }} diff --git a/roles/restic/vars/main.yml b/roles/restic/vars/main.yml new file mode 100644 index 0000000..a7f0545 --- /dev/null +++ b/roles/restic/vars/main.yml @@ -0,0 +1,4 @@ +--- +restic_states: + - "present" + - "absent"