Files
ansible-worker/files/ansible-playbooks/sot-updater.yml
2025-10-24 09:30:56 +03:00

324 lines
12 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
# updater.yml — read Dev1 & Dev2 firmware versions and publish to controls → netbox-reporter
# Run: nbplay updater.yml <device>
- name: Read fw on Dev1 + Dev2, publish NetBox custom fields
hosts: all
gather_facts: no
vars:
# Busybox-safe PATH prefix for raw calls on DEV1 (as in update-rebootin222)
pathprefix: "PATH=/sbin:/usr/sbin:/bin:/usr/bin:$PATH; "
# DEV1 credentials (as in update-indoor)
dev1_user: "root"
dev1_pass: "wavewave"
# Tunnel target DEV2 behind DEV1 (as in update-indoor)
dev2_host: "192.168.1.1"
dev2_port: 22
dev2_ssh_user: "root"
dev2_side_ip: "192.168.1.11/24"
dev1_iface: "br-wan"
# Password files sequence (as in update-indoor: try basicpass then basicpass2)
# NOTE: The selection below mirrors the exact “try #1 → select → try #2 → select → NONE” flow from update-indoor.
# Do not alter ordering.
# (Values are file names as used in your repo/environment.)
# We do not loop; we replicate the same task structure.
# The read of firmux uses whichever got selected.
# — Pavels rule: on-device commands remain immutable; were only orchestrating controller-side steps here.
# Rabbit journaling (mirrors update-indoor / rebootin222)
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) }}"
tasks:
# ----------------------------- DEV1: read current banner (exactly like update-rebootin222) -----------------------------
- name: Dev1 | Read first /etc/banner line containing 'rev'
ansible.builtin.raw: "{{ pathprefix }} cat /etc/banner | grep -i rev | head -n1"
register: dev1_banner
changed_when: false
- name: Dev1 | Extract 'X.Y.Z rev NNNN' from banner (no timestamp)
delegate_to: localhost
set_fact:
dev1_fw_clean: >-
{{
(dev1_banner.stdout | default('') | trim)
| regex_replace('^(.+?[Rr][Ee][Vv]\\.??\\s*[0-9]+).*$', '\\1')
}}
- name: Publish → controls | custom_field_set fw_version (Dev1)
delegate_to: localhost
ansible.builtin.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
} | to_json }}"
payload_encoding: "string"
changed_when: false
# ----------------------------- TUNNEL PREP (update-indoor blocks copied) -----------------------------
- name: Add temporary IP on DEV1 (tolerate 'File exists')
ansible.builtin.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: Pick a free local TCP port for the tunnel (controller)
delegate_to: localhost
ansible.builtin.shell: |
set -e
pick() {
for i in $(seq 1 25); do
p="$(shuf -i 20000-39999 -n 1)"
if command -v ss >/dev/null 2>&1; then
if ! ss -ltn | awk '{print $4}' | grep -qE "(:|\.)${p}$"; then
echo "$p"; return 0
fi
else
if ! nc -z 127.0.0.1 "$p" >/dev/null 2>&1; then
echo "$p"; return 0
fi
fi
done
return 1
}
pick
register: pick_port
changed_when: false
- name: Stop if no free local port was found
ansible.builtin.meta: end_host
when: (pick_port.stdout | trim | length) == 0
- name: Record chosen local port
delegate_to: localhost
set_fact:
_local_port: "{{ pick_port.stdout | trim }}"
- name: Create ControlMaster socket dir (mktemp)
delegate_to: localhost
set_fact:
_ctrl_dir: "{{ lookup('pipe', 'mktemp -d') }}"
- name: Compose ControlMaster socket path
delegate_to: localhost
set_fact:
_ctrl_sock: "{{ _ctrl_dir }}/ssh_tunnel_ctl"
- name: Start SSH ControlMaster and forward 127.0.0.1:local → DEV2:22 via DEV1
delegate_to: localhost
ansible.builtin.shell: |
set -e
USER="{{ dev1_user }}"
HOST="{{ ansible_host | default(inventory_hostname) }}"
sshpass -p '{{ dev1_pass }}' ssh -f -N \
-o PreferredAuthentications=password -o PubkeyAuthentication=no \
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o NumberOfPasswordPrompts=1 -o ConnectTimeout=30 \
-M -S "{{ _ctrl_sock }}" \
-L "127.0.0.1:{{ _local_port }}:{{ dev2_host }}:{{ dev2_port }}" \
"${USER}@${HOST}"
args: { executable: /bin/bash }
register: start_tunnel
changed_when: true
- name: Probe TCP reachability to DEV2 through the tunnel
delegate_to: localhost
ansible.builtin.shell: "nc -z -w5 127.0.0.1 {{ _local_port }}"
register: nc_probe
changed_when: false
ignore_errors: true
- name: Stop if tunnel TCP probe failed
ansible.builtin.meta: end_host
when: nc_probe.rc != 0
# ----------------------------- DEV2 AUTH PICK (exact task sequence from update-indoor) -----------------------------
- name: Try DEV2 login with 'basicpass' (root)
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f basicpass ssh \
-o AddressFamily=inet \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=30 \
-p "$PORT" root@127.0.0.1 echo OK >/dev/null 2>&1
register: dev2_try_basicpass
changed_when: false
ignore_errors: true
- name: Select 'basicpass' if previous login succeeded
when: dev2_try_basicpass.rc == 0
delegate_to: localhost
ansible.builtin.set_fact:
dev2_passfile_used: "basicpass"
changed_when: false
- name: Try DEV2 login with 'basicpass2' (only if first failed)
when: dev2_passfile_used is not defined
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f basicpass2 ssh \
-o AddressFamily=inet \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=30 \
-p "$PORT" root@127.0.0.1 echo OK >/dev/null 2>&1
register: dev2_try_basicpass2
changed_when: false
ignore_errors: true
- name: Select 'basicpass2' if previous login succeeded
when: dev2_passfile_used is not defined and dev2_try_basicpass2.rc == 0
delegate_to: localhost
ansible.builtin.set_fact:
dev2_passfile_used: "basicpass2"
changed_when: false
- name: Mark DEV2 auth as NONE if both attempts failed
when: dev2_passfile_used is not defined
delegate_to: localhost
ansible.builtin.set_fact:
dev2_passfile_used: "NONE"
changed_when: false
# ----------------------------- DEV2 firmware read (update-indoor firmux primary check) -----------------------------
- name: Dev2 | Read /usr/lib/release/firmux (if present)
when: dev2_passfile_used != "NONE"
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f "{{ dev2_passfile_used }}" ssh \
-o AddressFamily=inet \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=30 \
-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
ignore_errors: true
- name: Dev2 | Fallback to banner 'rev' if firmux not available
when: dev2_passfile_used != "NONE" and ((dev2_firmux.stdout | default('') | trim) | length == 0)
delegate_to: localhost
ansible.builtin.shell: |
set -e
PORT="{{ _local_port }}"
sshpass -f "{{ dev2_passfile_used }}" ssh \
-o AddressFamily=inet \
-o StrictHostKeyChecking=no -o PubkeyAuthentication=no \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1 \
-o ConnectTimeout=30 \
-p "$PORT" "{{ dev2_ssh_user }}@127.0.0.1" \
"grep -i rev /etc/banner 2>/dev/null | head -n1 || true"
args: { executable: /bin/bash }
register: dev2_banner
changed_when: false
ignore_errors: true
- name: Dev2 | Choose raw string (firmux preferred, else banner)
delegate_to: localhost
ansible.builtin.set_fact:
indoor_fw_raw: >-
{{
(dev2_firmux.stdout | default('') | trim)
if ((dev2_firmux.stdout | default('') | trim) | length > 0)
else (dev2_banner.stdout | default('') | trim)
}}
- name: Dev2 | Normalize to X.Y.Z-rNNNN (convert trailing 'rev NNNN' → '-rNNNN')
delegate_to: localhost
ansible.builtin.set_fact:
indoor_fw_norm: >-
{{
((indoor_fw_raw | default('') | trim | lower) is search('-r[0-9]+$'))
| ternary(
(indoor_fw_raw | default('') | trim),
((indoor_fw_raw | default('') | trim) | regex_replace('\\s*[Rr][Ee][Vv]\\.??\\s*([0-9]+)\\s*$', '-r\\1'))
)
}}
- name: Publish → controls | custom_field_set indoor_fwver (Dev2)
when:
- dev2_passfile_used != "NONE"
- indoor_fw_norm is defined
- (indoor_fw_norm | length) > 0
delegate_to: localhost
ansible.builtin.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
} | to_json }}"
payload_encoding: "string"
changed_when: false
post_tasks:
- name: Close SSH ControlMaster (best-effort)
delegate_to: localhost
ansible.builtin.shell: "ssh -S '{{ _ctrl_sock | default('/dev/null') }}' -O exit 2>/dev/null || true"
changed_when: false
ignore_errors: true
- name: Remove tunnel control dir (best-effort)
delegate_to: localhost
ansible.builtin.file:
path: "{{ _ctrl_dir | default('/tmp/none') }}"
state: absent
ignore_errors: true
- name: Remove temporary IP on DEV1 (tolerate 'Cannot assign requested address')
ansible.builtin.raw: >
{{ pathprefix }}
ip a del {{ dev2_side_ip }} dev {{ dev1_iface }}
register: del_ip
changed_when: del_ip.rc == 0
failed_when: del_ip.rc != 0
and ('Cannot assign requested address' not in (del_ip.stdout | default('')))
and ('Cannot assign requested address' not in (del_ip.stderr | default('')))