--- - name: Deploy WiFi debug (single device, linear) hosts: all gather_facts: no vars: remote_syslog_ip: "102.38.125.161" # Program version (0) wdbg_version: "v17" ssh_user: "{{ ansible_user | default('root') }}" ssh_pass: "{{ ansible_password | default(ansible_ssh_pass) }}" # RabbitMQ (use controls exchange + queue_controls like the reference) (e) 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','CONTROL_QUEUE') | default('queue_controls', true) }}" tasks: - block: # --- SSH reachability check --- - name: Check SSH connectivity (raw ping) raw: "echo ping" register: ping_result ignore_errors: true - block: ############ step 1 - name: Check if crontab launch is already present in sysstart.lua raw: | grep -F 'crond -f' /usr/share/config/sysstart.lua >/dev/null 2>&1 && echo PRESENT || echo ABSENT register: sysstart_cron_check changed_when: false failed_when: false - name: Announce presence and skip edits when: (sysstart_cron_check.stdout | trim) == 'PRESENT' debug: msg: "crontab is present in sysstart.lua; skipping download/edit/upload." - name: Fetch sysstart.lua via scp (password auth) when: (sysstart_cron_check.stdout | trim) != 'PRESENT' delegate_to: localhost command: > sshpass -p {{ ssh_pass | quote }} scp -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null {{ ssh_user }}@{{ ansible_host }}:/usr/share/config/sysstart.lua /tmp/{{ inventory_hostname }}_sysstart.lua register: scp_get retries: 3 delay: 2 until: scp_get.rc == 0 - name: Edit the file locally when: (sysstart_cron_check.stdout | trim) != 'PRESENT' delegate_to: localhost lineinfile: path: "/tmp/{{ inventory_hostname }}_sysstart.lua" line: 'launchd.set_process("crond", "/usr/sbin/crond -f")' insertbefore: '^tasks\.start\(ctx,\s*"finished"\)' - name: Push the file back (write to -2) when: (sysstart_cron_check.stdout | trim) != 'PRESENT' delegate_to: localhost command: > sshpass -p {{ ssh_pass | quote }} scp -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null /tmp/{{ inventory_hostname }}_sysstart.lua {{ ssh_user }}@{{ ansible_host }}:/usr/share/config/sysstart.lua register: scp_put retries: 3 delay: 2 until: scp_put.rc == 0 ############ step 2 - name: Compute MD5 of local wifidebug.sh delegate_to: localhost command: md5sum files/wifidebug.sh register: md5_local_wdbg changed_when: false - name: Compute MD5 of remote /root/wifidebug.sh raw: "md5sum /root/wifidebug.sh || busybox md5sum /root/wifidebug.sh" register: md5_remote_wdbg changed_when: false failed_when: false - name: Decide if wifidebug.sh needs upload set_fact: upload_wdbg: >- {{ (md5_remote_wdbg.rc != 0) or ((md5_local_wdbg.stdout.split()[0]) != (md5_remote_wdbg.stdout.split()[0] if (md5_remote_wdbg.stdout is defined) else '')) }} - name: Upload wifidebug.sh via scp (overwrite if changed) when: upload_wdbg | bool delegate_to: localhost command: > sshpass -p {{ ssh_pass | quote }} scp -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null files/wifidebug.sh {{ ssh_user }}@{{ ansible_host }}:/root/wifidebug.sh register: scp_wifidebug retries: 3 delay: 2 until: scp_wifidebug.rc == 0 - name: Ensure /root/wifidebug.sh is executable and owned by root raw: | chown root:root /root/wifidebug.sh && chmod 0755 /root/wifidebug.sh ############ step 3 - name: Ensure /etc/crontabs/root exists (touch with perms) raw: | if [ ! -f /etc/crontabs/root ]; then touch /etc/crontabs/root fi chown root:root /etc/crontabs/root chmod 0644 /etc/crontabs/root - name: Check if cron line already present raw: | grep -Fxq '*/1 * * * * /root/wifidebug.sh' /etc/crontabs/root register: cron_grep failed_when: false changed_when: false - name: Upload snippet crond-root to /tmp (only if missing) when: cron_grep.rc != 0 delegate_to: localhost command: > sshpass -p {{ ssh_pass | quote }} scp -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null files/crond-root {{ ssh_user }}@{{ ansible_host }}:/tmp/crond-root.snippet - name: Append snippet to /etc/crontabs/root (only if missing) when: cron_grep.rc != 0 raw: | cat /tmp/crond-root.snippet >> /etc/crontabs/root && rm -f /tmp/crond-root.snippet register: cron_append changed_when: true ############ step 4 - name: Skip config.json edit if remote_syslog already matches raw: | EN_OK=$(cat /tmp/config.json | grep -A9 -m1 'remote_syslog' | grep 'enabled' | grep true | wc -l || echo 0) SRV_OK=$(cat /tmp/config.json | grep -A9 -m1 'remote_syslog' | grep 'server' | grep '{{ remote_syslog_ip }}' | wc -l || echo 0) if [ "$EN_OK" -eq 1 ] && [ "$SRV_OK" -eq 1 ]; then echo "SKIP" else echo "EDIT" fi register: remote_syslog_check changed_when: false - name: Set do_edit flag from skip check set_fact: do_edit: "{{ (remote_syslog_check.stdout | default('EDIT')) | trim != 'SKIP' }}" # ===== ADDED: pre-download MD5 on device to guard download integrity ===== - name: MD5 of remote /tmp/config.json (pre-download) when: do_edit raw: "md5sum /tmp/config.json || busybox md5sum /tmp/config.json" register: md5_remote_src changed_when: false # ===== END ADDED ===== - name: Fetch /tmp/config.json to controller when: do_edit delegate_to: localhost command: > sshpass -p {{ ssh_pass | quote }} scp -q -o StrictHostKeyChecking=no {{ ssh_user }}@{{ ansible_host }}:/tmp/config.json /tmp/{{ inventory_hostname }}_config.json # ===== ADDED: verify download integrity by comparing MD5s ===== - name: MD5 of local downloaded config.json when: do_edit delegate_to: localhost command: md5sum "/tmp/{{ inventory_hostname }}_config.json" register: md5_local_src changed_when: false - name: Fail if MD5 mismatch on downloaded config.json when: - do_edit - (md5_local_src.stdout.split()[0]) != (md5_remote_src.stdout.split()[0] if (md5_remote_src.stdout is defined) else 'BAD') fail: msg: "MD5 mismatch on downloaded /tmp/config.json — aborting (download integrity check failed)." # ===== END ADDED ===== - name: Strict in-place style-preserving edit (services.remote_syslog only) when: do_edit delegate_to: localhost shell: | awk ' BEGIN { in_services=0; in_rs=0; depth=0; done_enabled=0; done_server=0 } { line = $0 if (!in_services && $0 ~ /"services"[[:space:]]*:/) { in_services=1 } if (in_services && !in_rs && $0 ~ /"remote_syslog"[[:space:]]*:/) { in_rs=1; depth=0 } if (in_rs) { if (!done_enabled && line ~ /^[[:space:]]*"enabled"[[:space:]]*:[[:space:]]*false([[:space:]]*,?[[:space:]]*)$/) { line = gensub(/^([[:space:]]*"enabled"[[:space:]]*:[[:space:]]*)false([[:space:]]*,?[[:space:]]*)$/, "\\1true\\2", 1, line) done_enabled=1 } if (!done_server && line ~ /^[[:space:]]*"server"[[:space:]]*:[[:space:]]*""([[:space:]]*,?[[:space:]]*)$/) { line = gensub(/^([[:space:]]*"server"[[:space:]]*:[[:space:]]*)""([[:space:]]*,?[[:space:]]*)$/, "\\1\"102.38.125.161\"\\2", 1, line) done_server=1 } } print line if (in_rs) { opens = gsub(/{/, "{", $0) closes = gsub(/}/, "}", $0) depth += (opens - closes) if (depth <= 0 && $0 ~ /}/) { in_rs=0 } } } ' "/tmp/{{ inventory_hostname }}_config.json" \ > "/tmp/{{ inventory_hostname }}_config.json.new" args: executable: /bin/bash # ===== ADDED: controller-side JSON sanity check (jq) ===== - name: Controller JSON sanity check (jq) when: do_edit delegate_to: localhost shell: jq empty "/tmp/{{ inventory_hostname }}_config.json.new" args: executable: /bin/bash register: jq_local changed_when: false failed_when: jq_local.rc != 0 # ===== END ADDED ===== - name: Push edited config.json.new to remote temp when: do_edit delegate_to: localhost shell: | sshpass -p {{ ssh_pass | quote }} scp -q -o StrictHostKeyChecking=no \ "/tmp/{{ inventory_hostname }}_config.json.new" \ {{ ssh_user }}@{{ ansible_host }}:/tmp/config.json.new args: executable: /bin/bash - name: Compute MD5 of local .new when: do_edit delegate_to: localhost command: md5sum "/tmp/{{ inventory_hostname }}_config.json.new" register: md5_local changed_when: false - name: Compute MD5 of remote .new (no Python) when: do_edit raw: "md5sum /tmp/config.json.new || busybox md5sum /tmp/config.json.new" register: md5_remote changed_when: false - name: Fail if MD5 mismatch when: - do_edit - (md5_local.stdout.split()[0]) != (md5_remote.stdout.split()[0] if (md5_remote.stdout is defined) else 'BAD') fail: msg: "MD5 mismatch between controller and remote copy — aborting replace." - name: Commit new config.json (overwrite original) when: do_edit raw: | cp -a /tmp/config.json /tmp/config.json.bak.$(date +%Y%m%d%H%M%S) mv /tmp/config.json.new /tmp/config.json - name: Apply config to runtime (sysconf -w) when: do_edit raw: | sysconf -w || /usr/sbin/sysconf -w register: sysconf_result changed_when: true failed_when: sysconf_result.rc not in [0] # (d) Pre-restart notice to control queue - name: Build pre-restart journal (explain why) when: do_edit delegate_to: localhost set_fact: prereboot_payload: inscope_device: "{{ ansible_hostname | default(inventory_hostname) }}" task_name: "journal_add" task_result: >- wifidebug: will restart services because remote_syslog settings were updated and applied (sysconf -w). - name: Publish pre-restart journal to control queue when: do_edit 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: "{{ prereboot_payload | to_json }}" payload_encoding: "string" register: rmq_preboot changed_when: (rmq_preboot.json is defined) and (rmq_preboot.json.routed | default(false) | bool) # ======== SURGICAL CHANGE: restart kick + verification like multissidfix ======== - name: Trigger full restart (system-stop; system-start) from controller with 10s cap when: do_edit delegate_to: localhost shell: "timeout 10s sshpass -p {{ ssh_pass | quote }} ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ServerAliveInterval=2 -o ServerAliveCountMax=1 {{ ssh_user }}@{{ ansible_host }} '/usr/sbin/system-stop ; sleep 1 ; /usr/sbin/system-start'" args: executable: /bin/bash register: restart_kick changed_when: true failed_when: false # probe SSH with nc: up to 24 tries, 5s each (nc timeout -w 3) - name: Probe SSH with nc (24 tries, 5s each) when: do_edit delegate_to: localhost shell: "nc -z -w 3 {{ ansible_host }} 22" register: nc_probe retries: 24 delay: 5 until: nc_probe.rc == 0 changed_when: false failed_when: false - name: Set ssh_up fact (from nc probe) when: do_edit delegate_to: localhost set_fact: ssh_up: "{{ (nc_probe.rc | default(1)) == 0 }}" - name: Fail if SSH did not return after restart when: - do_edit - not ssh_up | bool fail: msg: "wifidebug: restart issued; SSH did not return after 24 x 5s checks." # ======== END OF SURGICAL CHANGE ======== - name: Set result status (success deployed or no change) set_fact: result_status: "{{ 'SUCCESS_DEPLOYED' if (upload_wdbg | bool) else 'SUCCESS_NO_CHANGE' }}" when: ping_result is succeeded - name: Set status fact (no ssh) when: ping_result is failed set_fact: result_status: "NO_SSH" rescue: - name: Mark result as failed set_fact: result_status: "FAILED during {{ ansible_failed_task.name }}" always: - name: Compute inscope device set_fact: inscope_device_name: "{{ ansible_hostname | default(inventory_hostname) }}" # (a) Set custom field wifidebug -> v15 on control queue - name: Build custom-field payload (wifidebug -> version) delegate_to: localhost set_fact: wdbg_cf_payload: inscope_device: "{{ inscope_device_name }}" task_name: "deploy_wifidebug" task_result: "{{ wdbg_version }}" - name: Publish custom-field update to control queue 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: "{{ wdbg_cf_payload | to_json }}" payload_encoding: "string" register: rmq_cf changed_when: (rmq_cf.json is defined) and (rmq_cf.json.routed | default(false) | bool) # (b)(c) Final wrap-up journal "wifidebug: ..." with actions performed - name: Build actions list set_fact: _actions_list: >- {{ [] + ((upload_wdbg | default(false) | bool) | ternary(['uploaded wifidebug.sh'], [])) + (((cron_grep is defined) and ((cron_grep.rc | default(0)) != 0)) | ternary(['added crontab entry'], [])) + ((((sysstart_cron_check.stdout | default('PRESENT')) | trim) != 'PRESENT') | ternary(['updated sysstart.lua (crond launch)'], [])) + ((do_edit | default(false) | bool) | ternary(['updated remote_syslog + applied config (sysconf -w)'], [])) + ((do_edit | default(false) | bool) | ternary(['restarted services'], [])) }} - name: Build actions string set_fact: _actions_str: "{{ ((_actions_list | default([])) | length > 0) | ternary((_actions_list | join(', ')), 'no changes needed') }}" - name: Build wrap-up journal payload delegate_to: localhost set_fact: wrap_payload: inscope_device: "{{ inscope_device_name }}" task_name: "journal_add" task_result: >- wifidebug: {{ 'success' if (result_status == 'SUCCESS_DEPLOYED' or result_status == 'SUCCESS_NO_CHANGE') else result_status | lower }} — actions: {{ _actions_str }} - name: Publish wrap-up journal to control queue 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: "{{ wrap_payload | to_json }}" payload_encoding: "string" register: rmq_wrap changed_when: (rmq_wrap.json is defined) and (rmq_wrap.json.routed | default(false) | bool) # Local summary (kept for operator visibility) - name: Summary debug: msg: - "result_status: {{ result_status }}" - "we're good"