diff --git a/mikrotikbackup_clean.yml b/mikrotikbackup_clean.yml index af7d53f..c2f951f 100644 --- a/mikrotikbackup_clean.yml +++ b/mikrotikbackup_clean.yml @@ -1,149 +1,331 @@ # mikrotikbackup_clean.yml -- name: Backup and/or Upgrade MikroTik - hosts: mikrotik_routers - gather_facts: no +--- +- name: Backup and/or upgrade MikroTik + hosts: mikrotiks + gather_facts: false vars: - backup_dir: /opt/mikrotik_backups/ + # This directory is located on the Semaphore runner + backup_dir: /opt/mikrotik_backups + + # Maximum time for a router to return after an upgrade + upgrade_wait_timeout: 900 tasks: + # ------------------------------------------------------------------------- + # Common information + # ------------------------------------------------------------------------- - # ---------------------------- - # Always: identity + timestamp - # ---------------------------- - name: Get router identity community.routeros.command: - commands: /system identity print + commands: + - /system identity get name register: identity_raw - tags: always + changed_when: false + tags: + - always - - name: Parse router name - set_fact: - router_name: "{{ identity_raw.stdout[0].split(': ')[1] | trim }}" - tags: always + - name: Build safe router name + ansible.builtin.set_fact: + router_name: >- + {{ + ( + identity_raw.stdout[0] + | default(inventory_hostname, true) + | trim + ) + | regex_replace('[^A-Za-z0-9._-]+', '_') + | regex_replace('^_+|_+$', '') + }} + tags: + - always - - name: Get timestamp - ansible.builtin.command: date +%Y-%m-%d_%H-%M-%S + - name: Validate router name + ansible.builtin.assert: + that: + - router_name | length > 0 + fail_msg: >- + Could not determine RouterOS identity for + {{ inventory_hostname }}. + quiet: true + tags: + - always + + - name: Generate backup timestamp + ansible.builtin.command: + argv: + - date + - --utc + - "+%Y-%m-%d_%H-%M-%S" register: date_out + changed_when: false delegate_to: localhost - tags: always + tags: + - always - name: Set timestamp fact - set_fact: - ts: "{{ date_out.stdout }}" - tags: always + ansible.builtin.set_fact: + backup_timestamp: "{{ date_out.stdout | trim }}" + tags: + - always + + # ------------------------------------------------------------------------- + # Backup + # Run with: --tags backup + # ------------------------------------------------------------------------- - # ---------------------------- - # Backup (tag: backup) - # ---------------------------- - name: Ensure local backup directory exists ansible.builtin.file: path: "{{ backup_dir }}" state: directory - mode: "0755" + mode: "0700" delegate_to: localhost - tags: [backup, never] + run_once: true + tags: + - backup + - never - - name: Export router config + - name: Export RouterOS configuration community.routeros.command: - commands: /export terse show-sensitive + commands: + - /export terse show-sensitive register: export_cfg - tags: [backup, never] + changed_when: false + no_log: true + tags: + - backup + - never - - name: Save export locally + - name: Validate exported configuration + ansible.builtin.assert: + that: + - export_cfg.stdout is defined + - export_cfg.stdout | length > 0 + - export_cfg.stdout[0] | default('') | trim | length > 0 + fail_msg: >- + RouterOS returned an empty configuration export + for {{ inventory_hostname }}. + quiet: true + no_log: true + tags: + - backup + - never + + - name: Set backup filename + ansible.builtin.set_fact: + backup_file: >- + {{ + backup_dir + ~ '/' + ~ router_name + ~ '-' + ~ backup_timestamp + ~ '.rsc' + }} + tags: + - backup + - never + + - name: Save RouterOS configuration locally ansible.builtin.copy: - content: "{{ export_cfg.stdout[0] }}" - dest: "{{ backup_dir }}/{{ router_name }}-{{ ts }}.rsc" + content: "{{ export_cfg.stdout[0] | trim }}\n" + dest: "{{ backup_file }}" + mode: "0600" delegate_to: localhost - tags: [backup, never] + no_log: true + diff: false + tags: + - backup + - never - # ---------------------------- - # Upgrade (tag: upgrade) - # ---------------------------- - - name: Check current and latest available package versions + - name: Show backup result + ansible.builtin.debug: + msg: >- + MikroTik backup completed: + host={{ inventory_hostname }} + identity={{ router_name }} + file={{ backup_file }} + tags: + - backup + - never + + # ------------------------------------------------------------------------- + # Upgrade + # Run with: --tags upgrade + # Run with backup before upgrade: --tags backup,upgrade + # ------------------------------------------------------------------------- + + - name: Check current and latest RouterOS versions community.routeros.command: - commands: /system package update check-for-updates + commands: + - /system package update check-for-updates register: update_check - tags: [upgrade, never] + changed_when: false + tags: + - upgrade + - never - name: Normalize update output - set_fact: - _update_text: "{{ update_check.stdout[0] | replace('\r', '') }}" - tags: [upgrade, never] + ansible.builtin.set_fact: + update_text: >- + {{ + update_check.stdout[0] + | default('') + | replace('\r', '') + }} + tags: + - upgrade + - never - # ⬇️ Add this to see exactly what RouterOS returns before parsing - - name: Debug raw update output - ansible.builtin.debug: - msg: "{{ _update_text }}" - tags: [upgrade, never] + - name: Extract version matches + ansible.builtin.set_fact: + installed_version_matches: >- + {{ + update_text + | regex_findall( + '(?im)^\\s*(?:installed|current)-version:\\s*(\\S+)\\s*$' + ) + }} + latest_version_matches: >- + {{ + update_text + | regex_findall( + '(?im)^\\s*(?:latest|newest)-version:\\s*(\\S+)\\s*$' + ) + }} + tags: + - upgrade + - never - - name: Parse installed and latest versions - set_fact: + - name: Set installed and latest versions + ansible.builtin.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.]+)')) + installed_version_matches[0] + if installed_version_matches | length > 0 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.]+)')) + latest_version_matches[0] + if latest_version_matches | length > 0 else 'unknown' }} - tags: [upgrade, never] + tags: + - upgrade + - never - - 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 raw update output + ansible.builtin.debug: + msg: "{{ update_text }}" + tags: + - upgrade + - never - - name: Debug parsed versions + - name: Validate parsed RouterOS versions + ansible.builtin.assert: + that: + - installed_version != 'unknown' + - latest_version != 'unknown' + fail_msg: >- + Could not parse RouterOS versions on + {{ inventory_hostname }}. + Raw output: {{ update_text }} + quiet: true + tags: + - upgrade + - never + + - name: Show RouterOS versions ansible.builtin.debug: msg: + - "Router: {{ router_name }}" - "Installed: {{ installed_version }}" - "Latest: {{ latest_version }}" - tags: [upgrade, never] + tags: + - upgrade + - never - - name: Skip upgrade if already on latest + - name: Show already up-to-date result ansible.builtin.debug: - msg: "Router {{ router_name }} is already on latest version {{ installed_version }}. Skipping upgrade." + msg: >- + Router {{ router_name }} is already running + RouterOS {{ installed_version }}. when: installed_version == latest_version - tags: [upgrade, never] + tags: + - upgrade + - never - - name: Trigger package download and install + - name: Trigger RouterOS package installation community.routeros.command: - commands: /system package update install + commands: + - /system package update install register: upgrade_result + changed_when: true + failed_when: false when: installed_version != latest_version - tags: [upgrade, never] + tags: + - upgrade + - never - - name: Wait for router to come back online after reboot + - name: Wait for router SSH service to stop + ansible.builtin.wait_for: + host: "{{ ansible_host | default(inventory_hostname) }}" + port: "{{ ansible_port | default(22) | int }}" + state: stopped + connect_timeout: 5 + timeout: 180 + delegate_to: localhost + failed_when: false + when: installed_version != latest_version + tags: + - upgrade + - never + + - name: Wait for router SSH service to start + ansible.builtin.wait_for: + host: "{{ ansible_host | default(inventory_hostname) }}" + port: "{{ ansible_port | default(22) | int }}" + state: started + delay: 10 + connect_timeout: 5 + timeout: "{{ upgrade_wait_timeout }}" + delegate_to: localhost + when: installed_version != latest_version + tags: + - upgrade + - never + + - name: Reset RouterOS connection + ansible.builtin.meta: reset_connection + when: installed_version != latest_version + tags: + - upgrade + - never + + - name: Read RouterOS version after upgrade community.routeros.command: - commands: /system resource print - register: reboot_wait - until: reboot_wait is not failed - retries: 30 + commands: + - /system resource get version + register: post_upgrade_version + retries: 20 delay: 15 - when: - - installed_version != latest_version - - upgrade_result is not failed - tags: [upgrade, never] - - - name: Confirm upgraded version - community.routeros.command: - commands: /system resource print - register: post_upgrade_info + until: post_upgrade_version is succeeded + changed_when: false when: installed_version != latest_version - tags: [upgrade, never] + tags: + - upgrade + - never - - name: Show post-upgrade RouterOS version + - name: Show RouterOS version after upgrade ansible.builtin.debug: - msg: "{{ post_upgrade_info.stdout[0] | regex_search('version: .+') }}" + msg: >- + Router {{ router_name }} is online. + RouterOS version: + {{ post_upgrade_version.stdout[0] | default('unknown') | trim }} when: installed_version != latest_version - tags: [upgrade, never] + tags: + - upgrade + - never