# 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) }}" # Cloud API (offboard provisioning telemetry) cloud_config_base: "https://cloud.ikeja.co.za/v2/devices" cloud_bearer: "{{ lookup('env','CLOUD_BEARER') | default('', 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 auto_ssid config flag → auto_ssid ----------------------- - name: Dev1 | Read auto_ssid from config.json delegate_to: localhost shell: | sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ "grep -E '\"auto_ssid\":' /tmp/config.json 2>/dev/null | head -n1 | sed -nE 's/^[[:space:]]*\"auto_ssid\"[[:space:]]*:[[:space:]]*(true|false)[[:space:]]*,?[[:space:]]*$/\1/p' || true" register: auto_ssid_raw changed_when: false failed_when: false ignore_errors: true - name: Dev1 | Normalize auto_ssid value delegate_to: localhost vars: auto_ssid_raw_clean: "{{ auto_ssid_raw.stdout | default('') | trim | lower }}" set_fact: auto_ssid_value: >- {{ auto_ssid_raw_clean if auto_ssid_raw_clean in ['true', 'false'] else 'n/a' }} changed_when: false # ----------------------- Scan tracking: Dev1 auto_ssid ----------------------- - name: Scan tracking | Dev1 auto_ssid outcome delegate_to: localhost set_fact: scan_ok: "{{ scan_ok + (['auto_ssid'] if ((auto_ssid_value | default('n/a')) != 'n/a') else []) }}" scan_fail: "{{ scan_fail + (['auto_ssid=n/a'] if ((auto_ssid_value | default('n/a')) == 'n/a') else []) }}" changed_when: false - name: Dev1 | auto_ssid debug delegate_to: localhost debug: msg: - "auto_ssid_raw={{ auto_ssid_raw.stdout | default('') | trim }}" - "auto_ssid_value={{ auto_ssid_value | default('n/a') }}" - name: Publish Dev1 auto_ssid custom field when: - (not (nb_baseline_ok | default(false))) or ((auto_ssid_value) != (nb_custom_fields_current.get('auto_ssid'))) 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': 'auto_ssid', 'task_result': auto_ssid_value } | to_json }}" payload_encoding: "string" changed_when: false - name: Delta tracking | Mark changed auto_ssid when: - (not (nb_baseline_ok | default(false))) or ((auto_ssid_value) != (nb_custom_fields_current.get('auto_ssid'))) 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 bandwidth shaping → bw_up / bw_down ----------------------- - name: Dev1 | Read bw_up from ifb0 shaping delegate_to: localhost shell: | sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ "tc -d class show dev ifb0 2>/dev/null | grep 'root burst' | awk -F' ' '{ print \$12 }'" args: executable: /bin/bash register: dev1_bw_up_raw changed_when: false failed_when: false ignore_errors: true - name: Dev1 | Normalize bw_up value delegate_to: localhost set_fact: bw_up_value: >- {{ (dev1_bw_up_raw.stdout | default('') | trim) if ((dev1_bw_up_raw.stdout | default('') | trim | length) > 0) else 'unavailable' }} changed_when: false - name: Scan tracking | Dev1 bw_up outcome delegate_to: localhost set_fact: scan_ok: "{{ scan_ok + (['bw_up'] if ((bw_up_value | default('unavailable')) != 'unavailable') else []) }}" scan_fail: "{{ scan_fail + (['bw_up=unavailable'] if ((bw_up_value | default('unavailable')) == 'unavailable') else []) }}" changed_when: false - name: Publish Dev1 bw_up custom field when: - (not (nb_baseline_ok | default(false))) or ((bw_up_value) != (nb_custom_fields_current.get('bw_up'))) 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': 'bw_up', 'task_result': bw_up_value } | to_json }}" payload_encoding: "string" changed_when: false - name: Delta tracking | Mark changed bw_up when: - (not (nb_baseline_ok | default(false))) or ((bw_up_value) != (nb_custom_fields_current.get('bw_up'))) delegate_to: localhost set_fact: non_ts_diff_found: true changed_when: false - name: Dev1 | Read bw_down from eth0 shaping delegate_to: localhost shell: | sshpass -p '{{ dev1_pass }}' ssh {{ ssh_opts_common }} \ "{{ dev1_user }}@{{ ansible_host|default(inventory_hostname) }}" \ "tc -d qdisc show dev eth0 2>/dev/null | grep 'root' | awk -F' ' '{ print \$10 }'" args: executable: /bin/bash register: dev1_bw_down_raw changed_when: false failed_when: false ignore_errors: true - name: Dev1 | Normalize bw_down value delegate_to: localhost set_fact: bw_down_value: >- {{ (dev1_bw_down_raw.stdout | default('') | trim) if ((dev1_bw_down_raw.stdout | default('') | trim | length) > 0) else 'unavailable' }} changed_when: false - name: Scan tracking | Dev1 bw_down outcome delegate_to: localhost set_fact: scan_ok: "{{ scan_ok + (['bw_down'] if ((bw_down_value | default('unavailable')) != 'unavailable') else []) }}" scan_fail: "{{ scan_fail + (['bw_down=unavailable'] if ((bw_down_value | default('unavailable')) == 'unavailable') else []) }}" changed_when: false - name: Publish Dev1 bw_down custom field when: - (not (nb_baseline_ok | default(false))) or ((bw_down_value) != (nb_custom_fields_current.get('bw_down'))) 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': 'bw_down', 'task_result': bw_down_value } | to_json }}" payload_encoding: "string" changed_when: false - name: Delta tracking | Mark changed bw_down when: - (not (nb_baseline_ok | default(false))) or ((bw_down_value) != (nb_custom_fields_current.get('bw_down'))) 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('')) }} # ----------------------- Cloud offboard provisioning telemetry ----------------------- - name: Cloud offboard provisioning | Resolve bearer used by nb_onedevice_update.py delegate_to: localhost environment: OFFP_CLOUD_BEARER: "{{ cloud_bearer | default('') }}" shell: | if [ -n "${OFFP_CLOUD_BEARER:-}" ]; then printf '%s\n' "$OFFP_CLOUD_BEARER" exit 0 fi python3 - <<'PY' import ast from pathlib import Path path = Path('/opt/containers/ansible-worker/app/nb_onedevice_update.py') tree = ast.parse(path.read_text()) for node in tree.body: if not isinstance(node, ast.Assign): continue if any(isinstance(target, ast.Name) and target.id == 'CLOUD_BEARER' for target in node.targets): value = ast.literal_eval(node.value) if isinstance(value, str) and value: print(value) raise SystemExit(0) raise SystemExit(1) PY args: executable: /bin/bash register: offp_cloud_bearer_cmd changed_when: false failed_when: false - name: Cloud offboard provisioning | Set bearer fact delegate_to: localhost set_fact: offp_cloud_bearer: "{{ offp_cloud_bearer_cmd.stdout | default('') | trim }}" changed_when: false - name: Cloud offboard provisioning | Resolve cloud_id and initialize values delegate_to: localhost set_fact: offp_cloud_id: >- {{ (nb_custom_fields_current.get('cloud_id','') | string | trim) if ((nb_custom_fields_current.get('cloud_id','') | string | trim | length) > 0) else ( ( (nb_preflight | default({})).get('stdout','') | regex_findall('CL: fetch liveness cloud_id=([^\s]+)') | first ) | default('') ) }} offp_config_ok: false offp_status_value: "missing" offp_sync_value: "missing" indoor_offp_vlan_value: "missing" changed_when: false - name: Cloud offboard provisioning | Read reported config when: - (offp_cloud_id | default('') | length) > 0 - (offp_cloud_bearer | default('') | length) > 0 delegate_to: localhost uri: url: "{{ cloud_config_base }}/{{ offp_cloud_id }}/config" method: GET headers: Authorization: "Bearer {{ offp_cloud_bearer }}" Accept: "application/json" return_content: true status_code: 200 timeout: 30 register: offp_config_lookup failed_when: false changed_when: false - name: Cloud offboard provisioning | Normalize reported values when: - offp_config_lookup is defined - (offp_config_lookup.status | default(0) | int) == 200 - offp_config_lookup.json is defined delegate_to: localhost vars: _reported_data: "{{ (offp_config_lookup.json.get('reported', {})).get('data', {}) }}" _offp_services: "{{ (_reported_data.get('services', {})).get('offboardProvisioning', {}) }}" _offp_vlan: "{{ (((((((_reported_data.get('offboardProvisioned', {})).get('fox200', {})).get('ethernet', {})).get('ports', {})).get('eth0', {})).get('network', {})).get('dataVlan', {})) }}" set_fact: offp_config_ok: true offp_status_value: >- {%- if (_offp_services is mapping) and ('isEnabled' in _offp_services) -%} {%- if _offp_services.get('isEnabled') == true -%}enabled {%- elif _offp_services.get('isEnabled') == false -%}disabled {%- else -%}missing{%- endif -%} {%- else -%}missing{%- endif -%} offp_sync_value: >- {%- if (_offp_services is mapping) and ('isSynchronized' in _offp_services) -%} {%- if _offp_services.get('isSynchronized') == true -%}enabled {%- elif _offp_services.get('isSynchronized') == false -%}disabled {%- else -%}missing{%- endif -%} {%- else -%}missing{%- endif -%} indoor_offp_vlan_value: >- {%- if (_offp_vlan is mapping) and ('isEnabled' in _offp_vlan) -%} {%- set _vlan_id = (_offp_vlan.get('id') | string | trim) if ( ('id' in _offp_vlan) and (_offp_vlan.get('id') is not none) and ((_offp_vlan.get('id') | string | trim | length) > 0) ) else 'missing' -%} {%- if _offp_vlan.get('isEnabled') == true -%}enabled-{{ _vlan_id }} {%- elif _offp_vlan.get('isEnabled') == false -%}disabled-{{ _vlan_id }} {%- else -%}missing{%- endif -%} {%- else -%}missing{%- endif -%} changed_when: false - name: Cloud offboard provisioning | Debug delegate_to: localhost debug: msg: - "cloud_id={{ offp_cloud_id | default('') }}" - "config_http={{ offp_config_lookup.status | default('not-requested') }}" - "offp_status={{ offp_status_value | default('missing') }}" - "offp_sync={{ offp_sync_value | default('missing') }}" - "indoor_offp_vlan={{ indoor_offp_vlan_value | default('missing') }}" - name: Scan tracking | Cloud offboard provisioning outcome delegate_to: localhost set_fact: scan_ok: "{{ scan_ok + (['offp_status', 'offp_sync', 'indoor_offp_vlan'] if (offp_config_ok | default(false)) else []) }}" scan_fail: "{{ scan_fail + ([] if (offp_config_ok | default(false)) else ['offp_status=unavailable', 'offp_sync=unavailable', 'indoor_offp_vlan=unavailable']) }}" changed_when: false - name: Publish Cloud offp_status custom field when: - offp_config_ok | default(false) - (not (nb_baseline_ok | default(false))) or ((offp_status_value) != (nb_custom_fields_current.get('offp_status'))) 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': 'offp_status', 'task_result': offp_status_value } | to_json }}" payload_encoding: "string" changed_when: false - name: Delta tracking | Mark changed offp_status when: - offp_config_ok | default(false) - (not (nb_baseline_ok | default(false))) or ((offp_status_value) != (nb_custom_fields_current.get('offp_status'))) delegate_to: localhost set_fact: non_ts_diff_found: true changed_when: false - name: Publish Cloud offp_sync custom field when: - offp_config_ok | default(false) - (not (nb_baseline_ok | default(false))) or ((offp_sync_value) != (nb_custom_fields_current.get('offp_sync'))) 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': 'offp_sync', 'task_result': offp_sync_value } | to_json }}" payload_encoding: "string" changed_when: false - name: Delta tracking | Mark changed offp_sync when: - offp_config_ok | default(false) - (not (nb_baseline_ok | default(false))) or ((offp_sync_value) != (nb_custom_fields_current.get('offp_sync'))) delegate_to: localhost set_fact: non_ts_diff_found: true changed_when: false - name: Publish Cloud indoor_offp_vlan custom field when: - offp_config_ok | default(false) - (not (nb_baseline_ok | default(false))) or ((indoor_offp_vlan_value) != (nb_custom_fields_current.get('indoor_offp_vlan'))) 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_offp_vlan', 'task_result': indoor_offp_vlan_value } | to_json }}" payload_encoding: "string" changed_when: false - name: Delta tracking | Mark changed indoor_offp_vlan when: - offp_config_ok | default(false) - (not (nb_baseline_ok | default(false))) or ((indoor_offp_vlan_value) != (nb_custom_fields_current.get('indoor_offp_vlan'))) delegate_to: localhost set_fact: non_ts_diff_found: true changed_when: false # ----------------------- 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 | 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##*/}; ver=$(printf "%s\n" "$f" | grep -Eo "[0-9]+\.[0-9]+\.[0-9]+-r[0-9]+" | head -n1); [ -n "$ver" ] && echo "${ver/-r/ rev }" || 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##*/}; ver=$(printf "%s\n" "$f" | grep -Eo "[0-9]+\.[0-9]+\.[0-9]+-r[0-9]+" | head -n1); [ -n "$ver" ] && echo "${ver/-r/ rev }" || 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##*/}; ver=\$(printf \"%s\n\" \"\$f\" | grep -Eo \"[0-9]+\.[0-9]+\.[0-9]+-r[0-9]+\" | head -n1); [ -n \"\$ver\" ] && echo \"\${ver/-r/ rev }\" || 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: 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) 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 - 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 '0.0.0 rev 0' }} 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 # ----------------------- Dev2 payredirect config flag -> payredirect ----------------------- - name: Dev2 | Read payredirect (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 /tmp/config.json 2>/dev/null | grep -i block -A2 | grep enab | head -n1" args: executable: /bin/bash register: dev2_payredirect_tunnel changed_when: false failed_when: false ignore_errors: true - name: Dev2 | Read payredirect (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 /tmp/config.json 2>/dev/null | grep -i block -A2 | grep enab | head -n1" args: executable: /bin/bash register: dev2_payredirect_lldp changed_when: false failed_when: false ignore_errors: true - name: Dev2 | Read payredirect (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 /tmp/config.json 2>/dev/null | grep -i block -A2 | grep enab | head -n1'" args: executable: /bin/bash register: dev2_payredirect_lldp6 changed_when: false failed_when: false ignore_errors: true - name: Dev2 | Select payredirect output delegate_to: localhost set_fact: payredirect_raw: >- {{ (dev2_payredirect_tunnel.stdout | default('') | trim) if ( (dev2_payredirect_tunnel is defined) and ((dev2_payredirect_tunnel.stdout | default('') | trim | length) > 0) ) else ( (dev2_payredirect_lldp.stdout | default('') | trim) if ( (dev2_payredirect_lldp is defined) and ((dev2_payredirect_lldp.stdout | default('') | trim | length) > 0) ) else ( (dev2_payredirect_lldp6.stdout | default('') | trim) if ( (dev2_payredirect_lldp6 is defined) and ((dev2_payredirect_lldp6.stdout | default('') | trim | length) > 0) ) else '' ) ) }} changed_when: false - name: Dev2 | Normalize payredirect value delegate_to: localhost vars: _payredirect_raw_clean: "{{ payredirect_raw | default('') | trim | lower }}" set_fact: payredirect_value: >- {%- if 'true' in _payredirect_raw_clean -%} true {%- elif 'false' in _payredirect_raw_clean -%} false {%- else -%} na {%- endif -%} changed_when: false - name: Dev2 | payredirect debug delegate_to: localhost debug: msg: - "tunnel rc={{ dev2_payredirect_tunnel.rc | default('n/a') }} out={{ dev2_payredirect_tunnel.stdout | default('') | trim }} err={{ dev2_payredirect_tunnel.stderr | default('') | trim }}" - "lldp4 rc={{ dev2_payredirect_lldp.rc | default('n/a') }} out={{ dev2_payredirect_lldp.stdout | default('') | trim }} err={{ dev2_payredirect_lldp.stderr | default('') | trim }}" - "lldp6 rc={{ dev2_payredirect_lldp6.rc | default('n/a') }} out={{ dev2_payredirect_lldp6.stdout | default('') | trim }} err={{ dev2_payredirect_lldp6.stderr | default('') | trim }}" - "payredirect_raw={{ payredirect_raw | default('') | trim }}" - "payredirect_value={{ payredirect_value | default('na') }}" - name: Scan tracking | Dev2 payredirect outcome delegate_to: localhost set_fact: scan_ok: "{{ scan_ok + (['payredirect'] if ((payredirect_value | default('na')) != 'na') else []) }}" scan_fail: "{{ scan_fail + (['payredirect=na'] if ((payredirect_value | default('na')) == 'na') else []) }}" changed_when: false - name: Publish Dev2 payredirect custom field when: - (not (nb_baseline_ok | default(false))) or ((payredirect_value) != (nb_custom_fields_current.get('payredirect'))) 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': 'payredirect', 'task_result': payredirect_value } | to_json }}" payload_encoding: "string" changed_when: false - name: Delta tracking | Mark changed payredirect when: - (not (nb_baseline_ok | default(false))) or ((payredirect_value) != (nb_custom_fields_current.get('payredirect'))) 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 # ----------------------- Dev2 default gateway interface → indoor_defgw ----------------------- - name: Dev2 | Read default gateway interface (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" "ip route show 2>/dev/null | grep default | awk '{print \$NF}' | head -n1 || true" args: executable: /bin/bash register: dev2_defgw_tunnel changed_when: false failed_when: false ignore_errors: true - name: Dev2 | Read default gateway interface (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 }}" "ip route show 2>/dev/null | grep default | awk '{print \$NF}' | head -n1 || true" args: executable: /bin/bash register: dev2_defgw_lldp changed_when: false failed_when: false ignore_errors: true - name: Dev2 | Read default gateway interface (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 }}\]' "ip route show 2>/dev/null | grep default | awk '{print \\\$NF}' | head -n1 || true"" args: executable: /bin/bash register: dev2_defgw_lldp6 changed_when: false failed_when: false ignore_errors: true - name: Dev2 | Select default gateway interface output delegate_to: localhost set_fact: indoor_defgw_raw: >- {{ (dev2_defgw_tunnel.stdout | default('') | trim) if ( (dev2_defgw_tunnel is defined) and ((dev2_defgw_tunnel.stdout | default('') | trim | length) > 0) ) else ( (dev2_defgw_lldp.stdout | default('') | trim) if ( (dev2_defgw_lldp is defined) and ((dev2_defgw_lldp.stdout | default('') | trim | length) > 0) ) else ( (dev2_defgw_lldp6.stdout | default('') | trim) if ( (dev2_defgw_lldp6 is defined) and ((dev2_defgw_lldp6.stdout | default('') | trim | length) > 0) ) else '' ) ) }} changed_when: false - name: Dev2 | Normalize indoor_defgw value delegate_to: localhost set_fact: indoor_defgw_value: >- {{ 'unavailable' if (indoor_defgw_raw | default('') | trim) == '' else (indoor_defgw_raw | default('') | trim) }} changed_when: false - name: Scan tracking | Dev2 indoor_defgw outcome delegate_to: localhost set_fact: scan_ok: "{{ scan_ok + (['indoor_defgw'] if ((indoor_defgw_value | default('unavailable')) != 'unavailable') else []) }}" scan_fail: "{{ scan_fail + (['indoor_defgw=unavailable'] if ((indoor_defgw_value | default('unavailable')) == 'unavailable') else []) }}" changed_when: false - name: Publish Dev2 indoor_defgw custom field when: - (not (nb_baseline_ok | default(false))) or ((indoor_defgw_value) != (nb_custom_fields_current.get('indoor_defgw'))) 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_defgw', 'task_result': indoor_defgw_value } | to_json }}" payload_encoding: "string" changed_when: false - name: Delta tracking | Mark changed indoor_defgw when: - (not (nb_baseline_ok | default(false))) or ((indoor_defgw_value) != (nb_custom_fields_current.get('indoor_defgw'))) 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)"