--- - name: Backup and/or Upgrade MikroTik hosts: mikrotik_routers 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: - name: Get router identity community.routeros.command: commands: /system identity print register: identity_raw changed_when: false tags: always - name: Parse router name 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 ansible.builtin.set_fact: ts: "{{ date_out.stdout }}" tags: always - 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 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" mode: "0600" delegate_to: localhost vars: ansible_connection: local no_log: true - 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 ansible.builtin.set_fact: _update_text: "{{ update_check.stdout[0] | replace('\r', '') }}" - 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: 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: Show installed and available versions ansible.builtin.debug: msg: "{{ router_name }}: installed={{ installed_version }}, available={{ latest_version }}" - 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: Install and verify the available version when: installed_version != latest_version block: - name: Reset persistent connection before installation ansible.builtin.meta: reset_connection - 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 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. always: - name: Discard the installation connection ansible.builtin.meta: reset_connection # 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: 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 }}."