From 004292dba0e5fd7c0bb197718d8031a58b69912a Mon Sep 17 00:00:00 2001 From: pavel Date: Wed, 1 Apr 2026 13:44:31 +0300 Subject: [PATCH] 1344 --- .../sot-updater-pre-indoorbackup.yml | 2425 +++++++++++++++++ files/ansible-playbooks/sot-updater.yml | 82 +- 2 files changed, 2505 insertions(+), 2 deletions(-) create mode 100644 files/ansible-playbooks/sot-updater-pre-indoorbackup.yml diff --git a/files/ansible-playbooks/sot-updater-pre-indoorbackup.yml b/files/ansible-playbooks/sot-updater-pre-indoorbackup.yml new file mode 100644 index 0000000..d695e39 --- /dev/null +++ b/files/ansible-playbooks/sot-updater-pre-indoorbackup.yml @@ -0,0 +1,2425 @@ +# sot-updater.yml — Read fw on Dev1 + Dev2, publish to NetBox via Rabbit (telemetry mode, no upgrade scheduling) + +- name: Read fw on Dev1 + Dev2, publish NetBox custom fields (full base, AIRPINGs, soft-fail telemetry, no upgrade scheduling) + hosts: all + gather_facts: no + + vars: + pathprefix: "PATH=/sbin:/usr/sbin:/bin:/usr/bin:$PATH; " + dev1_user: "root" + dev1_pass: "wavewave" + ssh_timeout: 30 + + dev2_host: "192.168.1.1" + dev2_port: 22 + dev2_ssh_user: "root" + dev2_passfiles: [ "basicpass", "basicpass2" ] + + dev2_side_ip: "192.168.1.11/24" + dev1_iface: "br-wan" + arping_iface: "eth0" + dev2_side_ip_addr: "{{ dev2_side_ip.split('/')[0] }}" + + ssh_opts_common: >- + -o PreferredAuthentications=password + -o PubkeyAuthentication=no + -o StrictHostKeyChecking=no + -o UserKnownHostsFile=/dev/null + -o NumberOfPasswordPrompts=1 + -o ConnectTimeout=15 + + rmq_host: "{{ lookup('env','RMQ_HOST') | default('10.210.12.2', true) }}" + rmq_port: "{{ lookup('env','RMQ_PORT') | default('15672', true) }}" + rmq_user: "{{ lookup('env','RMQ_USER') | default('admin', true) }}" + rmq_pass: "{{ lookup('env','RMQ_PASS') | default('change_me', true) }}" + rmq_vhost: "{{ lookup('env','RMQ_VHOST') | default('app', true) }}" + rmq_exchange: "{{ lookup('env','RMQ_EXCHANGE') | default('controls', true) }}" + control_queue: "{{ lookup('env','CONTROLQUEUE') | default('queue_controls', true) }}" + + # NetBox API (delta compare; user may override) + netbox_url: "http://netbox.gt-tiso.ikeja.co.za" + netbox_token: "7648e4f5ee370cda7834682e61b47c2ee8e95623" + + # --- Retry attempt tracking (default 0 if missing) --- + requeue_attempt: "{{ (lookup('env','REQUEUE_ATTEMPT') | default('0', true)) | int }}" + + pre_tasks: + - name: "NB preflight | Verify script exists" + delegate_to: localhost + ansible.builtin.stat: + path: "/opt/containers/ansible-worker/app/nb_onedevice_update.py" + register: nb_script + + - name: "NB preflight | Abort softly if script missing" + when: not nb_script.stat.exists + delegate_to: localhost + ansible.builtin.debug: + msg: + - "NB preflight skipped: /opt/containers/ansible-worker/app/nb_onedevice_update.py not found." + - "Tip: adjust chdir/path or script name." + + - name: "NB preflight | Run nb_onedevice_update.py for {{ inventory_hostname }}" + when: nb_script.stat.exists + delegate_to: localhost + environment: + PYTHONUNBUFFERED: "1" + args: + chdir: "/opt/containers/ansible-worker/app" + executable: /bin/bash + shell: | + set -o pipefail + python3 -u nb_onedevice_update.py "{{ inventory_hostname }}" --chatty 2>&1 + register: nb_preflight + changed_when: false + failed_when: false + + - name: "NB preflight | Show results" + when: nb_script.stat.exists + delegate_to: localhost + debug: + msg: + - "rc={{ nb_preflight.rc }}" + - "stdout_lines:" + - "{{ (nb_preflight.stdout_lines | default([''])) }}" + - "stderr_lines:" + - "{{ (nb_preflight.stderr_lines | default([''])) }}" + - "raw stdout (joined): {{ nb_preflight.stdout | default('') | trim }}" + + - name: "NB preflight | Detect cloud failure" + when: nb_script.stat.exists + delegate_to: localhost + vars: + _out: "{{ (nb_preflight.stdout | default('') | regex_replace('\r','')) }}" + _lines: >- + {{ _out.split('\n') | map('trim') | list }} + _cloud_fail_line: >- + {{ (_lines | select('match', '^FAIL\\s+Cloud\\b') | list | last | default('')) }} + _error_line: >- + {{ (_lines | select('match', '^FAIL\\s+') | list | last | default('')) }} + _rc_is_3: "{{ (nb_preflight.rc | default(1)) | int == 3 }}" + _cloud_error_type: >- + {%- if _error_line is search('network error') + and _error_line is search('500 error responses') -%} + cloud_http_500 + {%- elif (_cloud_fail_line | length) > 0 -%} + cloud_logic + {%- elif _rc_is_3 -%} + cloud_other + {%- else -%} + none + {%- endif -%} + _cloud_retryable: >- + {{ _cloud_error_type not in ['cloud_http_500'] }} + set_fact: + cloud_fail_line: "{{ _cloud_fail_line }}" + cloud_bad: "{{ _rc_is_3 or (( _cloud_fail_line | length ) > 0) }}" + cloud_error_type: "{{ _cloud_error_type }}" + cloud_retryable: "{{ _cloud_retryable }}" + + # --- Extract upgrade_cmd line if any --- + - name: "NB preflight | Extract upgrade_cmd line" + when: nb_script.stat.exists + delegate_to: localhost + set_fact: + nb_upgrade_line: >- + {{ + ( + nb_preflight.stdout_lines | default([]) | map('regex_replace','\r','') | map('trim') + | select('match', '^NB:\\s*upgrade_cmd\\s*=') + | list | first + ) | default('') + }} + + - name: "NB preflight | Parse upgrade_cmd value" + when: nb_upgrade_line | length > 0 + delegate_to: localhost + shell: | + printf '%s\n' "{{ nb_upgrade_line }}" | awk -F'=' '{print $2}' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' + args: { executable: /bin/bash } + register: up_cmd_sh + changed_when: false + + - name: "NB preflight | Set parsed upgrade_cmd" + when: nb_upgrade_line | length > 0 + delegate_to: localhost + set_fact: + nb_upgrade_cmd: "{{ up_cmd_sh.stdout | default('') | trim }}" + + - name: "NB preflight | Debug parsed upgrade_cmd" + when: nb_upgrade_line | length > 0 + delegate_to: localhost + debug: + msg: + - "upgrade_cmd='{{ nb_upgrade_cmd }}' (len={{ nb_upgrade_cmd|length }})" + + - name: "NB preflight | Parse OK line" + when: nb_script.stat.exists + delegate_to: localhost + vars: + nb_lines: >- + {{ (nb_preflight.stdout | default('') | regex_replace('\r','')) | split('\n') | map('trim') | list }} + nb_ok_line: >- + {{ (nb_lines | select('match', '^OK\\s+') | list | last | default('')) }} + nb_tokens: >- + {{ (nb_ok_line | regex_replace('^OK\\s+', '')).split() }} + nb_kv: >- + {{ + dict( + nb_tokens + | select('match', '^[a-zA-Z_]+=') + | map('split', '=', 1) + | map('list') + ) + }} + set_fact: + nb_ok: "{{ (nb_preflight.rc|default(1)) == 0 and (nb_ok_line|length)>0 }}" + nb_dev: "{{ (nb_tokens|first|default('')) if (nb_tokens|length>0) else '' }}" + nb_ip: "{{ nb_kv.get('ip','') }}" + nb_fw: "{{ nb_kv.get('fw','') }}" + nb_node: "{{ nb_kv.get('node','') }}" + nb_sector: "{{ nb_kv.get('sector','') }}" + nb_small: "{{ nb_kv.get('small','') }}" + nb_ok_line: "{{ nb_ok_line }}" + + - name: "NB preflight | Detect IP change" + when: nb_script.stat.exists + delegate_to: localhost + vars: + out: "{{ (nb_preflight.stdout | default('') | regex_replace('\r','')) }}" + reason: >- + {%- if 'IP: moving ' in out -%}moving + {%- elif 'IP: create new ' in out -%}create new + {%- elif 'IP: pruning stale ' in out -%}pruning stale + {%- else -%}none{%- endif -%} + set_fact: + nb_ip_changed: "{{ reason != 'none' }}" + nb_change_reason: "{{ reason }}" + + - name: "NB preflight | Verdict" + when: nb_script.stat.exists + delegate_to: localhost + debug: + msg: + - "Preflight verdict:" + - "Cloud fail: {{ cloud_bad|default(false) }}" + - "IP changed: {{ nb_ip_changed|default(false) }}" + - "Reason: {{ nb_change_reason|default('none') }}" + + - name: "NB preflight | Compute next_attempt" + delegate_to: localhost + set_fact: + next_attempt: "{{ (requeue_attempt | int) + 1 }}" + + # --- Requeue control: only 3 attempts, 300s apart --- + - name: "NB preflight | Publish delayed requeue (300s (10s)) if needed" + when: + - nb_script.stat.exists + - (nb_ip_changed | default(false)) or ( + (cloud_bad | default(false)) and + (cloud_error_type | default('none')) != 'cloud_http_500' + ) + - (requeue_attempt | int) < 3 + delegate_to: localhost + uri: + url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/app/deviceconfig.delayed/publish" + method: POST + user: "admin" + password: "change_me" + force_basic_auth: true + status_code: 200 + return_content: yes + headers: { content-type: "application/json" } + body_format: json + body: + properties: + content_type: "application/json" + headers: { x-delay: 900000 } + routing_key: "deviceconfig" + payload: "{{ { + 'inscope_device': (ansible_hostname | default(inventory_hostname)), + 'task_name': 'sot-updater', + 'attempt': next_attempt + } | to_json }}" + payload_encoding: "string" + register: rmq_requeue + changed_when: false + failed_when: false + + - name: "NB preflight | Log requeue publish response" + when: + - nb_script.stat.exists + - ((cloud_bad | default(false)) or (nb_ip_changed | default(false))) + - (requeue_attempt | int) < 3 + delegate_to: localhost + debug: + msg: + - "Requeue published (attempt {{ (requeue_attempt | int) + 1 }})" + - "HTTP: {{ rmq_requeue.status|default('n/a') }}" + - "JSON: {{ rmq_requeue.json|default('') }}" + + - name: "NB preflight | Stop host after requeue" + when: + - nb_script.stat.exists + - ( + (nb_ip_changed | default(false)) or + ( + (cloud_bad | default(false)) and + (cloud_error_type | default('none')) != 'cloud_http_500' + ) + ) + meta: end_host + + - name: "NB preflight | Pause 1s if OK" + when: + - nb_script.stat.exists + - not ((cloud_bad | default(false)) or (nb_ip_changed | default(false))) + delegate_to: localhost + pause: + seconds: 1 + + + # ----------------------- NetBox baseline (for delta compare) ----------------------- + + - name: NB baseline | Init baseline facts + delegate_to: localhost + set_fact: + nb_baseline_ok: false + nb_device_id: "" + nb_custom_fields_current: {} + changed_when: false + + - name: NB baseline | Debug netbox_url used + delegate_to: localhost + debug: + msg: + - "netbox_url={{ netbox_url }}" + - "inventory_hostname={{ inventory_hostname }}" + changed_when: false + + - name: NB baseline | Read id and custom_fields (fields) + delegate_to: localhost + uri: + url: "{{ netbox_url }}/api/dcim/devices/?name={{ inventory_hostname }}&fields=id,custom_fields" + method: GET + headers: + Authorization: "Token {{ netbox_token }}" + Accept: "application/json" + return_content: true + status_code: 200 + timeout: 10 + register: nb_cf_lookup + failed_when: false + changed_when: false + + - name: NB baseline | Debug raw response if json not parsed + delegate_to: localhost + debug: + msg: + - "nb_cf_lookup.status={{ nb_cf_lookup.status | default('NA') }}" + - "nb_cf_lookup.content_type={{ nb_cf_lookup.content_type | default('NA') }}" + - "nb_cf_lookup.json_defined={{ (nb_cf_lookup.json is defined) }}" + - "nb_cf_lookup.content_head={{ (nb_cf_lookup.content | default('') | regex_replace('\\n',' ') )[:200] }}" + when: nb_cf_lookup.json is not defined + changed_when: false + + - name: NB baseline | Set baseline facts (best effort; never crash play) + delegate_to: localhost + block: + - name: NB baseline | Choose parsed payload (json or content) + set_fact: + nb_cf_json_local: >- + {{ + (nb_cf_lookup.json is defined and nb_cf_lookup.json) + | ternary(nb_cf_lookup.json, (nb_cf_lookup.content | from_json)) + }} + changed_when: false + + - name: NB baseline | Set baseline facts from payload + set_fact: + nb_baseline_ok: true + nb_device_id: "{{ (nb_cf_json_local.results | default([]) | first | default({})).id | default('') }}" + nb_custom_fields_current: "{{ (nb_cf_json_local.results | default([]) | first | default({})).custom_fields | default({}) }}" + changed_when: false + + rescue: + - name: NB baseline | Baseline parse failed (disable delta mode) + set_fact: + nb_baseline_ok: false + nb_device_id: "" + nb_custom_fields_current: {} + changed_when: false + + - name: NB baseline | Abort delta mode if baseline missing or unusable + delegate_to: localhost + set_fact: + nb_baseline_ok: false + nb_custom_fields_current: {} + nb_device_id: "" + when: nb_device_id | length == 0 + changed_when: false + + - name: NB baseline | Debug baseline ok and a sample key + delegate_to: localhost + debug: + msg: + - "nb_baseline_ok={{ nb_baseline_ok }}" + - "nb_device_id={{ nb_device_id }}" + - "baseline fw_version={{ nb_custom_fields_current.get('fw_version','') }}" + changed_when: false + # ----------------------- Scan status tracking (journal finalization) ----------------------- + - name: Init scan status tracking + delegate_to: localhost + set_fact: + scan_ok: [] + scan_fail: [] + changed_when: false + + + # ----------------------- Delta tracking (skip unchanged custom fields) ----------------------- + - name: Delta tracking | Init diff flag + delegate_to: localhost + set_fact: + non_ts_diff_found: false + changed_when: false + + # ----------------------- Temp IP on DEV1 ----------------------- + - name: Add temporary IP on DEV1 + raw: "{{ pathprefix }} ip a add {{ dev2_side_ip }} dev {{ dev1_iface }}" + register: add_ip + failed_when: false + ignore_errors: true + + - name: Add temporary IP on DEV1 (tolerate 'File exists') + raw: > + {{ pathprefix }} + ip a add {{ dev2_side_ip }} dev {{ dev1_iface }} + register: add_ip + changed_when: add_ip.rc == 0 + failed_when: > + add_ip.rc != 0 + and ('File exists' not in (add_ip.stdout|default(''))) + and ('File exists' not in (add_ip.stderr|default(''))) + + - name: Debug temp IP result + debug: + msg: + - "add_ip.rc={{ add_ip.rc|default('') }}" + - "add_ip.out={{ add_ip.stdout|default('')|trim }}" + - "add_ip.err={{ add_ip.stderr|default('')|trim }}" + + - name: Discover DEV2 MAC via bridge fdb + raw: > + {{ pathprefix }} + bridge fdb show {{ dev1_iface }} | grep eth0 | grep -v permanent | grep master | awk '{print $1}' | head -n1 + register: dev2_mac_scan + changed_when: false + failed_when: false + + - name: Capture discovered MAC + set_fact: + dev2_mac: "{{ dev2_mac_scan.stdout|default('')|trim }}" + + - name: Clear existing ARP entry + raw: > + {{ pathprefix }} + ip neigh del {{ dev2_host }} dev {{ dev1_iface }} 2>/dev/null || true + changed_when: false + failed_when: false + + - name: Add static ARP if MAC found + when: dev2_mac|length > 0 + raw: > + {{ pathprefix }} + ip neigh add {{ dev2_host }} lladdr {{ dev2_mac }} dev {{ dev1_iface }} nud permanent + register: dev2_arp_add + changed_when: dev2_arp_add.rc == 0 + failed_when: > + dev2_arp_add.rc != 0 + and ('File exists' not in (dev2_arp_add.stdout|default(''))) + and ('File exists' not in (dev2_arp_add.stderr|default(''))) + + - name: Debug ARP summary + debug: + msg: + - "dev2_mac={{ dev2_mac|default('unset') }}" + - "arp_add.rc={{ dev2_arp_add.rc|default('') }}" + - "arp_add.out={{ dev2_arp_add.stdout|default('')|trim }}" + - "arp_add.err={{ dev2_arp_add.stderr|default('')|trim }}" + + - name: Note skipping static ARP + when: dev2_mac|length == 0 + debug: + msg: "No MAC discovered; skipping static ARP add." + + - name: Refresh ARP + raw: "{{ pathprefix }} arping -U -I {{ arping_iface }} {{ dev2_side_ip_addr }} -c 3" + failed_when: false + ignore_errors: true + + # ----------------------- Tunnel setup ----------------------- + - name: Pick free local port + delegate_to: localhost + shell: | + for i in $(seq 1 50); do + p="$(shuf -i 20000-39999 -n 1)" + ss -ltn | awk '{print $4}' | grep -qE "(:|\.)${p}$" || { echo $p; exit 0; } + done + exit 1 + register: pick_port + failed_when: false + + - name: Create control dir + delegate_to: localhost + shell: "mktemp -d" + register: mktemp_dir + failed_when: false + + - name: Set tunnel facts + delegate_to: localhost + set_fact: + _local_port: "{{ pick_port.stdout|default('')|trim }}" + _ctrl_dir: "{{ mktemp_dir.stdout|default('')|trim }}" + _ctrl_sock: "{{ mktemp_dir.stdout|default('')|trim }}/ssh_tunnel_ctl" + + - name: Start SSH tunnel + delegate_to: localhost + shell: | + set -e + USER="{{ dev1_user }}" + HOST="{{ ansible_host|default(inventory_hostname) }}" + sshpass -p '{{ dev1_pass }}' ssh -f -N {{ ssh_opts_common }} \ + -M -S "{{ _ctrl_sock }}" \ + -L "127.0.0.1:{{ _local_port }}:{{ dev2_host }}:{{ dev2_port }}" \ + "${USER}@${HOST}" + args: { executable: /bin/bash } + failed_when: false + ignore_errors: true + + - name: Probe tunnel + delegate_to: localhost + shell: "nc -z -w5 127.0.0.1 {{ _local_port }}" + register: nc_probe + failed_when: false + ignore_errors: true + + - name: Debug nc_probe + delegate_to: localhost + debug: + msg: + - "nc_probe.rc={{ nc_probe.rc | default('n/a') }}" + - "nc_probe.stdout={{ nc_probe.stdout | default('') | trim }}" + - "nc_probe.stderr={{ nc_probe.stderr | default('') | trim }}" + + + - name: Set tunnel_ok + delegate_to: localhost + set_fact: + tunnel_ok: "{{ nc_probe.rc == 0 }}" + + # ----------------------- Dev1 banner ----------------------- + - name: Dev1 | Probe banner + delegate_to: localhost + shell: | + sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ + "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ + "cat /etc/banner | grep -i rev | head -n1" + register: dev1_banner + failed_when: false + ignore_errors: true + + - name: Dev1 | Normalize banner + delegate_to: localhost + shell: | + printf '%s\n' "{{ dev1_banner.stdout|trim }}" \ + | awk -F '|' '{print $1}' \ + | sed -E 's/[[:space:]]+rev[[:space:]]+/-r/' \ + | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+-r[0-9]+' || true + register: dev1_fw_clean_cmd + changed_when: false + + - name: Dev1 | Set final fw string + delegate_to: localhost + set_fact: + dev1_fw_clean: "{{ dev1_fw_clean_cmd.stdout|trim }}" + + # ----------------------- Scan tracking: Dev1 fw_version ----------------------- + - name: Scan tracking | Dev1 fw_version outcome + delegate_to: localhost + set_fact: + scan_ok: "{{ scan_ok + (['fw_version'] if ((dev1_fw_clean | default('') | length) > 0) else []) }}" + scan_fail: "{{ scan_fail + (['fw_version=unavailable'] if ((dev1_fw_clean | default('') | length) == 0) else []) }}" + changed_when: false + + - name: Publish Dev1 fw_version + when: + - (not (nb_baseline_ok | default(false))) or (((dev1_fw_clean if (dev1_fw_clean|length>0) else 'unavailable')) != (nb_custom_fields_current.get('fw_version'))) + delegate_to: localhost + uri: + url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost|urlencode }}/{{ rmq_exchange|urlencode }}/publish" + method: POST + user: "{{ rmq_user }}" + password: "{{ rmq_pass }}" + force_basic_auth: true + status_code: 200 + headers: { content-type: "application/json" } + body_format: json + body: + properties: { content_type: "application/json" } + routing_key: "{{ control_queue }}" + payload: "{{ { + 'inscope_device': (ansible_hostname|default(inventory_hostname)), + 'task_name': 'custom_field_set', + 'task_add1': 'fw_version', + 'task_result': (dev1_fw_clean if (dev1_fw_clean|length>0) else 'unavailable') + } | to_json }}" + payload_encoding: "string" + changed_when: false + + + - name: Delta tracking | Mark changed fw_version + when: + - (not (nb_baseline_ok | default(false))) or (((dev1_fw_clean if (dev1_fw_clean|length>0) else 'unavailable')) != (nb_custom_fields_current.get('fw_version'))) + delegate_to: localhost + set_fact: + non_ts_diff_found: true + changed_when: false + + # ----------------------- Dev1 bootbanks backup firmux → fw_version_backup ----------------------- + - name: Dev1 | Read bootbanks backup firmux via jq + delegate_to: localhost + shell: | + sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ + "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ + "jq -r '.backup.firmux // empty' /var/run/bootbanks.json 2>/dev/null || true" + register: dev1_fw_backup_raw + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev1 | Normalize fw_version_backup value + delegate_to: localhost + set_fact: + fw_version_backup_value: >- + {{ + (dev1_fw_backup_raw.stdout | default('') | trim) + if ((dev1_fw_backup_raw.stdout | default('') | trim | length) > 0) + else 'unavailable' + }} + changed_when: false + + - name: Dev1 | fw_version_backup raw debug + delegate_to: localhost + debug: + msg: + - "dev1_fw_backup_raw.rc={{ dev1_fw_backup_raw.rc | default('n/a') }}" + - "dev1_fw_backup_raw.stdout={{ dev1_fw_backup_raw.stdout | default('') | trim }}" + - "dev1_fw_backup_raw.stderr={{ dev1_fw_backup_raw.stderr | default('') | trim }}" + + - name: Dev1 | fw_version_backup final debug + delegate_to: localhost + debug: + msg: + - "fw_version_backup_value={{ fw_version_backup_value | default('unset') }}" + + - name: Scan tracking | Dev1 fw_version_backup outcome + delegate_to: localhost + set_fact: + scan_ok: "{{ scan_ok + (['fw_version_backup'] if ((fw_version_backup_value | default('unavailable')) != 'unavailable') else []) }}" + scan_fail: "{{ scan_fail + (['fw_version_backup=unavailable'] if ((fw_version_backup_value | default('unavailable')) == 'unavailable') else []) }}" + changed_when: false + + - name: Publish Dev1 fw_version_backup + when: + - (not (nb_baseline_ok | default(false))) or ((fw_version_backup_value) != (nb_custom_fields_current.get('fw_version_backup'))) + delegate_to: localhost + uri: + url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost|urlencode }}/{{ rmq_exchange|urlencode }}/publish" + method: POST + user: "{{ rmq_user }}" + password: "{{ rmq_pass }}" + force_basic_auth: true + status_code: 200 + headers: { content-type: "application/json" } + body_format: json + body: + properties: { content_type: "application/json" } + routing_key: "{{ control_queue }}" + payload: "{{ { + 'inscope_device': (ansible_hostname|default(inventory_hostname)), + 'task_name': 'custom_field_set', + 'task_add1': 'fw_version_backup', + 'task_result': fw_version_backup_value + } | to_json }}" + payload_encoding: "string" + changed_when: false + + + + - name: Delta tracking | Mark changed fw_version_backup + when: + - (not (nb_baseline_ok | default(false))) or ((fw_version_backup_value) != (nb_custom_fields_current.get('fw_version_backup'))) + delegate_to: localhost + set_fact: + non_ts_diff_found: true + changed_when: false + + # ----------------------- Dev1 SSID count (jq-based, schema-aware) ----------------------- + - name: Dev1 | SSID profiles summary via jq (AP radios only) + delegate_to: localhost + shell: | + sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ + "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ + "jq -r ' + [ + .wireless.radios + | to_entries[] + | select(.value.mode == \"ap\") + | (.value.vaps // [])[ ] + | select(has(\"ssid\")) + | {ssid:.ssid, enabled:(.enabled // false)} + ] + | unique_by(.ssid, .enabled) + | { + e: (map(select(.enabled==true)) | length), + d: (map(select(.enabled==false)) | length) + } + | \"\\(.e + .d) (\\(.e)e \\(.d)d)\" + ' /tmp/config.json 2>/dev/null || echo '0 (0e 0d)'" + register: ssid_profiles_summary_raw + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev1 | Normalize SSID profiles summary + delegate_to: localhost + set_fact: + multissidfix_value: "{{ (ssid_profiles_summary_raw.stdout | default('0 (0e 0d)')) | trim }}" + + # ----------------------- Scan tracking: Dev1 multissidfix ----------------------- + - name: Scan tracking | Dev1 multissidfix outcome + delegate_to: localhost + set_fact: + scan_ok: "{{ scan_ok + ['multissidfix'] }}" + changed_when: false + + - name: Dev1 | multissidfix debug + delegate_to: localhost + when: multissidfix_value is defined + debug: + msg: "multissidfix_value={{ multissidfix_value }}" + + - name: Publish Dev1 multissidfix custom field + when: + - (not (nb_baseline_ok | default(false))) or ((multissidfix_value) != (nb_custom_fields_current.get('multissidfix'))) + delegate_to: localhost + uri: + url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost|urlencode }}/{{ rmq_exchange|urlencode }}/publish" + method: POST + user: "{{ rmq_user }}" + password: "{{ rmq_pass }}" + force_basic_auth: true + status_code: 200 + headers: { content-type: "application/json" } + body_format: json + body: + properties: { content_type: "application/json" } + routing_key: "{{ control_queue }}" + payload: "{{ { + 'inscope_device': (ansible_hostname|default(inventory_hostname)), + 'task_name': 'custom_field_set', + 'task_add1': 'multissidfix', + 'task_result': multissidfix_value + } | to_json }}" + payload_encoding: "string" + changed_when: false + + + - name: Delta tracking | Mark changed multissidfix + when: + - (not (nb_baseline_ok | default(false))) or ((multissidfix_value) != (nb_custom_fields_current.get('multissidfix'))) + delegate_to: localhost + set_fact: + non_ts_diff_found: true + changed_when: false + + # ----------------------- Dev1 scroll24.sh check & publish ----------------------- + - name: Dev1 | Check scroll24.sh presence + delegate_to: localhost + shell: | + sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ + "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ + "test -f /root/scroll24.sh && echo present || echo missing" + register: scroll24_present_raw + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev1 | Set scroll24 presence fact + delegate_to: localhost + set_fact: + scroll24_present: "{{ (scroll24_present_raw.stdout | default('') | trim) == 'present' }}" + + - name: Dev1 | Read scroll24.sh version line (may be empty -> v1) + when: scroll24_present | default(false) + delegate_to: localhost + shell: | + sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ + "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ + "grep -i 'scroll24_version' /root/scroll24.sh 2>/dev/null | head -n1 || true" + register: scroll24_ver_line + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev1 | Normalize scroll24 version + when: scroll24_present | default(false) + delegate_to: localhost + set_fact: + scroll24_ver_clean: >- + {{ + ( + scroll24_ver_line.stdout | default('') | trim + ) + | regex_replace('^.*=', '') + | trim + if (scroll24_ver_line is defined + and (scroll24_ver_line.stdout | default('') | trim | length) > 0) + else 'v1' + }} + + - name: Dev1 | Read scroll24 crontab entry (first match) + delegate_to: localhost + shell: | + sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ + "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ + "crontab -l 2>/dev/null | grep 'scroll24.sh' | head -n1 || true" + register: scroll24_cron_raw + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev1 | Normalize scroll24 cron line + delegate_to: localhost + set_fact: + scroll24_cron_line: "{{ scroll24_cron_raw.stdout | default('') | trim }}" + + - name: Dev1 | Derive scroll24 schedule summary + delegate_to: localhost + shell: | + line="{{ scroll24_cron_line | default('') }}" + + # No crontab hit or no scroll24.sh line + if [ -z "$line" ]; then + echo nocron + exit 0 + fi + + # Extract first 5 cron fields + min="$(echo "$line" | awk '{print $1}')" + hour="$(echo "$line" | awk '{print $2}')" + + # Pattern 1: */N * * * * -> every N minutes + case "$min" in + '*/'*) + n="${min#*/}" + if [ -n "$n" ]; then + echo "${n}m" + exit 0 + fi + ;; + esac + + # Pattern 2: M */H * * * -> every H hours (we ignore the minute) + case "$hour" in + '*/'*) + h="${hour#*/}" + if [ -n "$h" ]; then + echo "${h}h" + exit 0 + fi + ;; + esac + + # Fallback + echo custom + args: + executable: /bin/bash + register: scroll24_sched_raw + changed_when: false + failed_when: false + + - name: Dev1 | Set scroll24 schedule fact + delegate_to: localhost + set_fact: + scroll24_schedule: "{{ scroll24_sched_raw.stdout | default('') | trim }}" + + - name: Dev1 | Compute scroll24 value + delegate_to: localhost + set_fact: + scroll24_value: >- + {%- if not (scroll24_present | default(false)) -%} + missing + {%- elif scroll24_schedule in ['nocron', ''] -%} + {{ scroll24_ver_clean | default('v1') }}_nocron + {%- elif scroll24_schedule == 'custom' -%} + {{ scroll24_ver_clean | default('v1') }}_custom + {%- else -%} + {{ scroll24_ver_clean | default('v1') }}_{{ scroll24_schedule }} + {%- endif -%} + + # ----------------------- Scan tracking: Dev1 scroll24 ----------------------- + - name: Scan tracking | Dev1 scroll24 outcome + delegate_to: localhost + set_fact: + scan_ok: "{{ scan_ok + ['scroll24'] }}" + changed_when: false + + - name: Dev1 | scroll24 debug + delegate_to: localhost + debug: + msg: + - "scroll24_present={{ scroll24_present | default(false) }}" + - "scroll24_ver_clean={{ scroll24_ver_clean | default('n/a') }}" + - "scroll24_cron_line={{ scroll24_cron_line | default('') }}" + - "scroll24_schedule={{ scroll24_schedule | default('n/a') }}" + - "scroll24_value={{ scroll24_value | default('n/a') }}" + + - name: Publish Dev1 scroll24 custom field + when: + - (not (nb_baseline_ok | default(false))) or ((scroll24_value) != (nb_custom_fields_current.get('scroll24'))) + delegate_to: localhost + uri: + url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost|urlencode }}/{{ rmq_exchange|urlencode }}/publish" + method: POST + user: "{{ rmq_user }}" + password: "{{ rmq_pass }}" + force_basic_auth: true + status_code: 200 + headers: { content-type: "application/json" } + body_format: json + body: + properties: { content_type: "application/json" } + routing_key: "{{ control_queue }}" + payload: "{{ { + 'inscope_device': (ansible_hostname|default(inventory_hostname)), + 'task_name': 'custom_field_set', + 'task_add1': 'scroll24', + 'task_result': scroll24_value + } | to_json }}" + payload_encoding: "string" + changed_when: false + + + - name: Delta tracking | Mark changed scroll24 + when: + - (not (nb_baseline_ok | default(false))) or ((scroll24_value) != (nb_custom_fields_current.get('scroll24'))) + delegate_to: localhost + set_fact: + non_ts_diff_found: true + changed_when: false + + # ----------------------- Dev1 wifidebug.sh check & publish ----------------------- + - name: Dev1 | Check wifidebug.sh presence + delegate_to: localhost + shell: | + sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ + "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ + "test -f /root/wifidebug.sh && echo present || echo missing" + register: wd_present + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev1 | Set presence fact + delegate_to: localhost + set_fact: + wifidebug_present: "{{ (wd_present.stdout | default('') | trim) == 'present' }}" + + - name: Dev1 | Read wifidebug.sh version (SCRIPTVERSION) + when: wifidebug_present | default(false) + delegate_to: localhost + shell: | + sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ + "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ + "grep -m1 'SCRIPTVERSION' /root/wifidebug.sh 2>/dev/null | awk -F'=' '{print \$2}' | tr -d '[:space:]' | sed -E 's/^v?([0-9].*)$/\1/'" + register: wd_version_raw + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev1 | Normalize wifidebug version + delegate_to: localhost + set_fact: + wd_version_clean: "{{ wd_version_raw.stdout | default('') | trim }}" + + - name: Dev1 | Compute wifidebug value + delegate_to: localhost + set_fact: + wifidebug_value: >- + {%- if not (wifidebug_present | default(false)) -%}missing + {%- elif (wd_version_clean | default('') | length) > 0 -%}v{{ wd_version_clean }} + {%- else -%}n/a{%- endif -%} + + # ----------------------- Dev1 connstats.sh check & publish ----------------------- + - name: Dev1 | Check connstats.sh presence + delegate_to: localhost + shell: | + sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ + "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ + "test -f /root/connstats.sh && echo present || echo missing" + register: cs_present + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev1 | Set connstats presence fact + delegate_to: localhost + set_fact: + connstats_present: "{{ (cs_present.stdout | default('') | trim) == 'present' }}" + + - name: Dev1 | Read connstats.sh version (SCRIPTVERSION) + when: connstats_present | default(false) + delegate_to: localhost + shell: | + sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ + "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ + "grep -m1 'SCRIPTVERSION' /root/connstats.sh 2>/dev/null | awk -F'=' '{print \$2}' | tr -d '[:space:]' | sed -E 's/^v?([0-9].*)$/\1/'" + register: cs_version_raw + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev1 | Normalize connstats version + delegate_to: localhost + set_fact: + cs_version_clean: "{{ cs_version_raw.stdout | default('') | trim }}" + + - name: Dev1 | Compute connstats value + delegate_to: localhost + set_fact: + connstats_value: >- + {%- if not (connstats_present | default(false)) -%}missing + {%- elif (cs_version_clean | default('') | length) > 0 -%}v{{ cs_version_clean }} + {%- else -%}n/a{%- endif -%} + + # ----------------------- Scan tracking: Dev1 connstats ----------------------- + - name: Scan tracking | Dev1 connstats outcome + delegate_to: localhost + set_fact: + scan_ok: "{{ scan_ok + ['connstats'] }}" + changed_when: false + + - name: Publish Dev1 connstats custom field + when: + - (not (nb_baseline_ok | default(false))) or ((connstats_value) != (nb_custom_fields_current.get('connstats'))) + delegate_to: localhost + uri: + url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost|urlencode }}/{{ rmq_exchange|urlencode }}/publish" + method: POST + user: "{{ rmq_user }}" + password: "{{ rmq_pass }}" + force_basic_auth: true + status_code: 200 + headers: { content-type: "application/json" } + body_format: json + body: + properties: { content_type: "application/json" } + routing_key: "{{ control_queue }}" + payload: "{{ { + 'inscope_device': (ansible_hostname|default(inventory_hostname)), + 'task_name': 'custom_field_set', + 'task_add1': 'connstats', + 'task_result': connstats_value + } | to_json }}" + payload_encoding: "string" + changed_when: false + + + - name: Delta tracking | Mark changed connstats + when: + - (not (nb_baseline_ok | default(false))) or ((connstats_value) != (nb_custom_fields_current.get('connstats'))) + delegate_to: localhost + set_fact: + non_ts_diff_found: true + changed_when: false + + # ----------------------- Scan tracking: Dev1 wifidebug ----------------------- + - name: Scan tracking | Dev1 wifidebug outcome + delegate_to: localhost + set_fact: + scan_ok: "{{ scan_ok + ['wifidebug'] }}" + changed_when: false + + - name: Publish Dev1 wifidebug custom field + when: + - (not (nb_baseline_ok | default(false))) or ((wifidebug_value) != (nb_custom_fields_current.get('wifidebug'))) + delegate_to: localhost + uri: + url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost|urlencode }}/{{ rmq_exchange|urlencode }}/publish" + method: POST + user: "{{ rmq_user }}" + password: "{{ rmq_pass }}" + force_basic_auth: true + status_code: 200 + headers: { content-type: "application/json" } + body_format: json + body: + properties: { content_type: "application/json" } + routing_key: "{{ control_queue }}" + payload: "{{ { + 'inscope_device': (ansible_hostname|default(inventory_hostname)), + 'task_name': 'custom_field_set', + 'task_add1': 'wifidebug', + 'task_result': wifidebug_value + } | to_json }}" + payload_encoding: "string" + changed_when: false + + + - name: Delta tracking | Mark changed wifidebug + when: + - (not (nb_baseline_ok | default(false))) or ((wifidebug_value) != (nb_custom_fields_current.get('wifidebug'))) + delegate_to: localhost + set_fact: + non_ts_diff_found: true + changed_when: false + + # ----------------------- Dev1 fw_printenv size → cpe_bootenv ----------------------- + - name: Dev1 | Read fw_printenv size (line count) + delegate_to: localhost + shell: | + sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ + "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ + "fw_printenv 2>/dev/null | wc -l" + args: + executable: /bin/bash + register: dev1_bootenv_wc + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev1 | Normalize cpe_bootenv value + delegate_to: localhost + set_fact: + cpe_bootenv_value: >- + {{ + 'unavailable' + if ( + (dev1_bootenv_wc.rc | default(1)) != 0 or + (dev1_bootenv_wc.stdout | default('') | trim) in ['', '0'] + ) + else (dev1_bootenv_wc.stdout | default('') | trim) + }} + + # ----------------------- Scan tracking: Dev1 cpe_bootenv ----------------------- + - name: Scan tracking | Dev1 cpe_bootenv outcome + delegate_to: localhost + set_fact: + scan_ok: "{{ scan_ok + (['cpe_bootenv'] if ((cpe_bootenv_value | default('unavailable')) != 'unavailable') else []) }}" + scan_fail: "{{ scan_fail + (['cpe_bootenv=unavailable'] if ((cpe_bootenv_value | default('unavailable')) == 'unavailable') else []) }}" + changed_when: false + + - name: Publish Dev1 cpe_bootenv custom field + when: + - (not (nb_baseline_ok | default(false))) or ((cpe_bootenv_value) != (nb_custom_fields_current.get('cpe_bootenv'))) + delegate_to: localhost + uri: + url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost|urlencode }}/{{ rmq_exchange|urlencode }}/publish" + method: POST + user: "{{ rmq_user }}" + password: "{{ rmq_pass }}" + force_basic_auth: true + status_code: 200 + headers: { content-type: "application/json" } + body_format: json + body: + properties: { content_type: "application/json" } + routing_key: "{{ control_queue }}" + payload: "{{ { + 'inscope_device': (ansible_hostname|default(inventory_hostname)), + 'task_name': 'custom_field_set', + 'task_add1': 'cpe_bootenv', + 'task_result': cpe_bootenv_value + } | to_json }}" + payload_encoding: "string" + changed_when: false + + + - name: Delta tracking | Mark changed cpe_bootenv + when: + - (not (nb_baseline_ok | default(false))) or ((cpe_bootenv_value) != (nb_custom_fields_current.get('cpe_bootenv'))) + delegate_to: localhost + set_fact: + non_ts_diff_found: true + changed_when: false + + # ----------------------- Dev1 hostname (for Dev2 mismatch check) ----------------------- + - name: Dev1 | Read kernel hostname + delegate_to: localhost + shell: | + sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ + "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ + "cat /proc/sys/kernel/hostname 2>/dev/null || hostname 2>/dev/null || echo ''" + args: + executable: /bin/bash + register: dev1_hostname_raw + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev1 | Normalize hostname + delegate_to: localhost + set_fact: + dev1_hostname: >- + {{ + (dev1_hostname_raw.stdout | default('') | trim) + if ((dev1_hostname_raw.stdout | default('') | trim | length) > 0) + else (inventory_hostname | default('')) + }} + + # ----------------------- Dev2 auth + firmux ----------------------- + - name: Try DEV2 login + when: tunnel_ok|default(false) + delegate_to: localhost + shell: | + PORT="{{ _local_port }}" + # First try: basicpass + if timeout {{ ssh_timeout }}s sshpass -f basicpass ssh {{ ssh_opts_common }} \ + -o ConnectionAttempts=1 -o ConnectTimeout={{ ssh_timeout }} \ + -p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" "echo ok" >/dev/null 2>&1; then + echo basicpass + exit 0 + fi + + # Second try: basicpass2 + if timeout {{ ssh_timeout }}s sshpass -f basicpass2 ssh {{ ssh_opts_common }} \ + -o ConnectionAttempts=1 -o ConnectTimeout={{ ssh_timeout }} \ + -p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" "echo ok" >/dev/null 2>&1; then + echo basicpass2 + exit 0 + fi + + # Both failed + echo NONE + exit 1 + args: + executable: /bin/bash + register: dev2_auth + failed_when: false + ignore_errors: true + + - name: Set dev2_passfile_used + delegate_to: localhost + set_fact: + dev2_passfile_used: >- + {{ + ( + (dev2_auth is defined) + and ((dev2_auth.rc | default(1)) == 0) + ) + | ternary((dev2_auth.stdout | default('') | trim), 'NONE') + }} + + - name: Dev2 | Auth debug + delegate_to: localhost + debug: + msg: + - "dev2_auth.rc={{ dev2_auth.rc | default('n/a') }}" + - "dev2_auth.stdout={{ dev2_auth.stdout | default('') | trim }}" + - "dev2_passfile_used={{ dev2_passfile_used }}" + + - name: Dev2 | Read firmux (tunnel) + when: + - tunnel_ok | default(false) + - dev2_passfile_used != 'NONE' + delegate_to: localhost + shell: | + PORT="{{ _local_port }}" + sshpass -f "{{ dev2_passfile_used }}" ssh {{ ssh_opts_common }} \ + -o ConnectTimeout={{ ssh_timeout }} \ + -p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \ + "cat /usr/lib/release/firmux 2>/dev/null || true" + args: + executable: /bin/bash + register: dev2_firmux + changed_when: false + failed_when: false + ignore_errors: true + + # ------ LLDP candidate IP when Dev2 login failed (tunnel up but Dev2 unreachable) ------ + - name: Dev1 | Try LLDP-derived Dev2 IP (auth to Dev2 failed) + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + delegate_to: localhost + shell: | + DIGITS="{{ inventory_hostname | regex_replace('[^0-9]', '') }}" + sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ + "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ + "cat /var/run/lldp_server.json 2>/dev/null | grep ${DIGITS} -A 10 | grep address | grep -vE 'subtype|ipv6' | awk -F'\"' '{ print \$4 }' | head -n1" + register: lldp_ip_raw + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev1 | Show LLDP candidate IP (Dev2, after SSH auth failure) + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + delegate_to: localhost + debug: + msg: + - >- + LLDP-derived Dev2 IP (raw best-effort): + {{ + (lldp_ip_raw.stdout | default('') | trim) + if ((lldp_ip_raw.stdout | default('') | trim | length) > 0) + else '' + }} + - >- + LLDP 10.0.0.0/8 usable: + {{ + ((lldp_ip_raw.stdout | default('') | trim) is match('^10\.')) + | ternary('yes (10/8)', 'no (not 10/8) or none') + }} + + - name: Set LLDP IP fact (10.0.0.0/8 only) + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + delegate_to: localhost + set_fact: + lldp_dev2_ip_raw: "{{ lldp_ip_raw.stdout | default('') | trim }}" + lldp_dev2_ip: >- + {{ + (lldp_ip_raw.stdout | default('') | trim) + if ((lldp_ip_raw.stdout | default('') | trim) is match('^10\.')) + else '' + }} + + - name: Try DEV2 login via direct LLDP IP + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (lldp_dev2_ip | default('') | length) > 0 + delegate_to: localhost + shell: | + for f in basicpass basicpass2; do + if timeout 20s sshpass -f "$f" ssh {{ ssh_opts_common }} \ + -o ConnectionAttempts=1 -o ConnectTimeout=10 \ + "{{ dev2_ssh_user }}@{{ lldp_dev2_ip }}" "echo ok" >/dev/null 2>&1; then + echo "$f"; exit 0 + fi + done + exit 1 + register: dev2_auth_lldp + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev2 | LLDP path selection debug + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (lldp_dev2_ip | default('') | length) > 0 + delegate_to: localhost + debug: + msg: + - "LLDP candidate IP={{ lldp_dev2_ip }}" + - "LLDP auth rc={{ dev2_auth_lldp.rc | default('n/a') }}" + - "passfile (if any) will follow in next step" + + - name: Set dev2_passfile_used_lldp + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (lldp_dev2_ip | default('') | length) > 0 + delegate_to: localhost + set_fact: + dev2_passfile_used_lldp: >- + {{ + ((dev2_auth_lldp.rc | default(1)) == 0) + | ternary(dev2_auth_lldp.stdout | default('') | trim, 'NONE') + }} + + - name: Dev2 | LLDP passfile decision + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (lldp_dev2_ip | default('') | length) > 0 + delegate_to: localhost + debug: + msg: + - "Using passfile for LLDP path: {{ dev2_passfile_used_lldp }}" + + - name: Dev2 | Check TCP/22 reachability (LLDP) + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (lldp_dev2_ip | default('') | length) > 0 + delegate_to: localhost + shell: "nc -z -w3 {{ lldp_dev2_ip }} 22" + register: lldp_ssh_tcp + changed_when: false + failed_when: false + ignore_errors: true + + # ------ LLDP IPv6 fallback (Dev1 → Dev2 on link-local) ------ + - name: Dev1 | Try LLDP-derived Dev2 IPv6 (auth to Dev2 failed, IPv4 LLDP unusable) + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (dev2_passfile_used_lldp is not defined or dev2_passfile_used_lldp == 'NONE') + delegate_to: localhost + shell: | + DIGITS="{{ inventory_hostname | regex_replace('[^0-9]', '') }}" + sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ + "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ + "cat /var/run/lldp_server.json 2>/dev/null | grep ${DIGITS} -A 15 | grep 'address_ipv6' | awk -F'\\\"' '{ print \\$4 }' | head -n1" + args: + executable: /bin/bash + register: lldp_ip6_raw + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev1 | Set LLDP IPv6 fact + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (dev2_passfile_used_lldp is not defined or dev2_passfile_used_lldp == 'NONE') + delegate_to: localhost + set_fact: + lldp_dev2_ip6: "{{ lldp_ip6_raw.stdout | default('') | trim }}" + + - name: Dev1 | Show LLDP IPv6 candidate + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (dev2_passfile_used_lldp is not defined or dev2_passfile_used_lldp == 'NONE') + delegate_to: localhost + debug: + msg: + - "LLDP-derived Dev2 IPv6 (best-effort): {{ lldp_dev2_ip6 | default('') }}" + + - name: Dev1 | Copy Dev2 passfiles for IPv6 LLDP use + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (dev2_passfile_used_lldp is not defined or dev2_passfile_used_lldp == 'NONE') + - lldp_dev2_ip6 is defined + - lldp_dev2_ip6 | length > 0 + copy: + src: "{{ item }}" + dest: "/tmp/{{ item }}" + mode: "0600" + loop: "{{ dev2_passfiles }}" + ignore_errors: true + + - name: Dev1 | Try DEV2 login via LLDP IPv6 (nested SSH via Dev1) + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (dev2_passfile_used_lldp is not defined or dev2_passfile_used_lldp == 'NONE') + - lldp_dev2_ip6 is defined + - lldp_dev2_ip6 | length > 0 + delegate_to: localhost + shell: | + IP6="{{ lldp_dev2_ip6 }}" + if [ -z "$IP6" ]; then + exit 1 + fi + + for f in {{ dev2_passfiles | join(' ') }}; do + if sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ + "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ + "timeout {{ ssh_timeout }}s sshpass -f '/tmp/${f}' ssh \ + -o StrictHostKeyChecking=no -o PubkeyAuthentication=no \ + -o ConnectTimeout={{ ssh_timeout }} \ + '{{ dev2_ssh_user }}@\[${IP6}%{{ dev1_iface }}\]' 'echo ok' " \ + >/dev/null 2>&1; then + echo "$f" + exit 0 + fi + done + exit 1 + args: + executable: /bin/bash + register: dev2_auth_lldp6 + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev2 | LLDP IPv6 path debug + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (dev2_passfile_used_lldp is not defined or dev2_passfile_used_lldp == 'NONE') + - lldp_dev2_ip6 is defined + - lldp_dev2_ip6 | length > 0 + delegate_to: localhost + debug: + msg: + - "LLDP IPv6 candidate={{ lldp_dev2_ip6 | default('') }}" + - "LLDP IPv6 auth rc={{ dev2_auth_lldp6.rc | default('n/a') }}" + + - name: Set dev2_passfile_used_lldp6 + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (dev2_passfile_used_lldp is not defined or dev2_passfile_used_lldp == 'NONE') + - lldp_dev2_ip6 is defined + - lldp_dev2_ip6 | length > 0 + delegate_to: localhost + set_fact: + dev2_passfile_used_lldp6: >- + {{ + ((dev2_auth_lldp6.rc | default(1)) == 0) + | ternary(dev2_auth_lldp6.stdout | default('') | trim, 'NONE') + }} + + - name: Dev2 | IPv6 LLDP passfile decision + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (dev2_passfile_used_lldp is not defined or dev2_passfile_used_lldp == 'NONE') + - lldp_dev2_ip6 is defined + - lldp_dev2_ip6 | length > 0 + delegate_to: localhost + debug: + msg: + - "Using passfile for LLDP IPv6 path: {{ dev2_passfile_used_lldp6 | default('NONE') }}" + + - name: Dev2 | Read firmux (LLDP IPv6 via Dev1) + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (dev2_passfile_used_lldp is not defined or dev2_passfile_used_lldp == 'NONE') + - lldp_dev2_ip6 is defined + - lldp_dev2_ip6 | length > 0 + - dev2_passfile_used_lldp6 is defined + - dev2_passfile_used_lldp6 != 'NONE' + delegate_to: localhost + shell: | + IP6="{{ lldp_dev2_ip6 }}" + F="{{ dev2_passfile_used_lldp6 }}" + if [ -z "$IP6" ] || [ -z "$F" ] || [ "$F" = "NONE" ]; then + exit 0 + fi + + sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ + "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ + "sshpass -f '/tmp/${F}' ssh \ + -o StrictHostKeyChecking=no -o PubkeyAuthentication=no \ + -o ConnectTimeout={{ ssh_timeout }} \ + '{{ dev2_ssh_user }}@\[${IP6}%{{ dev1_iface }}\]' \ + 'cat /usr/lib/release/firmux 2>/dev/null || true'" + args: + executable: /bin/bash + register: dev2_firmux_lldp6 + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev2 | Read firmux (direct LLDP IP) + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (lldp_dev2_ip | default('') | length) > 0 + - dev2_passfile_used_lldp != 'NONE' + delegate_to: localhost + shell: | + sshpass -f "{{ dev2_passfile_used_lldp }}" ssh \ + -o StrictHostKeyChecking=no -o PubkeyAuthentication=no \ + -o ConnectTimeout=15 \ + "{{ dev2_ssh_user }}@{{ lldp_dev2_ip }}" \ + "cat /usr/lib/release/firmux 2>/dev/null || true" + register: dev2_firmux_lldp + failed_when: false + ignore_errors: true + + - name: Dev2 | Choose firmux output (tunnel, IPv4 LLDP, then IPv6 LLDP) + delegate_to: localhost + set_fact: + dev2_firmux_text: >- + {{ + (dev2_firmux.stdout | default('') | trim) + if ((dev2_firmux.stdout | default('') | trim | length) > 0) + else ( + (dev2_firmux_lldp.stdout | default('') | trim) + if ( + (dev2_firmux_lldp is defined) and + ((dev2_firmux_lldp.stdout | default('') | trim | length) > 0) + ) + else (dev2_firmux_lldp6.stdout | default('') | trim) + ) + }} + + - name: Dev2 | firmux selection debug + delegate_to: localhost + debug: + msg: + - "dev2_firmux_text='{{ dev2_firmux_text | default('') | regex_replace('\n','\\n') }}'" + - "indoor_fw_norm='{{ indoor_fw_norm | default('') }}'" + + - name: Dev2 | Extract firmware version line + delegate_to: localhost + run_once: true + set_fact: + indoor_fw_norm: >- + {{ + ( + (dev2_firmux_text | default('') | regex_replace('\r',''))|split('\n') + | map('trim') | select('truthy') | list + ) | select('match','(?i).*\\brev\\s*[0-9]+.*') + | list | first | default( + ((dev2_firmux_text | default('') | regex_replace('\r','')) + | split('\n')|map('trim')|select('truthy')|list|first|default('')) + ) | trim + }} + + - name: Dev2 | firmux selection debug + delegate_to: localhost + debug: + msg: + - "dev2_firmux_text='{{ dev2_firmux_text | default('') | regex_replace('\n','\\n') }}'" + - "indoor_fw_norm='{{ indoor_fw_norm | default('') }}'" + + # ----------------------- Scan tracking: Dev2 indoor_fwver ----------------------- + - name: Scan tracking | Dev2 indoor_fwver outcome + delegate_to: localhost + set_fact: + scan_ok: "{{ scan_ok + (['indoor_fwver'] if ((indoor_fw_norm | default('') | length) > 0) else []) }}" + scan_fail: "{{ scan_fail + (['indoor_fwver=unavailable'] if ((indoor_fw_norm | default('') | length) == 0) else []) }}" + changed_when: false + + - name: Publish Dev2 indoor_fwver + when: + - (not (nb_baseline_ok | default(false))) or (((indoor_fw_norm if indoor_fw_norm|length>0 else 'unavailable')) != (nb_custom_fields_current.get('indoor_fwver'))) + delegate_to: localhost + uri: + url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost|urlencode }}/{{ rmq_exchange|urlencode }}/publish" + method: POST + user: "{{ rmq_user }}" + password: "{{ rmq_pass }}" + force_basic_auth: true + status_code: 200 + headers: { content-type: "application/json" } + body_format: json + body: + properties: { content_type: "application/json" } + routing_key: "{{ control_queue }}" + payload: "{{ { + 'inscope_device': (ansible_hostname|default(inventory_hostname)), + 'task_name': 'custom_field_set', + 'task_add1': 'indoor_fwver', + 'task_result': (indoor_fw_norm if indoor_fw_norm|length>0 else 'unavailable') + } | to_json }}" + payload_encoding: "string" + changed_when: false + + + + - name: Delta tracking | Mark changed indoor_fwver + when: + - (not (nb_baseline_ok | default(false))) or (((indoor_fw_norm if indoor_fw_norm|length>0 else 'unavailable')) != (nb_custom_fields_current.get('indoor_fwver'))) + delegate_to: localhost + set_fact: + non_ts_diff_found: true + changed_when: false + + # ----------------------- Dev2 bootbanks backup firmux → indoor_fwver_backup ----------------------- + - name: Dev2 | Read bootbanks backup firmux (tunnel) + when: + - tunnel_ok | default(false) + - dev2_passfile_used != 'NONE' + delegate_to: localhost + shell: | + PORT="{{ _local_port }}" + sshpass -f "{{ dev2_passfile_used }}" ssh {{ ssh_opts_common }} \ + -o ConnectTimeout={{ ssh_timeout }} \ + -p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \ + "jq -r '.backup.firmux // empty' /var/run/bootbanks.json 2>/dev/null || true" + args: + executable: /bin/bash + register: dev2_fwver_backup_tunnel + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev2 | Read bootbanks backup firmux (LLDP IPv4) + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (lldp_dev2_ip | default('') | length) > 0 + - dev2_passfile_used_lldp is defined + - dev2_passfile_used_lldp != 'NONE' + delegate_to: localhost + shell: | + sshpass -f "{{ dev2_passfile_used_lldp }}" ssh \ + -o StrictHostKeyChecking=no -o PubkeyAuthentication=no \ + -o ConnectTimeout=15 \ + "{{ dev2_ssh_user }}@{{ lldp_dev2_ip }}" \ + "jq -r '.backup.firmux // empty' /var/run/bootbanks.json 2>/dev/null || true" + args: + executable: /bin/bash + register: dev2_fwver_backup_lldp + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev2 | Read bootbanks backup firmux (LLDP IPv6 via Dev1) + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (dev2_passfile_used_lldp is not defined or dev2_passfile_used_lldp == 'NONE') + - lldp_dev2_ip6 is defined + - lldp_dev2_ip6 | length > 0 + - dev2_passfile_used_lldp6 is defined + - dev2_passfile_used_lldp6 != 'NONE' + delegate_to: localhost + shell: | + IP6="{{ lldp_dev2_ip6 }}" + F="{{ dev2_passfile_used_lldp6 }}" + if [ -z "$IP6" ] || [ -z "$F" ] || [ "$F" = "NONE" ]; then + exit 0 + fi + + sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ + "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ + "sshpass -f '/tmp/${F}' ssh \ + -o StrictHostKeyChecking=no -o PubkeyAuthentication=no \ + -o ConnectTimeout={{ ssh_timeout }} \ + '{{ dev2_ssh_user }}@\[${IP6}%{{ dev1_iface }}\]' \ + 'jq -r \".backup.firmux // empty\" /var/run/bootbanks.json 2>/dev/null || true'" + args: + executable: /bin/bash + register: dev2_fwver_backup_lldp6 + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev2 | Select bootbanks backup firmux output + delegate_to: localhost + set_fact: + indoor_fwver_backup_raw: >- + {{ + (dev2_fwver_backup_tunnel.stdout | default('') | trim) + if ( + (dev2_fwver_backup_tunnel is defined) and + ((dev2_fwver_backup_tunnel.stdout | default('') | trim | length) > 0) + ) + else ( + (dev2_fwver_backup_lldp.stdout | default('') | trim) + if ( + (dev2_fwver_backup_lldp is defined) and + ((dev2_fwver_backup_lldp.stdout | default('') | trim | length) > 0) + ) + else (dev2_fwver_backup_lldp6.stdout | default('') | trim) + ) + }} + changed_when: false + + - name: Dev2 | Normalize indoor_fwver_backup value + delegate_to: localhost + set_fact: + indoor_fwver_backup_value: >- + {{ + (indoor_fwver_backup_raw | default('') | trim) + if ((indoor_fwver_backup_raw | default('') | trim | length) > 0) + else 'unavailable' + }} + changed_when: false + + - name: Dev2 | indoor_fwver_backup raw debug + delegate_to: localhost + debug: + msg: + - "tunnel rc={{ dev2_fwver_backup_tunnel.rc | default('n/a') }} out={{ dev2_fwver_backup_tunnel.stdout | default('') | trim }} err={{ dev2_fwver_backup_tunnel.stderr | default('') | trim }}" + - "lldp4 rc={{ dev2_fwver_backup_lldp.rc | default('n/a') }} out={{ dev2_fwver_backup_lldp.stdout | default('') | trim }} err={{ dev2_fwver_backup_lldp.stderr | default('') | trim }}" + - "lldp6 rc={{ dev2_fwver_backup_lldp6.rc | default('n/a') }} out={{ dev2_fwver_backup_lldp6.stdout | default('') | trim }} err={{ dev2_fwver_backup_lldp6.stderr | default('') | trim }}" + + - name: Dev2 | indoor_fwver_backup final debug + delegate_to: localhost + debug: + msg: + - "indoor_fwver_backup_value={{ indoor_fwver_backup_value | default('unset') }}" + + # ----------------------- Scan tracking: Dev2 indoor_fwver_backup ----------------------- + - name: Scan tracking | Dev2 indoor_fwver_backup outcome + delegate_to: localhost + set_fact: + scan_ok: "{{ scan_ok + (['indoor_fwver_backup'] if ((indoor_fwver_backup_value | default('unavailable')) != 'unavailable') else []) }}" + scan_fail: "{{ scan_fail + (['indoor_fwver_backup=unavailable'] if ((indoor_fwver_backup_value | default('unavailable')) == 'unavailable') else []) }}" + changed_when: false + + - name: Publish Dev2 indoor_fwver_backup + when: + - (not (nb_baseline_ok | default(false))) or ((indoor_fwver_backup_value) != (nb_custom_fields_current.get('indoor_fwver_backup'))) + delegate_to: localhost + uri: + url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost|urlencode }}/{{ rmq_exchange|urlencode }}/publish" + method: POST + user: "{{ rmq_user }}" + password: "{{ rmq_pass }}" + force_basic_auth: true + status_code: 200 + headers: { content-type: "application/json" } + body_format: json + body: + properties: { content_type: "application/json" } + routing_key: "{{ control_queue }}" + payload: "{{ { + 'inscope_device': (ansible_hostname|default(inventory_hostname)), + 'task_name': 'custom_field_set', + 'task_add1': 'indoor_fwver_backup', + 'task_result': indoor_fwver_backup_value + } | to_json }}" + payload_encoding: "string" + changed_when: false + + + + - name: Delta tracking | Mark changed indoor_fwver_backup + when: + - (not (nb_baseline_ok | default(false))) or ((indoor_fwver_backup_value) != (nb_custom_fields_current.get('indoor_fwver_backup'))) + delegate_to: localhost + set_fact: + non_ts_diff_found: true + changed_when: false + + # ----------------------- Dev2 fw_printenv size → indoor_bootenv ----------------------- + - name: Dev2 | Read fw_printenv size (tunnel) + when: + - tunnel_ok | default(false) + - dev2_passfile_used != 'NONE' + delegate_to: localhost + shell: | + PORT="{{ _local_port }}" + sshpass -f "{{ dev2_passfile_used }}" ssh {{ ssh_opts_common }} \ + -o ConnectTimeout={{ ssh_timeout }} \ + -p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \ + "fw_printenv 2>/dev/null | wc -l" + args: + executable: /bin/bash + register: dev2_bootenv_wc_tunnel + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev2 | Read fw_printenv size (LLDP) + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (lldp_dev2_ip | default('') | length) > 0 + - dev2_passfile_used_lldp is defined + - dev2_passfile_used_lldp != 'NONE' + delegate_to: localhost + shell: | + sshpass -f "{{ dev2_passfile_used_lldp }}" ssh \ + -o StrictHostKeyChecking=no -o PubkeyAuthentication=no \ + -o ConnectTimeout=15 \ + "{{ dev2_ssh_user }}@{{ lldp_dev2_ip }}" \ + "fw_printenv 2>/dev/null | wc -l" + args: + executable: /bin/bash + register: dev2_bootenv_wc_lldp + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev2 | Read fw_printenv size (LLDP IPv6 via Dev1) + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (dev2_passfile_used_lldp is not defined or dev2_passfile_used_lldp == 'NONE') + - lldp_dev2_ip6 is defined + - lldp_dev2_ip6 | length > 0 + - dev2_passfile_used_lldp6 is defined + - dev2_passfile_used_lldp6 != 'NONE' + delegate_to: localhost + shell: | + IP6="{{ lldp_dev2_ip6 }}" + F="{{ dev2_passfile_used_lldp6 }}" + if [ -z "$IP6" ] || [ -z "$F" ] || [ "$F" = "NONE" ]; then + exit 0 + fi + + sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ + "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ + "sshpass -f '/tmp/${F}' ssh \ + -o StrictHostKeyChecking=no -o PubkeyAuthentication=no \ + -o ConnectTimeout={{ ssh_timeout }} \ + '{{ dev2_ssh_user }}@\[${IP6}%{{ dev1_iface }}\]' \ + 'fw_printenv 2>/dev/null | wc -l'" + args: + executable: /bin/bash + register: dev2_bootenv_wc_lldp6 + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev2 | Select fw_printenv size output + delegate_to: localhost + set_fact: + indoor_bootenv_raw: >- + {{ + (dev2_bootenv_wc_tunnel.stdout | default('') | trim) + if ( + (dev2_bootenv_wc_tunnel is defined) and + ((dev2_bootenv_wc_tunnel.rc | default(1)) == 0) and + ((dev2_bootenv_wc_tunnel.stdout | default('') | trim | length) > 0) + ) + else ( + (dev2_bootenv_wc_lldp.stdout | default('') | trim) + if ( + (dev2_bootenv_wc_lldp is defined) and + ((dev2_bootenv_wc_lldp.rc | default(1)) == 0) and + ((dev2_bootenv_wc_lldp.stdout | default('') | trim | length) > 0) + ) + else ( + (dev2_bootenv_wc_lldp6.stdout | default('') | trim) + if ( + (dev2_bootenv_wc_lldp6 is defined) and + ((dev2_bootenv_wc_lldp6.rc | default(1)) == 0) and + ((dev2_bootenv_wc_lldp6.stdout | default('') | trim | length) > 0) + ) + else '' + ) + ) + }} + + - name: Dev2 | Normalize indoor_bootenv value + delegate_to: localhost + set_fact: + indoor_bootenv_value: >- + {{ + 'unavailable' + if (indoor_bootenv_raw | default('') | trim) in ['', '0'] + else (indoor_bootenv_raw | default('') | trim) + }} + + # ----------------------- Scan tracking: Dev2 indoor_bootenv ----------------------- + - name: Scan tracking | Dev2 indoor_bootenv outcome + delegate_to: localhost + set_fact: + scan_ok: "{{ scan_ok + (['indoor_bootenv'] if ((indoor_bootenv_value | default('unavailable')) != 'unavailable') else []) }}" + scan_fail: "{{ scan_fail + (['indoor_bootenv=unavailable'] if ((indoor_bootenv_value | default('unavailable')) == 'unavailable') else []) }}" + changed_when: false + + - name: Publish Dev2 indoor_bootenv custom field + when: + - (not (nb_baseline_ok | default(false))) or ((indoor_bootenv_value) != (nb_custom_fields_current.get('indoor_bootenv'))) + delegate_to: localhost + uri: + url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost|urlencode }}/{{ rmq_exchange|urlencode }}/publish" + method: POST + user: "{{ rmq_user }}" + password: "{{ rmq_pass }}" + force_basic_auth: true + status_code: 200 + headers: { content-type: "application/json" } + body_format: json + body: + properties: { content_type: "application/json" } + routing_key: "{{ control_queue }}" + payload: "{{ { + 'inscope_device': (ansible_hostname|default(inventory_hostname)), + 'task_name': 'custom_field_set', + 'task_add1': 'indoor_bootenv', + 'task_result': indoor_bootenv_value + } | to_json }}" + payload_encoding: "string" + changed_when: false + + + - name: Delta tracking | Mark changed indoor_bootenv + when: + - (not (nb_baseline_ok | default(false))) or ((indoor_bootenv_value) != (nb_custom_fields_current.get('indoor_bootenv'))) + delegate_to: localhost + set_fact: + non_ts_diff_found: true + changed_when: false + + # ----------------------- Dev2 hostname check + publish ----------------------- + - name: Dev2 | Read hostname (tunnel) + when: + - tunnel_ok | default(false) + - dev2_passfile_used != 'NONE' + delegate_to: localhost + shell: | + PORT="{{ _local_port }}" + sshpass -f "{{ dev2_passfile_used }}" ssh {{ ssh_opts_common }} \ + -o ConnectTimeout={{ ssh_timeout }} \ + -p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \ + "cat /proc/sys/kernel/hostname 2>/dev/null || hostname 2>/dev/null || echo ''" + args: + executable: /bin/bash + register: dev2_hostname_tunnel + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev2 | Read hostname (LLDP IPv4) + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (lldp_dev2_ip | default('') | length) > 0 + - dev2_passfile_used_lldp is defined + - dev2_passfile_used_lldp != 'NONE' + delegate_to: localhost + shell: | + sshpass -f "{{ dev2_passfile_used_lldp }}" ssh \ + -o StrictHostKeyChecking=no -o PubkeyAuthentication=no \ + -o ConnectTimeout=15 \ + "{{ dev2_ssh_user }}@{{ lldp_dev2_ip }}" \ + "cat /proc/sys/kernel/hostname 2>/dev/null || hostname 2>/dev/null || echo ''" + args: + executable: /bin/bash + register: dev2_hostname_lldp + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev2 | Read hostname (LLDP IPv6 via Dev1) + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (dev2_passfile_used_lldp is not defined or dev2_passfile_used_lldp == 'NONE') + - lldp_dev2_ip6 is defined + - lldp_dev2_ip6 | length > 0 + - dev2_passfile_used_lldp6 is defined + - dev2_passfile_used_lldp6 != 'NONE' + delegate_to: localhost + shell: | + IP6="{{ lldp_dev2_ip6 }}" + F="{{ dev2_passfile_used_lldp6 }}" + if [ -z "$IP6" ] || [ -z "$F" ] || [ "$F" = "NONE" ]; then + exit 0 + fi + + sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ + "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ + "sshpass -f '/tmp/${F}' ssh \ + -o StrictHostKeyChecking=no -o PubkeyAuthentication=no \ + -o ConnectTimeout={{ ssh_timeout }} \ + '{{ dev2_ssh_user }}@\[${IP6}%{{ dev1_iface }}\]' \ + 'cat /proc/sys/kernel/hostname 2>/dev/null || hostname 2>/dev/null || echo '''" + args: + executable: /bin/bash + register: dev2_hostname_lldp6 + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev2 | Select hostname output + delegate_to: localhost + set_fact: + indoor_hostname_raw: >- + {{ + (dev2_hostname_tunnel.stdout | default('') | trim) + if ( + (dev2_hostname_tunnel is defined) and + ((dev2_hostname_tunnel.stdout | default('') | trim | length) > 0) + ) + else ( + (dev2_hostname_lldp.stdout | default('') | trim) + if ( + (dev2_hostname_lldp is defined) and + ((dev2_hostname_lldp.stdout | default('') | trim | length) > 0) + ) + else ( + (dev2_hostname_lldp6.stdout | default('') | trim) + if ( + (dev2_hostname_lldp6 is defined) and + ((dev2_hostname_lldp6.stdout | default('') | trim | length) > 0) + ) + else '' + ) + ) + }} + + - name: Dev2 | Normalize indoor_hostname + delegate_to: localhost + set_fact: + indoor_hostname_value: >- + {{ + 'unavailable' + if (indoor_hostname_raw | default('') | trim) == '' + else ( + ( + '!' ~ (indoor_hostname_raw | default('') | trim) + ) + if ( + (dev1_hostname | default(inventory_hostname)) + != (indoor_hostname_raw | default('') | trim) + ) + else (indoor_hostname_raw | default('') | trim) + ) + }} + + # ----------------------- Scan tracking: Dev2 indoor_hostname ----------------------- + - name: Scan tracking | Dev2 indoor_hostname outcome + delegate_to: localhost + set_fact: + scan_ok: "{{ scan_ok + (['indoor_hostname'] if ((indoor_hostname_value | default('unavailable')) != 'unavailable') else []) }}" + scan_fail: "{{ scan_fail + (['indoor_hostname=unavailable'] if ((indoor_hostname_value | default('unavailable')) == 'unavailable') else []) }}" + changed_when: false + + - name: Dev2 | indoor_hostname debug + delegate_to: localhost + debug: + msg: + - "dev1_hostname={{ dev1_hostname | default('n/a') }}" + - "indoor_hostname_raw={{ indoor_hostname_raw | default('n/a') }}" + - "indoor_hostname_value={{ indoor_hostname_value | default('n/a') }}" + + - name: Publish Dev2 indoor_hostname custom field + when: + - (not (nb_baseline_ok | default(false))) or ((indoor_hostname_value) != (nb_custom_fields_current.get('indoor_hostname'))) + delegate_to: localhost + uri: + url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost|urlencode }}/{{ rmq_exchange|urlencode }}/publish" + method: POST + user: "{{ rmq_user }}" + password: "{{ rmq_pass }}" + force_basic_auth: true + status_code: 200 + headers: { content-type: "application/json" } + body_format: json + body: + properties: { content_type: "application/json" } + routing_key: "{{ control_queue }}" + payload: "{{ { + 'inscope_device': (ansible_hostname|default(inventory_hostname)), + 'task_name': 'custom_field_set', + 'task_add1': 'indoor_hostname', + 'task_result': indoor_hostname_value + } | to_json }}" + payload_encoding: "string" + changed_when: false + + + + - name: Delta tracking | Mark changed indoor_hostname + when: + - (not (nb_baseline_ok | default(false))) or ((indoor_hostname_value) != (nb_custom_fields_current.get('indoor_hostname'))) + delegate_to: localhost + set_fact: + non_ts_diff_found: true + changed_when: false + + - name: Indoor | Normalize Dev2 passfile facts (avoid undefined) + delegate_to: localhost + set_fact: + dev2_passfile_used_lldp: "{{ dev2_passfile_used_lldp | default('NONE') }}" + dev2_passfile_used_lldp6: "{{ dev2_passfile_used_lldp6 | default('NONE') }}" + changed_when: false + + # ----------------------- Dev2 indoor_ip resolution (ppp0/eth0.4000 preferred) ----------------------- + - name: Indoor | Compute fallback connected IP (as used for Dev2 reachability) + delegate_to: localhost + set_fact: + indoor_conn_ip_fallback: >- + {%- if (tunnel_ok | default(false)) and (dev2_passfile_used | default('NONE')) != 'NONE' -%} + {{ dev2_host }} + {%- elif (dev2_passfile_used_lldp is defined) + and (dev2_passfile_used_lldp | default('NONE')) != 'NONE' + and (lldp_dev2_ip | default('') | length) > 0 -%} + {{ lldp_dev2_ip }} + {%- elif (dev2_passfile_used_lldp6 is defined) + and (dev2_passfile_used_lldp6 | default('NONE')) != 'NONE' + and (lldp_dev2_ip6 | default('') | length) > 0 -%} + {{ lldp_dev2_ip6 }} + {%- else -%} + na + {%- endif -%} + changed_when: false + + + - name: Indoor | Normalize LLDP IP facts (avoid undefined) + delegate_to: localhost + set_fact: + lldp_dev2_ip: "{{ lldp_dev2_ip | default('') }}" + lldp_dev2_ip6: "{{ lldp_dev2_ip6 | default('') }}" + changed_when: false + + - name: Dev2 | Read ppp0 10/8 IPv4 (preferred) + when: + - (dev2_passfile_used | default('NONE')) != 'NONE' + or ((dev2_passfile_used_lldp | default('NONE')) != 'NONE') + or ((dev2_passfile_used_lldp6 | default('NONE')) != 'NONE') + delegate_to: localhost + shell: | + set -o pipefail + + # Choose connection method (tunnel > LLDP IPv4 > LLDP IPv6) + if [ "{{ dev2_passfile_used | default('NONE') }}" != "NONE" ]; then + PORT="{{ _local_port }}" + sshpass -f "{{ dev2_passfile_used }}" ssh {{ ssh_opts_common }} \ + -o ConnectTimeout={{ ssh_timeout }} \ + -p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \ + "ip -o -4 addr show dev ppp0 2>/dev/null | awk '{print \$4}' | cut -d/ -f1 | grep -m1 '^10\\.' || true" + exit 0 + fi + + if [ "{{ dev2_passfile_used_lldp | default('NONE') }}" != "NONE" ] && [ -n "{{ lldp_dev2_ip | default('') }}" ]; then + sshpass -f "{{ dev2_passfile_used_lldp }}" ssh \ + -o StrictHostKeyChecking=no -o PubkeyAuthentication=no \ + -o ConnectTimeout=15 \ + "{{ dev2_ssh_user }}@{{ lldp_dev2_ip }}" \ + "ip -o -4 addr show dev ppp0 2>/dev/null | awk '{print \$4}' | cut -d/ -f1 | grep -m1 '^10\\.' || true" + exit 0 + fi + + if [ "{{ dev2_passfile_used_lldp6 | default('NONE') }}" != "NONE" ] && [ -n "{{ lldp_dev2_ip6 | default('') }}" ]; then + IP6="{{ lldp_dev2_ip6 }}" + F="{{ dev2_passfile_used_lldp6 }}" + sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ + "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ + "sshpass -f '/tmp/${F}' ssh \ + -o StrictHostKeyChecking=no -o PubkeyAuthentication=no \ + -o ConnectTimeout={{ ssh_timeout }} \ + '{{ dev2_ssh_user }}@\[${IP6}%{{ dev1_iface }}\]' \ + \"ip -o -4 addr show dev ppp0 2>/dev/null | awk '{print \\\\\$4}' | cut -d/ -f1 | grep -m1 '^10\\\\.' || true\"" + exit 0 + fi + + exit 0 + args: + executable: /bin/bash + register: dev2_ppp0_ip + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev2 | Read eth0.4000 10/8 IPv4 (only if ppp0 not usable) + when: + - (dev2_ppp0_ip.stdout | default('') | trim) == '' + - (dev2_passfile_used | default('NONE')) != 'NONE' + or ((dev2_passfile_used_lldp | default('NONE')) != 'NONE') + or ((dev2_passfile_used_lldp6 | default('NONE')) != 'NONE') + delegate_to: localhost + shell: | + set -o pipefail + + # Choose connection method (tunnel > LLDP IPv4 > LLDP IPv6) + if [ "{{ dev2_passfile_used | default('NONE') }}" != "NONE" ]; then + PORT="{{ _local_port }}" + sshpass -f "{{ dev2_passfile_used }}" ssh {{ ssh_opts_common }} \ + -o ConnectTimeout={{ ssh_timeout }} \ + -p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \ + "ip -o -4 addr show dev eth0.4000 2>/dev/null | awk '{print \$4}' | cut -d/ -f1 | grep -m1 '^10\\.' || true" + exit 0 + fi + + if [ "{{ dev2_passfile_used_lldp | default('NONE') }}" != "NONE" ] && [ -n "{{ lldp_dev2_ip | default('') }}" ]; then + sshpass -f "{{ dev2_passfile_used_lldp }}" ssh \ + -o StrictHostKeyChecking=no -o PubkeyAuthentication=no \ + -o ConnectTimeout=15 \ + "{{ dev2_ssh_user }}@{{ lldp_dev2_ip }}" \ + "ip -o -4 addr show dev eth0.4000 2>/dev/null | awk '{print \$4}' | cut -d/ -f1 | grep -m1 '^10\\.' || true" + exit 0 + fi + + if [ "{{ dev2_passfile_used_lldp6 | default('NONE') }}" != "NONE" ] && [ -n "{{ lldp_dev2_ip6 | default('') }}" ]; then + IP6="{{ lldp_dev2_ip6 }}" + F="{{ dev2_passfile_used_lldp6 }}" + sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ + "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ + "sshpass -f '/tmp/${F}' ssh \ + -o StrictHostKeyChecking=no -o PubkeyAuthentication=no \ + -o ConnectTimeout={{ ssh_timeout }} \ + '{{ dev2_ssh_user }}@\[${IP6}%{{ dev1_iface }}\]' \ + \"ip -o -4 addr show dev eth0.4000 2>/dev/null | awk '{print \\\\\$4}' | cut -d/ -f1 | grep -m1 '^10\\\\.' || true\"" + exit 0 + fi + + exit 0 + args: + executable: /bin/bash + register: dev2_eth04000_ip + changed_when: false + failed_when: false + ignore_errors: true + + - name: Indoor | Select indoor_ip (ppp0 > eth0.4000 > fallback) + delegate_to: localhost + set_fact: + indoor_ip_value: >- + {%- if (dev2_ppp0_ip.stdout | default('') | trim) is match('^10\.') -%} + {{ dev2_ppp0_ip.stdout | trim }} + {%- elif (dev2_eth04000_ip.stdout | default('') | trim) is match('^10\.') -%} + {{ dev2_eth04000_ip.stdout | trim }} + {%- else -%} + {{ indoor_conn_ip_fallback | default('na') | trim }} + {%- endif -%} + changed_when: false + + - name: Dev2 | indoor_ip debug + delegate_to: localhost + debug: + msg: + - "ppp0_ip={{ dev2_ppp0_ip.stdout | default('') | trim }}" + - "eth0.4000_ip={{ dev2_eth04000_ip.stdout | default('') | trim }}" + - "fallback_ip={{ indoor_conn_ip_fallback | default('') | trim }}" + - "selected indoor_ip={{ indoor_ip_value | default('') | trim }}" + + - name: Publish Dev2 indoor_ip custom field + when: + - (not (nb_baseline_ok | default(false))) or (((indoor_ip_value | default('na') | trim)) != (nb_custom_fields_current.get('indoor_ip'))) + delegate_to: localhost + uri: + url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost|urlencode }}/{{ rmq_exchange|urlencode }}/publish" + method: POST + user: "{{ rmq_user }}" + password: "{{ rmq_pass }}" + force_basic_auth: true + status_code: 200 + headers: { content-type: "application/json" } + body_format: json + body: + properties: { content_type: "application/json" } + routing_key: "{{ control_queue }}" + payload: "{{ { + 'inscope_device': (ansible_hostname|default(inventory_hostname)), + 'task_name': 'custom_field_set', + 'task_add1': 'indoor_ip', + 'task_result': (indoor_ip_value | default('na') | trim) + } | to_json }}" + payload_encoding: "string" + changed_when: false + + + - name: Delta tracking | Mark changed indoor_ip + when: + - (not (nb_baseline_ok | default(false))) or (((indoor_ip_value | default('na') | trim)) != (nb_custom_fields_current.get('indoor_ip'))) + delegate_to: localhost + set_fact: + non_ts_diff_found: true + changed_when: false + + # ----------------------- SOT timestamp (container local time) → sot_ts ----------------------- + - name: SOT ts | Get container timestamp + delegate_to: localhost + shell: "date '+%y%m%d-%H%M%S'" + args: + executable: /bin/bash + register: sot_ts_cmd + changed_when: false + failed_when: false + + - name: SOT ts | Set sot_ts value + delegate_to: localhost + set_fact: + sot_ts_value: "{{ sot_ts_cmd.stdout | default('') | trim }}" + changed_when: false + + - name: SOT ts | Debug + delegate_to: localhost + debug: + msg: + - "sot_ts_value={{ sot_ts_value | default('unset') }}" + + - name: Scan tracking | sot_ts outcome + delegate_to: localhost + set_fact: + scan_ok: "{{ scan_ok + (['sot_ts'] if ((sot_ts_value | default('') | length) > 0) else []) }}" + scan_fail: "{{ scan_fail + (['sot_ts=unavailable'] if ((sot_ts_value | default('') | length) == 0) else []) }}" + changed_when: false + + - name: Publish sot_ts + delegate_to: localhost + uri: + url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost|urlencode }}/{{ rmq_exchange|urlencode }}/publish" + method: POST + user: "{{ rmq_user }}" + password: "{{ rmq_pass }}" + force_basic_auth: true + status_code: 200 + headers: { content-type: "application/json" } + body_format: json + body: + properties: { content_type: "application/json" } + routing_key: "{{ control_queue }}" + payload: "{{ { + 'inscope_device': (ansible_hostname|default(inventory_hostname)), + 'task_name': 'custom_field_set', + 'task_add1': 'sot_ts', + 'task_result': (sot_ts_value if (sot_ts_value|length>0) else 'unavailable') + } | to_json }}" + payload_encoding: "string" + changed_when: false + + + # ----------------------- Final journal: scan status ----------------------- + - name: Journal final scan status (success / partial / fail) + when: non_ts_diff_found | default(false) + delegate_to: localhost + vars: + _ok_n: "{{ (scan_ok | default([])) | length }}" + _fail_n: "{{ (scan_fail | default([])) | length }}" + _reason: "{{ (scan_fail | default([])) | join('; ') }}" + _msg: >- + {%- if (_fail_n | int) == 0 -%} + scan was successful + {%- elif (_ok_n | int) == 0 -%} + scanning failed — {{ _reason }} + {%- else -%} + scan was completed partially — {{ _reason }} + {%- endif -%} + uri: + url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish" + method: POST + user: "{{ rmq_user }}" + password: "{{ rmq_pass }}" + force_basic_auth: true + status_code: 200 + headers: { content-type: "application/json" } + body_format: json + body: + properties: { content_type: "application/json" } + routing_key: "{{ control_queue }}" + payload: "{{ { + 'inscope_device': (ansible_hostname | default(inventory_hostname)), + 'task_name': 'journal_add', + 'task_result': _msg + } | to_json }}" + payload_encoding: "string" + changed_when: false + + post_tasks: + - name: "✅ Cleanup start" + debug: + msg: "Closing tunnel and removing temporary IP" + + - name: Refresh ARP (cleanup) + ignore_unreachable: true + raw: "{{ pathprefix }} arping -U -I {{ arping_iface }} {{ dev2_side_ip_addr }} -c 3" + failed_when: false + ignore_errors: true + + - name: Close SSH ControlMaster + delegate_to: localhost + shell: "ssh -S '{{ _ctrl_sock|default('/dev/null') }}' -O exit 2>/dev/null || true" + failed_when: false + ignore_errors: true + + - name: Remove control dir + delegate_to: localhost + file: + path: "{{ _ctrl_dir|default('/tmp/none') }}" + state: absent + ignore_errors: true + + - name: Remove temporary IP + ignore_unreachable: true + raw: "{{ pathprefix }} ip a del {{ dev2_side_ip }} dev {{ dev1_iface }}" + failed_when: false + ignore_errors: true + + - name: "✅ Completed | Device processed" + debug: + msg: + - "Device: {{ inventory_hostname }}" + - "Status: DONE (telemetry only)" diff --git a/files/ansible-playbooks/sot-updater.yml b/files/ansible-playbooks/sot-updater.yml index d695e39..36e0c31 100644 --- a/files/ansible-playbooks/sot-updater.yml +++ b/files/ansible-playbooks/sot-updater.yml @@ -1699,6 +1699,63 @@ failed_when: false ignore_errors: true + - name: Dev2 | Read prepared_backup marker version (tunnel) + when: + - tunnel_ok | default(false) + - dev2_passfile_used != 'NONE' + delegate_to: localhost + shell: | + PORT="{{ _local_port }}" + sshpass -f "{{ dev2_passfile_used }}" ssh {{ ssh_opts_common }} -o ConnectTimeout={{ ssh_timeout }} -p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" "f=$(ls -1 /root/prepared_backup_* 2>/dev/null | head -n1); f=${f##*/prepared_backup_}; v=${f%-r*}; r=${f##*-r}; [ -n "$v" ] && [ -n "$r" ] && echo "$v rev $r" || true" + args: + executable: /bin/bash + register: dev2_fwver_backup_marker_tunnel + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev2 | Read prepared_backup marker version (LLDP IPv4) + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (lldp_dev2_ip | default('') | length) > 0 + - dev2_passfile_used_lldp is defined + - dev2_passfile_used_lldp != 'NONE' + delegate_to: localhost + shell: | + sshpass -f "{{ dev2_passfile_used_lldp }}" ssh -o StrictHostKeyChecking=no -o PubkeyAuthentication=no -o ConnectTimeout=15 "{{ dev2_ssh_user }}@{{ lldp_dev2_ip }}" "f=$(ls -1 /root/prepared_backup_* 2>/dev/null | head -n1); f=${f##*/prepared_backup_}; v=${f%-r*}; r=${f##*-r}; [ -n "$v" ] && [ -n "$r" ] && echo "$v rev $r" || true" + args: + executable: /bin/bash + register: dev2_fwver_backup_marker_lldp + changed_when: false + failed_when: false + ignore_errors: true + + - name: Dev2 | Read prepared_backup marker version (LLDP IPv6 via Dev1) + when: + - tunnel_ok | default(false) + - dev2_passfile_used == 'NONE' + - (dev2_passfile_used_lldp is not defined or dev2_passfile_used_lldp == 'NONE') + - lldp_dev2_ip6 is defined + - lldp_dev2_ip6 | length > 0 + - dev2_passfile_used_lldp6 is defined + - dev2_passfile_used_lldp6 != 'NONE' + delegate_to: localhost + shell: | + IP6="{{ lldp_dev2_ip6 }}" + F="{{ dev2_passfile_used_lldp6 }}" + if [ -z "$IP6" ] || [ -z "$F" ] || [ "$F" = "NONE" ]; then + exit 0 + fi + + sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" "sshpass -f '/tmp/${F}' ssh -o StrictHostKeyChecking=no -o PubkeyAuthentication=no -o ConnectTimeout={{ ssh_timeout }} '{{ dev2_ssh_user }}@\[${IP6}%{{ dev1_iface }}\]' 'f=\$(ls -1 /root/prepared_backup_* 2>/dev/null | head -n1); f=\${f##*/prepared_backup_}; v=\${f%-r*}; r=\${f##*-r}; [ -n "\$v" ] && [ -n "\$r" ] && echo "\$v rev \$r" || true'" + args: + executable: /bin/bash + register: dev2_fwver_backup_marker_lldp6 + changed_when: false + failed_when: false + ignore_errors: true + - name: Dev2 | Select bootbanks backup firmux output delegate_to: localhost set_fact: @@ -1715,7 +1772,28 @@ (dev2_fwver_backup_lldp is defined) and ((dev2_fwver_backup_lldp.stdout | default('') | trim | length) > 0) ) - else (dev2_fwver_backup_lldp6.stdout | default('') | trim) + else ( + (dev2_fwver_backup_lldp6.stdout | default('') | trim) + if ( + (dev2_fwver_backup_lldp6 is defined) and + ((dev2_fwver_backup_lldp6.stdout | default('') | trim | length) > 0) + ) + else ( + (dev2_fwver_backup_marker_tunnel.stdout | default('') | trim) + if ( + (dev2_fwver_backup_marker_tunnel is defined) and + ((dev2_fwver_backup_marker_tunnel.stdout | default('') | trim | length) > 0) + ) + else ( + (dev2_fwver_backup_marker_lldp.stdout | default('') | trim) + if ( + (dev2_fwver_backup_marker_lldp is defined) and + ((dev2_fwver_backup_marker_lldp.stdout | default('') | trim | length) > 0) + ) + else (dev2_fwver_backup_marker_lldp6.stdout | default('') | trim) + ) + ) + ) ) }} changed_when: false @@ -1727,7 +1805,7 @@ {{ (indoor_fwver_backup_raw | default('') | trim) if ((indoor_fwver_backup_raw | default('') | trim | length) > 0) - else 'unavailable' + else '0.0.0 rev 0' }} changed_when: false