324 lines
12 KiB
YAML
324 lines
12 KiB
YAML
---
|
||
# 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.
|
||
# — Pavel’s rule: on-device commands remain immutable; we’re 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('')))
|