From e064ffbeb298e5d0bfe4c62cd11179a010ac5a2a Mon Sep 17 00:00:00 2001 From: pavel Date: Wed, 5 Nov 2025 22:51:38 +0200 Subject: [PATCH] 22:51 --- files/ansible-playbooks/sot-updater.yml | 333 ++++++++++++------------ 1 file changed, 163 insertions(+), 170 deletions(-) diff --git a/files/ansible-playbooks/sot-updater.yml b/files/ansible-playbooks/sot-updater.yml index 239f12d..fe20a16 100644 --- a/files/ansible-playbooks/sot-updater.yml +++ b/files/ansible-playbooks/sot-updater.yml @@ -1,7 +1,6 @@ ---- -# sot-updater.yml — Read fw on Dev1 + Dev2, publish to NetBox via Rabbit (telemetry mode) +# 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) +- name: Read fw on Dev1 + Dev2, publish NetBox custom fields (full base, AIRPINGs, soft-fail telemetry, no upgrade scheduling) hosts: all gather_facts: no @@ -37,6 +36,9 @@ rmq_exchange: "{{ lookup('env','RMQ_EXCHANGE') | default('controls', true) }}" control_queue: "{{ lookup('env','CONTROLQUEUE') | default('queue_controls', true) }}" + # --- 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 @@ -44,7 +46,7 @@ path: "/opt/containers/ansible-worker/app/nb_onedevice_update.py" register: nb_script - - name: "NB preflight | Abort softly if script missing (path typo?)" + - name: "NB preflight | Abort softly if script missing" when: not nb_script.stat.exists delegate_to: localhost ansible.builtin.debug: @@ -52,7 +54,7 @@ - "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 }} (chatty)" + - name: "NB preflight | Run nb_onedevice_update.py for {{ inventory_hostname }}" when: nb_script.stat.exists delegate_to: localhost environment: @@ -70,7 +72,7 @@ - name: "NB preflight | Show results" when: nb_script.stat.exists delegate_to: localhost - ansible.builtin.debug: + debug: msg: - "rc={{ nb_preflight.rc }}" - "stdout_lines:" @@ -79,8 +81,58 @@ - "{{ (nb_preflight.stderr_lines | default([''])) }}" - "raw stdout (joined): {{ nb_preflight.stdout | default('') | trim }}" - # --- Parse the OK line robustly (token-based) --- - - name: "NB preflight | Parse OK line (token)" + # --- Detect cloud failure (rc==3 or FAIL Cloud line) --- + - name: "NB preflight | Detect cloud failure" + when: nb_script.stat.exists + delegate_to: localhost + vars: + _lines: >- + {{ (nb_preflight.stdout | default('') | regex_replace('\r','')) | split('\n') | map('trim') | list }} + _cloud_fail_line: >- + {{ (_lines | select('match', '^FAIL\\s+Cloud\\b') | list | last | default('')) }} + _rc_is_3: "{{ (nb_preflight.rc | default(1)) | int == 3 }}" + set_fact: + cloud_fail_line: "{{ _cloud_fail_line }}" + cloud_bad: "{{ _rc_is_3 or (( _cloud_fail_line | length ) > 0) }}" + + # --- 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 }})" + + # --- Parse OK line & detect IP change --- + - name: "NB preflight | Parse OK line" when: nb_script.stat.exists delegate_to: localhost vars: @@ -100,17 +152,16 @@ ) }} 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: "{{ (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 }}" - # --- Detect "cloud vs NetBox (before update) was different" (regex-free, robust) - - name: "NB preflight | Detect whether IP changed (pre-update)" + - name: "NB preflight | Detect IP change" when: nb_script.stat.exists delegate_to: localhost vars: @@ -124,40 +175,22 @@ nb_ip_changed: "{{ reason != 'none' }}" nb_change_reason: "{{ reason }}" - # --- Detect cloud error (network/HTTP/500s) --- - - name: "NB preflight | Detect cloud error" - when: nb_script.stat.exists - delegate_to: localhost - vars: - out: "{{ (nb_preflight.stdout | default('') | regex_replace('\r','')) }}" - set_fact: - nb_cloud_error: "{{ (nb_preflight.rc | default(1)) != 0 or ('FAIL ' in out) }}" - nb_cloud_error_rc: "{{ nb_preflight.rc | default('n/a') }}" - - name: "NB preflight | Verdict" when: nb_script.stat.exists delegate_to: localhost - ansible.builtin.debug: + debug: msg: - - >- - NB preflight verdict: - {{ 'IP CHANGED (will requeue)' if (nb_ip_changed|default(false)) else - ('CLOUD ERROR (will requeue)' if (nb_cloud_error|default(false)) else - 'OK (continue)') }} - - "Reason (IP): {{ nb_change_reason | default('none') }}" - - "rc={{ nb_cloud_error_rc | default('n/a') }}" - - >- - First FAIL line (if any): - {{ - ((nb_preflight.stdout | default('')) | regex_replace('\r','')). - split('\n') | select('search','^FAIL ') | list | first | default('') - }} + - "Preflight verdict:" + - "Cloud fail: {{ cloud_bad|default(false) }}" + - "IP changed: {{ nb_ip_changed|default(false) }}" + - "Reason: {{ nb_change_reason|default('none') }}" - # --- If IP changed or cloud errored → publish a 3s delayed 'sot-updater' task and stop this host --- - - name: "NB preflight | Publish delayed requeue (3s) and stop" + # --- Requeue control: only 3 attempts, 300s apart --- + - name: "NB preflight | Publish delayed requeue (300s) if needed" when: - nb_script.stat.exists - - (nb_ip_changed | default(false)) or (nb_cloud_error | default(false)) + - (cloud_bad | default(false)) or (nb_ip_changed | default(false)) + - (requeue_attempt | int) < 3 delegate_to: localhost uri: url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/app/deviceconfig.delayed/publish" @@ -167,16 +200,18 @@ force_basic_auth: true status_code: 200 return_content: yes - headers: - content-type: "application/json" + headers: { content-type: "application/json" } body_format: json body: properties: content_type: "application/json" - headers: - x-delay: 3000 + headers: { x-delay: 300000 } # 300 seconds = 5 minutes routing_key: "deviceconfig" - payload: "{{ {'inscope_device': (ansible_hostname | default(inventory_hostname)), 'task_name': 'sot-updater'} | to_json }}" + payload: "{{ { + 'inscope_device': (ansible_hostname | default(inventory_hostname)), + 'task_name': 'sot-updater', + 'attempt': ((requeue_attempt | int) + 1) + } | to_json }}" payload_encoding: "string" register: rmq_requeue changed_when: false @@ -185,28 +220,27 @@ - name: "NB preflight | Log requeue publish response" when: - nb_script.stat.exists - - (nb_ip_changed | default(false)) or (nb_cloud_error | default(false)) + - ((cloud_bad | default(false)) or (nb_ip_changed | default(false))) + - (requeue_attempt | int) < 3 delegate_to: localhost - ansible.builtin.debug: + debug: msg: - - "RMQ publish URL: http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/app/deviceconfig.delayed/publish" - - "HTTP status: {{ rmq_requeue.status | default('unknown') }}" - - "Parsed JSON: {{ rmq_requeue.json | default('') }}" - - "Raw content: {{ rmq_requeue.content | default('') }}" + - "Requeue published (attempt {{ requeue_attempt+1 }})" + - "HTTP: {{ rmq_requeue.status|default('n/a') }}" + - "JSON: {{ rmq_requeue.json|default('') }}" - - name: "NB preflight | Stop further tasks for this host" + - name: "NB preflight | Stop host after requeue" when: - nb_script.stat.exists - - (nb_ip_changed | default(false)) or (nb_cloud_error | default(false)) + - ((cloud_bad | default(false)) or (nb_ip_changed | default(false))) meta: end_host - # --- If neither IP changed nor cloud error → optional 1s pause, then continue normally --- - - name: "NB preflight | Pause 1s" + - name: "NB preflight | Pause 1s if OK" when: - nb_script.stat.exists - - not ((nb_ip_changed | default(false)) or (nb_cloud_error | default(false))) + - not ((cloud_bad | default(false)) or (nb_ip_changed | default(false))) delegate_to: localhost - ansible.builtin.pause: + pause: seconds: 1 tasks: @@ -214,7 +248,7 @@ debug: msg: - "Device: {{ inventory_hostname }}" - - "Mode: report-only (soft-fail; publish journals on failures)" + - "Mode: report-only (soft-fail telemetry)" # ----------------------- Temp IP on DEV1 ----------------------- - name: Add temporary IP on DEV1 @@ -223,85 +257,75 @@ failed_when: false ignore_errors: true - # ---------------------------- Idempotent temp IP on DEV1 ---------------------------- - name: Add temporary IP on DEV1 (tolerate 'File exists') - ansible.builtin.raw: > + 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(''))) + and ('File exists' not in (add_ip.stdout|default(''))) + and ('File exists' not in (add_ip.stderr|default(''))) - - name: Debug result of adding temp IP to DEV1 - ansible.builtin.debug: + - name: Debug temp IP result + debug: msg: - - "add_ip.rc={{ add_ip.rc | default('') }}" - - "add_ip.stdout={{ (add_ip.stdout | default('')) | trim }}" - - "add_ip.stderr={{ (add_ip.stderr | default('')) | trim }}" + - "add_ip.rc={{ add_ip.rc|default('') }}" + - "add_ip.out={{ add_ip.stdout|default('')|trim }}" + - "add_ip.err={{ add_ip.stderr|default('')|trim }}" - # ---------------------------- Discover MAC via bridge FDB and add static ARP ---------------------------- - - name: Discover DEV2 MAC via bridge fdb on DEV1 (best-effort) - ansible.builtin.raw: > + - 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 DEV2 MAC (if any) - ansible.builtin.set_fact: - dev2_mac: "{{ (dev2_mac_scan.stdout | default('') ) | trim }}" - changed_when: false + - name: Capture discovered MAC + set_fact: + dev2_mac: "{{ dev2_mac_scan.stdout|default('')|trim }}" - - name: Clear existing ARP entry for DEV2 on DEV1 (best-effort) - ansible.builtin.raw: > + - name: Clear existing ARP entry + raw: > {{ pathprefix }} ip neigh del {{ dev2_host }} dev {{ dev1_iface }} 2>/dev/null || true - register: dev2_arp_del changed_when: false failed_when: false - - name: Add static ARP entry on DEV1 (locks DEV2 IP → discovered MAC) - when: dev2_mac is defined and dev2_mac | length > 0 - ansible.builtin.raw: > + - 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(''))) + and ('File exists' not in (dev2_arp_add.stdout|default(''))) + and ('File exists' not in (dev2_arp_add.stderr|default(''))) - - name: Debug ARP action summary on DEV1 - ansible.builtin.debug: + - 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 }}" + - "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 add (no MAC discovered) - when: dev2_mac is not defined or dev2_mac | length == 0 - ansible.builtin.debug: - msg: "No suitable dynamic MAC found via bridge fdb; skipping static ARP add on DEV1" + - name: Note skipping static ARP + when: dev2_mac|length == 0 + debug: + msg: "No MAC discovered; skipping static ARP add." - # ---------------------------- ARP refresh ---------------------------- - - name: Refresh ARP #1 - ansible.builtin.raw: "{{ pathprefix }} arping -U -I {{ arping_iface }} {{ dev2_side_ip_addr }} -c 3" - failed_when: false - ignore_errors: true - - - name: Refresh ARP #1 + - name: Refresh ARP raw: "{{ pathprefix }} arping -U -I {{ arping_iface }} {{ dev2_side_ip_addr }} -c 3" failed_when: false ignore_errors: true - # ----------------------- Local tunnel prep ----------------------- - - name: Pick a free local TCP port for the tunnel + # ----------------------- Tunnel setup ----------------------- + - name: Pick free local port delegate_to: localhost shell: | for i in $(seq 1 50); do @@ -312,32 +336,25 @@ register: pick_port failed_when: false - - name: Record chosen port and create control dir + - name: Create control dir delegate_to: localhost shell: "mktemp -d" register: mktemp_dir failed_when: false - - name: Set facts for tunnel paths + - 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" + _local_port: "{{ pick_port.stdout|default('')|trim }}" + _ctrl_dir: "{{ mktemp_dir.stdout|default('')|trim }}" + _ctrl_sock: "{{ mktemp_dir.stdout|default('')|trim }}/ssh_tunnel_ctl" - # ----------------------- AIRPING #2 ----------------------- - - name: Refresh ARP #2 - raw: "{{ pathprefix }} arping -U -I {{ arping_iface }} {{ dev2_side_ip_addr }} -c 3" - failed_when: false - ignore_errors: true - - # ----------------------- Start tunnel ----------------------- - - name: Start SSH tunnel via DEV1 + - name: Start SSH tunnel delegate_to: localhost shell: | set -e USER="{{ dev1_user }}" - HOST="{{ ansible_host | default(inventory_hostname) }}" + 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 }}" \ @@ -358,21 +375,21 @@ set_fact: tunnel_ok: "{{ nc_probe.rc == 0 }}" - # ----------------------- DEV1 banner ----------------------- + # ----------------------- 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) }}" \ + "{{ 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 → X.X.X-rYYYY (POSIX tools) + - name: Dev1 | Normalize banner delegate_to: localhost shell: | - printf '%s\n' "{{ dev1_banner.stdout | trim }}" \ + 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 @@ -382,17 +399,12 @@ - name: Dev1 | Set final fw string delegate_to: localhost set_fact: - dev1_fw_clean: "{{ dev1_fw_clean_cmd.stdout | trim }}" - - - name: Debug Dev1 normalized firmware - delegate_to: localhost - debug: - msg: "Dev1 fw_version {{ dev1_fw_clean | default('N/A') }}" + dev1_fw_clean: "{{ dev1_fw_clean_cmd.stdout|trim }}" - name: Publish Dev1 fw_version delegate_to: localhost uri: - url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish" + url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost|urlencode }}/{{ rmq_exchange|urlencode }}/publish" method: POST user: "{{ rmq_user }}" password: "{{ rmq_pass }}" @@ -404,7 +416,7 @@ properties: { content_type: "application/json" } routing_key: "{{ control_queue }}" payload: "{{ { - 'inscope_device': (ansible_hostname | default(inventory_hostname)), + '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') @@ -412,9 +424,9 @@ payload_encoding: "string" changed_when: false - # ----------------------- DEV2 auth ----------------------- + # ----------------------- Dev2 auth + firmux ----------------------- - name: Try DEV2 login - when: tunnel_ok | default(false) + when: tunnel_ok|default(false) delegate_to: localhost shell: | for f in basicpass basicpass2; do @@ -432,17 +444,10 @@ - name: Set dev2_passfile_used delegate_to: localhost set_fact: - dev2_passfile_used: "{{ (dev2_auth.rc == 0) | ternary(dev2_auth.stdout | trim, 'NONE') }}" + dev2_passfile_used: "{{ (dev2_auth.rc == 0)|ternary(dev2_auth.stdout|trim,'NONE') }}" - # ----------------------- AIRPING #3 ----------------------- - - name: Refresh ARP #3 - raw: "{{ pathprefix }} arping -U -I {{ arping_iface }} {{ dev2_side_ip_addr }} -c 3" - failed_when: false - ignore_errors: true - - # ----------------------- DEV2 firmux (simplified, literal) ----------------------- - - name: Dev2 | Read /usr/lib/release/firmux - when: tunnel_ok | default(false) and dev2_passfile_used != 'NONE' + - name: Dev2 | Read firmux + when: tunnel_ok|default(false) and dev2_passfile_used != 'NONE' delegate_to: localhost shell: | PORT="{{ _local_port }}" @@ -455,38 +460,26 @@ failed_when: false ignore_errors: true - - name: Dev2 | Extract firmware version line (prefer 'rev', else first non-empty) + - name: Dev2 | Extract firmware version line delegate_to: localhost run_once: true set_fact: indoor_fw_norm: >- {{ ( - ( - dev2_firmux.stdout | default('') | regex_replace('\r','') - ) | split('\n') - | map('trim') - | select('truthy') - | list - ) | select('match', '(?i).*\\brev\\s*[0-9]+.*') - | list - | first - | default( - ((dev2_firmux.stdout | default('') | regex_replace('\r','')) - | split('\n') - | map('trim') - | select('truthy') - | list - | first - | default('')) - ) - | trim + (dev2_firmux.stdout|default('')|regex_replace('\r',''))|split('\n') + | map('trim') | select('truthy') | list + ) | select('match','(?i).*\\brev\\s*[0-9]+.*') + | list | first | default( + ((dev2_firmux.stdout|default('')|regex_replace('\r','')) + | split('\n')|map('trim')|select('truthy')|list|first|default('')) + ) | trim }} - name: Publish Dev2 indoor_fwver delegate_to: localhost uri: - url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish" + url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost|urlencode }}/{{ rmq_exchange|urlencode }}/publish" method: POST user: "{{ rmq_user }}" password: "{{ rmq_pass }}" @@ -498,7 +491,7 @@ properties: { content_type: "application/json" } routing_key: "{{ control_queue }}" payload: "{{ { - 'inscope_device': (ansible_hostname | default(inventory_hostname)), + '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') @@ -511,21 +504,21 @@ debug: msg: "Closing tunnel and removing temporary IP" - - name: Refresh ARP #4 + - name: Refresh ARP (cleanup) 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" + shell: "ssh -S '{{ _ctrl_sock|default('/dev/null') }}' -O exit 2>/dev/null || true" failed_when: false ignore_errors: true - - name: Remove tunnel control dir + - name: Remove control dir delegate_to: localhost file: - path: "{{ _ctrl_dir | default('/tmp/none') }}" + path: "{{ _ctrl_dir|default('/tmp/none') }}" state: absent ignore_errors: true @@ -538,4 +531,4 @@ debug: msg: - "Device: {{ inventory_hostname }}" - - "Status: DONE" + - "Status: DONE (telemetry only)"