diff --git a/README.md b/README.md index 8738c43..4e697d1 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ ansible-galaxy install arillso.restic | `restic_dir_owner` | `'{{ansible_user}}'` | The owner of all created dirs | | `restic_dir_group` | `'{{ansible_user}}'` | The group of all created dirs | | `restic_no_log` | `true` | set to false to see hidden ansible logs | +| `restic_do_not_cleanup_cron ` | `false` | we changed the cron location and clean up the old one. You can skip the cleanup here | ### Repos Restic stores data in repositories. You have to specify at least one repository diff --git a/defaults/main.yml b/defaults/main.yml index aad7302..a9b721f 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -22,6 +22,7 @@ restic_systemd_timer_default_OnCalendar: '*-*-* 02:00:00' # perform simple version check for this role? (true is recomended) submodules_versioncheck: false +restic_do_not_cleanup_cron: false # outdated variables because of irritating names, but kept for compatibility restic_create_cron: false diff --git a/handlers/main.yml b/handlers/main.yml index f1799ec..43648a3 100644 --- a/handlers/main.yml +++ b/handlers/main.yml @@ -8,6 +8,7 @@ masked: false with_items: '{{ restic_backups }}' ignore_errors: true + tags: skip_ansible_lint when: - restic_create_schedule - item.name is defined diff --git a/tasks/backup.yml b/tasks/backup.yml index d04a9a3..086d27d 100644 --- a/tasks/backup.yml +++ b/tasks/backup.yml @@ -1,11 +1,11 @@ --- -- name: reformat dict if necessary +- name: (BACKUP) reformat dict if necessary set_fact: restic_backups: "{{ restic_backups|dict2items|json_query('[*].value') }}" when: - restic_backups | type_debug == "dict" -- name: Create backup credentials +- name: (BACKUP) Create backup credentials template: src: restic_access_Linux.j2 dest: "{{ restic_script_dir }}/access-{{ item.name | replace(' ', '') }}.sh" @@ -20,7 +20,7 @@ - item.src is defined or item.stdin and item.stdin_cmd is defined - item.repo in restic_repos -- name: Create backup script +- name: (BACKUP) Create backup script template: src: restic_script_Linux.j2 dest: "{{ restic_script_dir }}/backup-{{ item.name | replace(' ', '') }}.sh" diff --git a/tasks/configure.yml b/tasks/configure.yml index 88217b5..bce46b8 100644 --- a/tasks/configure.yml +++ b/tasks/configure.yml @@ -1,5 +1,5 @@ --- -- name: Initialize repository +- name: (CONF)Initialize repository command: '{{ restic_install_path }}/restic init' environment: RESTIC_REPOSITORY: '{{ item.value.location }}' diff --git a/tasks/delete_legacy_cron_entry.yml b/tasks/delete_legacy_cron_entry.yml new file mode 100644 index 0000000..4920331 --- /dev/null +++ b/tasks/delete_legacy_cron_entry.yml @@ -0,0 +1,49 @@ +--- +- name: "(SCHEDULE) (OLD) check if ansible version is under 2.12.0" + ansible.builtin.assert: + that: + - "ansible_version.full is version_compare('2.12.0', '<')" + fail_msg: "[ERROR] Youre ansible version is above 2.12.0" + success_msg: "Congratulations. You are using ansible version {{ ansible_version.full }}" + delegate_to: localhost + +- name: (SCHEDULE) (OLD) try to remove entries from /etc/crontab + ansible.builtin.cron: + name: "do1jlr.restic backup {{ item.name }}" + job: "CRON=true {{ restic_script_dir }}/backup-{{ item.name | replace(' ', '') }}.sh" + minute: '{{ item.schedule_minute | default("*") }}' + hour: '{{ item.schedule_hour | default("2") }}' + weekday: '{{ item.schedule_weekday | default("*") }}' + month: '{{ item.schedule_month | default("*") }}' + state: absent + cron_file: '/etc/crontab' + user: 'root' + become: true + no_log: "{{ restic_no_log }}" + with_items: '{{ restic_backups }}' + when: + - restic_create_schedule | bool + - item.name is defined + - item.scheduled | default(false) + - ansible_service_mgr != 'systemd' or restic_force_cron | default(false) or restic_schedule_type == "cronjob" + ignore_error: true + tags: skip_ansible_lint + register: cron_delete + +- name: "(SCHEDULE) (OLD) make sure 'do1jlr.restic backup {{ item.name }}' is not in /etc/crontab" + become: true + ansible.builtin.lineinfile: + path: '/etc/crontab' + state: absent + search_string: "do1jlr.restic backup {{ item.name }}" + when: cron_delete.failed + with_items: '{{ restic_backups }}' + +- name: "(SCHEDULE) (OLD) make sure '{{ restic_script_dir }}/backup-{{ item.name | replace(' ', '') }}.sh' is not in /etc/crontab" + become: true + ansible.builtin.lineinfile: + path: '/etc/crontab' + state: absent + search_string: "{{ restic_script_dir }}/backup-{{ item.name | replace(' ', '') }}.sh" + when: cron_delete.failed + with_items: '{{ restic_backups }}' diff --git a/tasks/distribution/Windows.yml b/tasks/distribution/Windows.yml index 719e017..a412054 100644 --- a/tasks/distribution/Windows.yml +++ b/tasks/distribution/Windows.yml @@ -2,5 +2,5 @@ # tasks file for skeleton - name: Message - debug: + ansible.builtin.fail: msg: 'Your {{ ansible_system }} is not yet supported' diff --git a/tasks/install.yml b/tasks/install.yml index 3aab83f..e2cbb80 100644 --- a/tasks/install.yml +++ b/tasks/install.yml @@ -1,8 +1,8 @@ --- -- name: install and verify restic binary +- name: (INSTALL) install and verify restic binary become: true block: - - name: Download client binary + - name: (INSTALL) Download client binary ansible.builtin.get_url: url: '{{ restic_url }}' dest: '{{ restic_download_path }}/restic.bz2' @@ -10,43 +10,43 @@ register: get_url_restic # TODO: This needs to become independent of the shell module to actually work - - name: Decompress the binary + - name: (INSTALL) Decompress the binary ansible.builtin.shell: "bzip2 -dc {{ get_url_restic.dest }} > {{ restic_bin_bath }}" args: creates: '{{ restic_download_path }}/bin/restic-{{ restic_version }}' - - name: Ensure permissions are correct + - name: (INSTALL) Ensure permissions are correct ansible.builtin.file: path: '{{ restic_download_path }}/bin/restic-{{ restic_version }}' mode: '0755' owner: '{{ restic_dir_owner }}' group: '{{ restic_dir_group }}' - - name: Test the binary + - name: (INSTALL) Test the binary ansible.builtin.command: "{{ restic_bin_bath }} version" ignore_errors: true register: restic_test_result - - name: Remove faulty binary + - name: (INSTALL) Remove faulty binary ansible.builtin.file: path: '{{ restic_bin_bath }}' state: absent when: "'FAILED' in restic_test_result.stderr" - - name: Fail if restic could not be installed + - name: (INSTALL) Fail if restic could not be installed ansible.builtin.fail: msg: >- Restic binary has been faulty and has been removed. Try to re-run the role and make sure you have bzip2 installed! when: "'FAILED' in restic_test_result.stderr" - - name: Create symbolic link to the correct version + - name: (INSTALL) Create symbolic link to the correct version ansible.builtin.file: src: '{{ restic_download_path }}/bin/restic-{{ restic_version }}' path: '{{ restic_install_path }}/restic' state: link force: true rescue: - - name: try restic self-update + - name: (INSTALL) try restic self-update become: true ansible.builtin.command: "{{ restic_install_path }}/restic self-update" diff --git a/tasks/main.yml b/tasks/main.yml index 09466bb..95a64a9 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -2,18 +2,18 @@ - name: add OS specific variables ansible.builtin.include_vars: "{{ lookup('first_found', restic_os_variables) }}" -- name: perform versionscheck +- name: perform optional versionscheck ansible.builtin.include_tasks: 'versioncheck.yml' - when: submodules_versioncheck|bool + when: submodules_versioncheck | bool - name: make sure restic is available ansible.builtin.include_tasks: 'preperation.yml' -- name: Install restic +- name: make sure restic is installed ansible.builtin.include_tasks: 'install.yml' when: not restic_executable.stat.exists or not restic_installed.stat.exists -- name: Configure restic +- name: initialize restic repo(s) ansible.builtin.include_tasks: 'configure.yml' - name: create backup script diff --git a/tasks/preperation.yml b/tasks/preperation.yml index 22d58e2..6b48b6e 100644 --- a/tasks/preperation.yml +++ b/tasks/preperation.yml @@ -1,5 +1,6 @@ --- -- name: Ensure restic directories exist +- name: (PREPARE) Ensure restic directories exist + become: true ansible.builtin.file: state: 'directory' path: '{{ item }}' @@ -8,12 +9,12 @@ group: '{{ restic_dir_group }}' with_items: '{{ restic_create_paths }}' -- name: Check if downloaded binary is present +- name: (PREPARE) Check if downloaded binary is present ansible.builtin.stat: path: '{{ restic_download_path }}/bin/restic-{{ restic_version }}' register: restic_executable -- name: Check if installed binary is present +- name: (PREPARE) Check if installed binary is present ansible.builtin.stat: path: '{{ restic_install_path }}/restic' register: restic_installed diff --git a/tasks/restic_create_cron.yml b/tasks/restic_create_cron.yml new file mode 100644 index 0000000..28e6102 --- /dev/null +++ b/tasks/restic_create_cron.yml @@ -0,0 +1,18 @@ +--- +- name: (SCHEDULE) (CRON) install restic backup cronjob + ansible.builtin.cron: + name: "do1jlr.restic backup {{ item.name }}" + job: "CRON=true {{ restic_script_dir }}/backup-{{ item.name | replace(' ', '') }}.sh" + minute: '{{ item.schedule_minute | default("*") }}' + hour: '{{ item.schedule_hour | default("2") }}' + weekday: '{{ item.schedule_weekday | default("*") }}' + month: '{{ item.schedule_month | default("*") }}' + state: present + user: 'root' + cron_file: restic_backup + become: true + no_log: "{{ restic_no_log }}" + with_items: '{{ restic_backups }}' + when: + - item.name is defined + - item.scheduled | default(false) diff --git a/tasks/restic_create_systemd.yml b/tasks/restic_create_systemd.yml new file mode 100644 index 0000000..2f31889 --- /dev/null +++ b/tasks/restic_create_systemd.yml @@ -0,0 +1,66 @@ +--- +- name: (SCHEDULE) (SYSTEMD) create systemd timer + block: + - name: (SCHEDULE) (SYSTEMD) copy systemd timer + become: true + ansible.builtin.template: + src: templates/restic.timer.j2 + dest: "/lib/systemd/system/restic-{{ item.name | replace(' ', '') }}.timer" + owner: 'root' + group: 'root' + mode: '0644' + no_log: "{{ restic_no_log }}" + with_items: '{{ restic_backups }}' + notify: systemctl restart restic.timer + when: + - item.name is defined + - item.scheduled | default(false) + + - name: (SCHEDULE) (SYSTEMD) copy systemd service + become: true + ansible.builtin.template: + src: templates/restic.service.j2 + dest: "/lib/systemd/system/restic-{{ item.name | replace(' ', '') }}.service" + owner: 'root' + group: 'root' + mode: '0644' + no_log: "{{ restic_no_log }}" + with_items: '{{ restic_backups }}' + when: + - item.name is defined + - item.scheduled | default(false) + + - name: (SCHEDULE) (SYSTEMD) Enable restic service + become: true + ansible.builtin.systemd: + name: "restic-{{ item.name | replace(' ', '') | string }}.service" + enabled: true + daemon_reload: true + masked: false + with_items: '{{ restic_backups }}' + notify: systemctl restart restic.timer + when: + - item.name is defined + - item.scheduled | default(false) + + - name: (SCHEDULE) (SYSTEMD) Enable and start restic timer + become: true + ansible.builtin.systemd: + name: "restic-{{ item.name | replace(' ', '') | string }}.timer" + enabled: true + state: started + daemon_reload: true + masked: false + with_items: '{{ restic_backups }}' + notify: systemctl restart restic.timer + when: + - item.name is defined + - item.scheduled | default(false) + when: + - ansible_service_mgr == 'systemd' + - restic_schedule_type == "systemd" + - restic_create_schedule | bool + rescue: + - name: set cronjob intead of systemd + set_fact: + restic_force_cron: true diff --git a/tasks/restic_delete_cron.yml b/tasks/restic_delete_cron.yml new file mode 100644 index 0000000..d8d6095 --- /dev/null +++ b/tasks/restic_delete_cron.yml @@ -0,0 +1,18 @@ +--- +- name: (SCHEDULE) (CRON) remove restic backup cronjob + ansible.builtin.cron: + name: "do1jlr.restic backup {{ item.name }}" + job: "CRON=true {{ restic_script_dir }}/backup-{{ item.name | replace(' ', '') }}.sh" + minute: '{{ item.schedule_minute | default("*") }}' + hour: '{{ item.schedule_hour | default("2") }}' + weekday: '{{ item.schedule_weekday | default("*") }}' + month: '{{ item.schedule_month | default("*") }}' + state: present + user: 'root' + cron_file: restic_backup + become: true + no_log: "{{ restic_no_log }}" + with_items: '{{ restic_backups }}' + when: + - item.name is defined + - item.scheduled | default(false) diff --git a/tasks/restic_delete_systemd.yml b/tasks/restic_delete_systemd.yml new file mode 100644 index 0000000..6a60695 --- /dev/null +++ b/tasks/restic_delete_systemd.yml @@ -0,0 +1,60 @@ +--- +- name: (SCHEDULE) (SYSTEMD) remove systemd timer + block: + - name: (SCHEDULE) (SYSTEMD) mask restic timer + become: true + ansible.builtin.systemd: + name: "restic-{{ item.name | replace(' ', '') | string }}.timer" + enabled: false + state: stopped + daemon_reload: true + masked: false + with_items: '{{ restic_backups }}' + ignore_errors: true + tags: skip_ansible_lint + failed_when: false + changed_when: false + when: + - item.name is defined + - item.scheduled | default(false) + - restic_schedule_type == "cronjob" or restic_force_cron | default(false) + + - name: (SCHEDULE) (SYSTEMD) mask restic service + become: true + ansible.builtin.systemd: + name: "restic-{{ item.name | replace(' ', '') | string }}.service" + enabled: false + state: stopped + daemon_reload: true + masked: true + with_items: '{{ restic_backups }}' + when: + - item.name is defined + - item.scheduled | default(false) + - restic_schedule_type == "cronjob" or restic_force_cron | default(false) + ignore_errors: true + failed_when: false + tags: skip_ansible_lint + changed_when: false + + - name: (SCHEDULE) (SYSTEMD) delete systemd .timer file + become: true + ansible.builtin.file: + path: "/lib/systemd/system/restic-{{ item.name | replace(' ', '') }}.timer" + state: absent + with_items: '{{ restic_backups }}' + when: + - item.name is defined + - item.scheduled | default(false) + - restic_schedule_type == "cronjob" or restic_force_cron | default(false) + + - name: (SCHEDULE) (SYSTEMD) delete systemd .service file + become: true + ansible.builtin.file: + path: "/lib/systemd/system/restic-{{ item.name | replace(' ', '') }}.service" + state: absent + with_items: '{{ restic_backups }}' + when: + - item.name is defined + - item.scheduled | default(false) + - restic_schedule_type == "cronjob" or restic_force_cron | default(false) diff --git a/tasks/schedule.yml b/tasks/schedule.yml index 54a02cc..185a572 100644 --- a/tasks/schedule.yml +++ b/tasks/schedule.yml @@ -1,146 +1,32 @@ --- -- name: create systemd timer - block: - - name: copy systemd timer - become: true - ansible.builtin.template: - src: templates/restic.timer.j2 - dest: "/lib/systemd/system/restic-{{ item.name | replace(' ', '') }}.timer" - owner: 'root' - group: 'root' - mode: '0644' - no_log: "{{ restic_no_log }}" - with_items: '{{ restic_backups }}' - notify: systemctl restart restic.timer - when: - - item.name is defined - - item.scheduled | default(false) - - - name: copy systemd service - become: true - ansible.builtin.template: - src: templates/restic.service.j2 - dest: "/lib/systemd/system/restic-{{ item.name | replace(' ', '') }}.service" - owner: 'root' - group: 'root' - mode: '0644' - no_log: "{{ restic_no_log }}" - with_items: '{{ restic_backups }}' - when: - - item.name is defined - - item.scheduled | default(false) - - - name: copy systemd service - become: true - ansible.builtin.template: - src: templates/restic.service.j2 - dest: "/lib/systemd/system/restic-{{ item.name | replace(' ', '') }}.service" - owner: 'root' - group: 'root' - mode: '0644' - no_log: "{{ restic_no_log }}" - with_items: '{{ restic_backups }}' - when: - - item.name is defined - - item.scheduled | default(false) - - - name: Enable restic service - become: true - ansible.builtin.systemd: - name: "restic-{{ item.name | replace(' ', '') | string }}.service" - enabled: true - daemon_reload: true - masked: false - with_items: '{{ restic_backups }}' - notify: systemctl restart restic.timer - when: - - item.name is defined - - item.scheduled | default(false) - - - name: Enable and start restic timer - become: true - ansible.builtin.systemd: - name: "restic-{{ item.name | replace(' ', '') | string }}.timer" - enabled: true - state: started - daemon_reload: true - masked: false - with_items: '{{ restic_backups }}' - notify: systemctl restart restic.timer - when: - - item.name is defined - - item.scheduled | default(false) - - - name: delete old cronjob entry if available - ansible.builtin.cron: - name: "do1jlr.restic backup {{ item.name }}" - job: "CRON=true {{ restic_script_dir }}/backup-{{ item.name | replace(' ', '') }}.sh" - minute: '{{ item.schedule_minute | default("*") }}' - hour: '{{ item.schedule_hour | default("2") }}' - weekday: '{{ item.schedule_weekday | default("*") }}' - month: '{{ item.schedule_month | default("*") }}' - state: absent - cron_file: '/etc/crontab' - user: 'root' - become: true - no_log: "{{ restic_no_log }}" - with_items: '{{ restic_backups }}' - when: - - item.name is defined - - item.scheduled | default(false) +- name: (SCHEDULE) create restic systemd timer + ansible.builtin.include_tasks: restic_create_systemd.yml when: - ansible_service_mgr == 'systemd' - restic_schedule_type == "systemd" - restic_create_schedule | bool - rescue: - - name: set cronjob intead of systemd - set_fact: - restic_force_cron: true -- name: install cronjob instead of systemd - ansible.builtin.cron: - name: "do1jlr.restic backup {{ item.name }}" - job: "CRON=true {{ restic_script_dir }}/backup-{{ item.name | replace(' ', '') }}.sh" - minute: '{{ item.schedule_minute | default("*") }}' - hour: '{{ item.schedule_hour | default("2") }}' - weekday: '{{ item.schedule_weekday | default("*") }}' - month: '{{ item.schedule_month | default("*") }}' - state: present - cron_file: '/etc/crontab' - user: 'root' - become: true - no_log: "{{ restic_no_log }}" - with_items: '{{ restic_backups }}' +- name: (SCHEDULE) delete systemd timers if available + ansible.builtin.include_tasks: restic_delete_systemd.yml + when: + - ansible_service_mgr == 'systemd' + - restic_schedule_type == "cronjob" or restic_force_cron | default(false) + - restic_create_schedule | bool + +- name: (SCHEDULE) delete old cron entry from previous versions of this role + ansible.builtin.include_tasks: delete_legacy_cron_entry.yml + when: restic_do_not_cleanup_cron | bool + +- name: (SCHEDULE) install restic via cronjob + ansible.builtin.include_tasks: restic_create_cron.yml when: - restic_create_schedule | bool - - item.name is defined - - item.scheduled | default(false) - ansible_service_mgr != 'systemd' or restic_force_cron | default(false) or restic_schedule_type == "cronjob" -- name: make sure no unwanted systemd timer is available - ansible.builtin.systemd: - name: "restic-{{ item.name | replace(' ', '') | string }}.timer" - state: 'stopped' - enabled: false - masked: true - with_items: '{{ restic_backups }}' - ignore_errors: true +- name: (SCHEDULE) remove restic cronjobs + ansible.builtin.include_tasks: restic_delete_cron.yml when: - restic_create_schedule | bool - - item.name is defined - - item.scheduled | default(false) - - ansible_service_mgr != 'systemd' or restic_force_cron | default(false) or restic_schedule_type == "cronjob" - -- name: mask systemd service - ansible.builtin.systemd: - name: "restic-{{ item.name | replace(' ', '') | string }}.service" - state: 'stopped' - enabled: false - masked: true - with_items: '{{ restic_backups }}' - ignore_errors: true - when: - - restic_create_schedule | bool - - item.name is defined - - item.scheduled | default(false) - - ansible_service_mgr != 'systemd' or restic_force_cron | default(false) or restic_schedule_type == "cronjob" + - ansible_service_mgr == 'systemd' + - not restic_force_cron | default(false) + - restic_schedule_type != "cronjob" diff --git a/templates/restic_script_Linux.j2 b/templates/restic_script_Linux.j2 index 93a15d3..cfd3751 100644 --- a/templates/restic_script_Linux.j2 +++ b/templates/restic_script_Linux.j2 @@ -3,6 +3,28 @@ # Backup script for {{ item.src|default('stdin') }} # Use this file to create a Backup and prune existing data with one execution. +pid="/var/run/restic_backup_{{ item.name | regex_replace('\'', '\'\\\'\'') }}.pid" +trap "rm -f $pid" SIGSEGV +trap "rm -f $pid" SIGINT + +if [ -e $pid ]; then + echo "A nother version of this restic backup script is already running!" + {% if item.mail_on_error is defined and item.mail_on_error == true %} + mail -s "starting restic backup failed on {{ ansible_hostname }}" {{ item.mail_address }} <<< "Another restic backup process is already running. We canceled starting a new restic backup script running at {{ ansible_hostname }} at $(date -u '+%Y-%m-%d %H:%M:%S'). + {%- if item.src is defined -%} + {{ ' ' }}We tried to backup '{{ item.src }}'. + {%- endif -%} + {{ ' ' }}Please repair the restic-{{ item.name | replace(' ', '') }} job." + {% endif %} + exit # pid file exists, another instance is running, so now we politely exit +else + echo $$ > $pid # pid file doesn't exit, create one and go on +fi +# your normal workflow here... + + + + {% if item.disable_logging is defined and item.disable_logging %} {% set backup_result_log, backup_output_log = "/dev/null", "/dev/null" %} {% set forget_result_log, forget_output_log = "/dev/null", "/dev/null" %} @@ -164,3 +186,5 @@ else {{ ' ' }}Please repair the restic-{{ item.name | replace(' ', '') }} job." {% endif %} fi +rm -f $pid # remove pid file just before exiting +exit diff --git a/vars/main.yml b/vars/main.yml index e1abffb..c7e5ec8 100644 --- a/vars/main.yml +++ b/vars/main.yml @@ -10,5 +10,5 @@ restic_os_variables: paths: - 'vars' -playbook_version_number: 20 # should be int +playbook_version_number: 22 # should be int playbook_version_path: 'do1jlr.restic.version'