diff --git a/mikrotikbackup_clean.yml b/mikrotikbackup_clean.yml index aa7884e..2779249 100644 --- a/mikrotikbackup_clean.yml +++ b/mikrotikbackup_clean.yml @@ -1,151 +1,195 @@ +--- - name: Backup and/or Upgrade MikroTik hosts: mikrotik_routers - gather_facts: no + gather_facts: false vars: backup_dir: /opt/mikrotik_backups/ + ansible_connection: ansible.netcommon.network_cli + ansible_network_os: community.routeros.routeros + ansible_connect_timeout: 60 + ansible_command_timeout: 60 + upgrade_command_timeout: 600 + upgrade_reconnect_timeout: 600 + upgrade_verify_retries: 40 + upgrade_verify_delay: 15 tasks: - - # ---------------------------- - # Always: identity + timestamp - # ---------------------------- - name: Get router identity community.routeros.command: commands: /system identity print register: identity_raw + changed_when: false tags: always - name: Parse router name - set_fact: - router_name: "{{ identity_raw.stdout[0].split(': ')[1] | trim }}" + ansible.builtin.set_fact: + router_name: "{{ identity_raw.stdout[0].split(': ', 1)[1] | trim }}" tags: always - name: Get timestamp ansible.builtin.command: date +%Y-%m-%d_%H-%M-%S register: date_out delegate_to: localhost + vars: + ansible_connection: local + changed_when: false tags: always - name: Set timestamp fact - set_fact: + ansible.builtin.set_fact: ts: "{{ date_out.stdout }}" tags: always - # ---------------------------- - # Backup (tag: backup) - # ---------------------------- - - name: Ensure local backup directory exists - ansible.builtin.file: - path: "{{ backup_dir }}" - state: directory - mode: "0755" - delegate_to: localhost + - name: Back up router configuration tags: [backup, never] + block: + - name: Ensure local backup directory exists + ansible.builtin.file: + path: "{{ backup_dir }}" + state: directory + mode: "0755" + delegate_to: localhost + vars: + ansible_connection: local - - name: Export router config - community.routeros.command: - commands: /export terse show-sensitive - register: export_cfg - tags: [backup, never] + - name: Export router config + community.routeros.command: + commands: /export terse show-sensitive + register: export_cfg + changed_when: false + no_log: true - - name: Save export locally - ansible.builtin.copy: - content: "{{ export_cfg.stdout[0] }}" - dest: "{{ backup_dir }}/{{ router_name }}-{{ ts }}.rsc" - delegate_to: localhost - tags: [backup, never] + - name: Save export locally + ansible.builtin.copy: + content: "{{ export_cfg.stdout[0] }}" + dest: "{{ backup_dir }}/{{ router_name }}-{{ ts }}.rsc" + mode: "0600" + delegate_to: localhost + vars: + ansible_connection: local + no_log: true - # ---------------------------- - # Upgrade (tag: upgrade) - # ---------------------------- - - name: Check current and latest available package versions - community.routeros.command: - commands: /system package update check-for-updates - register: update_check + - name: Check and upgrade RouterOS packages tags: [upgrade, never] + when: not ansible_check_mode + block: + - name: Check current and latest available package versions + community.routeros.command: + commands: /system package update check-for-updates + register: update_check + changed_when: false - - name: Normalize update output - set_fact: - _update_text: "{{ update_check.stdout[0] | replace('\r', '') }}" - tags: [upgrade, never] + - name: Normalize update output + ansible.builtin.set_fact: + _update_text: "{{ update_check.stdout[0] | replace('\r', '') }}" - - name: Debug raw update output - ansible.builtin.debug: - msg: "{{ _update_text }}" - tags: [upgrade, never] + - name: Parse installed and latest versions + ansible.builtin.set_fact: + installed_version: >- + {{ (_update_text | regex_findall('(?:installed|current)-version:[ \t]*([0-9][0-9A-Za-z.-]*)') + | first) | default('unknown') }} + latest_version: >- + {{ (_update_text | regex_findall('(?:latest|newest)-version:[ \t]*([0-9][0-9A-Za-z.-]*)') + | first) | default('unknown') }} - - name: Parse installed and latest versions - set_fact: - installed_version: >- - {{ - (_update_text | regex_findall('(?:installed|current)-version:[ ]*([0-9A-Za-z.]+)'))[0] - if (_update_text | regex_findall('(?:installed|current)-version:[ ]*([0-9A-Za-z.]+)')) - else 'unknown' - }} - latest_version: >- - {{ - (_update_text | regex_findall('(?:latest|newest)-version:[ ]*([0-9A-Za-z.]+)'))[0] - if (_update_text | regex_findall('(?:latest|newest)-version:[ ]*([0-9A-Za-z.]+)')) - else 'unknown' - }} - tags: [upgrade, never] + - name: Require valid versions before attempting an upgrade + ansible.builtin.assert: + that: + - installed_version != 'unknown' + - latest_version != 'unknown' + fail_msg: "Could not parse versions from update output: {{ _update_text }}" - - name: Fail if versions could not be parsed - ansible.builtin.fail: - msg: > - Could not parse versions from update output. - Raw text was: {{ _update_text }} - when: installed_version == 'unknown' or latest_version == 'unknown' - tags: [upgrade, never] + - name: Show installed and available versions + ansible.builtin.debug: + msg: "{{ router_name }}: installed={{ installed_version }}, available={{ latest_version }}" - - name: Debug parsed versions - ansible.builtin.debug: - msg: - - "Installed: {{ installed_version }}" - - "Latest: {{ latest_version }}" - tags: [upgrade, never] + - name: Skip upgrade if already on latest + ansible.builtin.debug: + msg: "{{ router_name }} is already on {{ installed_version }}. Skipping upgrade." + when: installed_version == latest_version - - name: Skip upgrade if already on latest - ansible.builtin.debug: - msg: "Router {{ router_name }} is already on latest version {{ installed_version }}. Skipping upgrade." - when: installed_version == latest_version - tags: [upgrade, never] + - name: Install and verify the available version + when: installed_version != latest_version + block: + - name: Reset persistent connection before installation + ansible.builtin.meta: reset_connection - - name: Trigger package download and install - community.routeros.command: - commands: /system package update install - vars: - ansible_command_timeout: 60 - when: installed_version != latest_version - ignore_errors: true - tags: [upgrade, never] + - name: Request package installation + block: + - name: Trigger package download and install + community.routeros.command: + commands: /system package update install + vars: + ansible_command_timeout: "{{ upgrade_command_timeout }}" + register: install_result - - name: Wait before reconnecting after reboot - ansible.builtin.pause: - seconds: 30 - when: installed_version != latest_version - tags: [upgrade, never] + rescue: + # A reboot can interrupt the command response. This is NOT + # success: the mandatory version check below decides the result. + - name: Record installation error for final verification + ansible.builtin.debug: + msg: >- + Installation returned an error: {{ install_result.msg | default('Unknown error') }}. + The command may or may not have reached the router. + Continuing only to verify the actual installed version. - - name: Wait for router to come back online after reboot - community.routeros.command: - commands: /system resource print - register: reboot_wait - until: reboot_wait is succeeded - retries: 20 - delay: 15 - when: installed_version != latest_version - tags: [upgrade, never] + always: + - name: Discard the installation connection + ansible.builtin.meta: reset_connection - - name: Confirm upgraded version - community.routeros.command: - commands: /system resource print - register: post_upgrade_info - when: installed_version != latest_version - tags: [upgrade, never] + # This readiness check does not claim that a reboot occurred. + # The exact version comparison below is the success criterion. + - name: Wait for SSH availability from Semaphore + ansible.builtin.wait_for: + host: >- + {{ hostvars[inventory_hostname].ansible_host + | default(hostvars[inventory_hostname].ansible_ssh_host) + | default(inventory_hostname) }} + port: >- + {{ hostvars[inventory_hostname].ansible_port + | default(hostvars[inventory_hostname].ansible_ssh_port) + | default(22) }} + state: started + search_regex: SSH- + delay: 30 + sleep: 5 + connect_timeout: 5 + timeout: "{{ upgrade_reconnect_timeout }}" + delegate_to: localhost + vars: + ansible_connection: local + changed_when: false - - name: Show post-upgrade RouterOS version - ansible.builtin.debug: - msg: "{{ post_upgrade_info.stdout[0] | regex_search('version: .+') }}" - when: installed_version != latest_version - tags: [upgrade, never] \ No newline at end of file + - name: Reset persistent connection before verification + ansible.builtin.meta: reset_connection + + - name: Verify the installed version + block: + - name: Wait until the router reports the expected version + community.routeros.command: + commands: /system resource print + register: post_upgrade_info + changed_when: false + until: + - post_upgrade_info is succeeded + - >- + ((post_upgrade_info.stdout | default(['']) | first + | regex_findall('(?m)^\s*version:[ \t]*([0-9][0-9A-Za-z.-]*)') + | first) | default('unknown')) == latest_version + retries: "{{ upgrade_verify_retries }}" + delay: "{{ upgrade_verify_delay }}" + + rescue: + - name: Fail if the expected RouterOS version could not be verified + ansible.builtin.fail: + msg: >- + Upgrade NOT verified for {{ router_name }}. + Previous={{ installed_version }}; expected={{ latest_version }}. + Last router response={{ post_upgrade_info.stdout | default([]) }}. + Verification error={{ post_upgrade_info.msg | default('Version mismatch') }}. + Install error={{ install_result.msg | default('No module error reported') }}. + + - name: Report verified upgrade + ansible.builtin.debug: + msg: "{{ router_name }}: verified RouterOS {{ installed_version }} -> {{ latest_version }}."