This commit is contained in:
2026-02-06 10:36:31 +02:00
parent 73634180f2
commit bd1d536bf9
2 changed files with 854 additions and 0 deletions

View File

@@ -0,0 +1,198 @@
---
- name: Deploy connstats (single device, linear)
hosts: all
gather_facts: no
vars:
ssh_user: "{{ ansible_user | default('root') }}"
ssh_pass: "{{ ansible_password | default(ansible_ssh_pass) }}"
# RabbitMQ (use controls exchange + queue_controls like the reference)
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) }}"
# Cron line we must ensure (preserve any other lines)
connstats_cron_line: "*/10 * * * * /root/connstats.sh --debug --always-find-offset >/dev/null 2>&1"
tasks:
- block:
- name: Check SSH connectivity (raw ping)
raw: "echo ping"
register: ping_result
ignore_errors: true
- block:
# Step 2 fork
- name: Compute MD5 of local connstats.sh
delegate_to: localhost
command: md5sum files/connstats.sh
register: md5_local_cstats
changed_when: false
- name: Compute MD5 of remote /root/connstats.sh
raw: "md5sum /root/connstats.sh || busybox md5sum /root/connstats.sh"
register: md5_remote_cstats
changed_when: false
failed_when: false
- name: Decide if connstats.sh needs upload
set_fact:
upload_cstats: >-
{{ (md5_remote_cstats.rc != 0)
or ((md5_local_cstats.stdout.split()[0])
!= (md5_remote_cstats.stdout.split()[0] if (md5_remote_cstats.stdout is defined) else '')) }}
- name: Upload connstats.sh via scp (overwrite if changed)
when: upload_cstats | bool
delegate_to: localhost
command: >
sshpass -p {{ ssh_pass | quote }}
scp -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null
files/connstats.sh
{{ ssh_user }}@{{ ansible_host }}:/root/connstats.sh
register: scp_connstats
retries: 3
delay: 2
until: scp_connstats.rc == 0
- name: Ensure /root/connstats.sh is executable and owned by root
raw: |
chown root:root /root/connstats.sh && chmod 0755 /root/connstats.sh
# Step 3 fork (cron entry only, preserve anything else)
- 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 connstats cron line already present
raw: |
grep -Fxq '{{ connstats_cron_line }}' /etc/crontabs/root
register: cron_grep
failed_when: false
changed_when: false
- name: Upload snippet connstats-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/connstats-crond-root
{{ ssh_user }}@{{ ansible_host }}:/tmp/connstats-crond-root.snippet
- name: Append snippet to /etc/crontabs/root (only if missing)
when: cron_grep.rc != 0
raw: |
cat /tmp/connstats-crond-root.snippet >> /etc/crontabs/root && rm -f /tmp/connstats-crond-root.snippet
register: cron_append
changed_when: true
- name: Set result status (success deployed or no change)
set_fact:
result_status: "{{ 'SUCCESS_DEPLOYED' if (upload_cstats | 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) }}"
# Custom field update (per your sample)
- name: Publish custom-field update connstats deployed 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: "{{ {
'inscope_device': inscope_device_name,
'task_name': 'custom_field_set',
'task_add1': 'connstats',
'task_result': 'deployed'
} | to_json }}"
payload_encoding: "string"
register: rmq_cf
changed_when: false
# Final wrap-up journal connstats
- name: Build actions list
set_fact:
_actions_list: >-
{{
[]
+ ((upload_cstats | default(false) | bool) | ternary(['uploaded connstats.sh'], []))
+ (((cron_grep is defined) and ((cron_grep.rc | default(0)) != 0)) | ternary(['added connstats crontab entry'], []))
}}
- 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: >-
connstats: {{ '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)
- name: Summary
debug:
msg:
- "result_status: {{ result_status }}"
- "we're good"

656
files/files/connstats.sh Normal file
View File

@@ -0,0 +1,656 @@
#!/bin/sh
# connstats.sh
# Log-derived Wi-Fi connection counters per run (intended for cron every 10 minutes).
# Writes one summary line per run to /root/connstats.log
# Optional: --debug for chatty troubleshooting output in the same log.
# Optional: --always-find-offset to calibrate syslog header time via a logger marker
# and (on fresh start / rotation / truncation) build a true last-10-min slice.
LOG_SRC="/var/log/messages"
OUT_LOG="/root/connstats.log"
STATE="/tmp/connstats.state"
TMPDIR="/tmp"
# How many tail lines to scan when we have no state (first run / after reboot / rotation)
BOOTSTRAP_BACKFILL_LINES=2000
# Used only with --always-find-offset during bootstrap-like runs
BOOTSTRAP_TIME_WINDOW_SEC=600
BOOTSTRAP_TIME_TAIL_LINES=10000
DEBUG=0
ALWAYS_FIND_OFFSET=0
for arg in "$@"; do
case "$arg" in
--debug) DEBUG=1 ;;
--always-find-offset) ALWAYS_FIND_OFFSET=1 ;;
-h|--help)
echo "Usage: $0 [--debug] [--always-find-offset]"
exit 0
;;
*)
echo "Unknown arg: $arg" >&2
echo "Usage: $0 [--debug] [--always-find-offset]" >&2
exit 2
;;
esac
done
umask 077
ts_iso() {
# ISO-ish timestamp; works on BusyBox and GNU date
date "+%Y-%m-%dT%H:%M:%S%z"
}
log() {
# Always append to OUT_LOG
echo "[$(ts_iso)] $*" >> "$OUT_LOG"
}
# Generate a short random token (8 chars) for marker grep
rand_token() {
if [ -r /dev/urandom ]; then
tr -dc 'a-z0-9' < /dev/urandom 2>/dev/null | head -c 8
else
echo "$(date -u +%s 2>/dev/null)$$" | tr -dc 'a-z0-9' | tail -c 8
fi
}
# Parse syslog header timestamp (Mon DD HH:MM:SS) into epoch seconds (UTC) using awk mktime()
# Expects typical line like: "<13> Jan 31 12:31:00 root[-] [notice]: message"
syslog_header_to_epoch_utc() {
echo "$1" | TZ=UTC awk '
function mon2num(m) {
if (m=="Jan") return 1
if (m=="Feb") return 2
if (m=="Mar") return 3
if (m=="Apr") return 4
if (m=="May") return 5
if (m=="Jun") return 6
if (m=="Jul") return 7
if (m=="Aug") return 8
if (m=="Sep") return 9
if (m=="Oct") return 10
if (m=="Nov") return 11
if (m=="Dec") return 12
return 0
}
{
mon=$2; day=$3; tod=$4
year=strftime("%Y")
m=mon2num(mon)
if (m==0) { print ""; exit 1 }
split(tod, t, ":")
if (length(t) != 3) { print ""; exit 1 }
hh=t[1]+0; mm=t[2]+0; ss=t[3]+0
d=day+0
print mktime(sprintf("%d %02d %02d %02d %02d %02d", year, m, d, hh, mm, ss))
}
'
}
# Compute offset_sec = payload_epoch_utc - syslog_header_epoch_utc
# Returns offset seconds on stdout (blank on failure)
find_time_offset_sec() {
token="$(rand_token)"
now_epoch="$(date -u +%s 2>/dev/null | tr -d ' ')"
[ -z "$now_epoch" ] && now_epoch=0
logger "connstat calculation started. issueing time marker $token epoch=$now_epoch" 2>/dev/null
found_line=""
i=0
while [ $i -lt 3 ]; do
found_line="$(tail -n 300 "$LOG_SRC" 2>/dev/null | grep "$token" | tail -n 1)"
[ -n "$found_line" ] && break
sleep 1
i=$((i + 1))
done
[ -z "$found_line" ] && { echo ""; return; }
hdr_epoch="$(syslog_header_to_epoch_utc "$found_line" | tr -d ' ')"
[ -z "$hdr_epoch" ] && { echo ""; return; }
payload_epoch="$(echo "$found_line" | awk '{
for (i=1; i<=NF; i++) {
if ($i ~ /^epoch=[0-9]+$/) { sub(/^epoch=/,"",$i); print $i; exit }
}
}' | tr -d ' ')"
[ -z "$payload_epoch" ] && payload_epoch="$now_epoch"
echo $((payload_epoch - hdr_epoch))
}
# Basic sanity checks
if [ ! -r "$LOG_SRC" ]; then
log "ERROR log_src_unreadable path=$LOG_SRC"
exit 1
fi
# Get current inode + line count (line-based incremental read is most portable)
cur_inode="$(ls -i "$LOG_SRC" 2>/dev/null | awk '{print $1}')"
cur_lines="$(wc -l < "$LOG_SRC" 2>/dev/null | tr -d ' ')"
if [ -z "$cur_inode" ] || [ -z "$cur_lines" ]; then
log "ERROR cannot_stat_log inode='$cur_inode' lines='$cur_lines'"
exit 1
fi
last_inode=""
last_line=""
if [ -f "$STATE" ]; then
# STATE format:
# inode=<num>
# line=<num>
last_inode="$(grep '^inode=' "$STATE" 2>/dev/null | head -n1 | cut -d= -f2)"
last_line="$(grep '^line=' "$STATE" 2>/dev/null | head -n1 | cut -d= -f2)"
fi
# Decide where to start reading
start_line=""
backfill_start() {
start_line=$((cur_lines - BOOTSTRAP_BACKFILL_LINES + 1))
[ "$start_line" -lt 1 ] && start_line=1
}
# Extract the slice
tmp_slice="$TMPDIR/connstats.slice.$$"
# Grep patterns (hostapd transition events)
re_auth='hostapd.*IEEE 802\.11: authenticated'
re_assoc='hostapd.*IEEE 802\.11: associated'
re_disassoc='hostapd.*IEEE 802\.11: disassociated'
# Kernel extras for "seen_any" breadth (optional but useful)
re_kernel_seen='kernel.*(station kicked out|station timed out)'
# Are we in a bootstrap-like condition (no state OR rotation/truncation)?
bootstrap_like=0
if [ -z "$last_inode" ] || [ -z "$last_line" ]; then
bootstrap_like=1
else
if [ "$cur_inode" != "$last_inode" ] || [ "$cur_lines" -lt "$last_line" ]; then
bootstrap_like=1
fi
fi
# Bootstrap handling
if [ "$ALWAYS_FIND_OFFSET" -eq 1 ] && [ "$bootstrap_like" -eq 1 ]; then
offset_sec="$(find_time_offset_sec)"
if [ -n "$offset_sec" ]; then
now_epoch="$(date -u +%s 2>/dev/null | tr -d ' ')"
[ -z "$now_epoch" ] && now_epoch=0
cutoff_epoch=$((now_epoch - BOOTSTRAP_TIME_WINDOW_SEC))
if [ $DEBUG -eq 1 ]; then
log "DEBUG bootstrap_time_window_last_10m offset_sec=$offset_sec cutoff_epoch=$cutoff_epoch tail_lines=$BOOTSTRAP_TIME_TAIL_LINES"
fi
# Build slice = last-10-min relevant lines (hostapd transitions + kernel station kicked/timed out)
# using (syslog_header_epoch + offset_sec) >= cutoff_epoch
tail -n "$BOOTSTRAP_TIME_TAIL_LINES" "$LOG_SRC" 2>/dev/null \
| TZ=UTC awk -v cutoff="$cutoff_epoch" -v off="$offset_sec" '
function mon2num(m) {
if (m=="Jan") return 1
if (m=="Feb") return 2
if (m=="Mar") return 3
if (m=="Apr") return 4
if (m=="May") return 5
if (m=="Jun") return 6
if (m=="Jul") return 7
if (m=="Aug") return 8
if (m=="Sep") return 9
if (m=="Oct") return 10
if (m=="Nov") return 11
if (m=="Dec") return 12
return 0
}
function header_epoch( mon,day,tod,year,m,hh,mm,ss,t) {
mon=$2; day=$3; tod=$4
year=strftime("%Y")
m=mon2num(mon)
if (m==0) return -1
split(tod, t, ":")
if (length(t) != 3) return -1
hh=t[1]+0; mm=t[2]+0; ss=t[3]+0
return mktime(sprintf("%d %02d %02d %02d %02d %02d", year, m, (day+0), hh, mm, ss))
}
{
line=$0
if (line ~ /hostapd.*IEEE 802\.11: (authenticated|associated|disassociated)/ ||
line ~ /kernel.*(station kicked out|station timed out)/) {
he=header_epoch()
if (he < 0) next
corr=he + off
if (corr >= cutoff) print line
}
}
' > "$tmp_slice" 2>/dev/null
start_line="(time_window_last_10m)"
else
# Offset calc failed -> fall back to old bootstrap behavior
backfill_start
if [ $DEBUG -eq 1 ]; then
log "DEBUG time_offset_failed_fallback_to_backfill cur_inode=$cur_inode cur_lines=$cur_lines start_line=$start_line backfill_lines=$BOOTSTRAP_BACKFILL_LINES"
fi
sed -n "${start_line},\$p" "$LOG_SRC" > "$tmp_slice" 2>/dev/null
fi
else
# Original behavior (unchanged)
if [ -z "$last_inode" ] || [ -z "$last_line" ]; then
backfill_start
if [ $DEBUG -eq 1 ]; then
log "DEBUG bootstrap_no_state_backfill cur_inode=$cur_inode cur_lines=$cur_lines start_line=$start_line backfill_lines=$BOOTSTRAP_BACKFILL_LINES"
fi
else
# Rotation / truncation detection
if [ "$cur_inode" != "$last_inode" ] || [ "$cur_lines" -lt "$last_line" ]; then
backfill_start
if [ $DEBUG -eq 1 ]; then
log "DEBUG bootstrap_rotation_or_truncate_backfill last_inode=$last_inode cur_inode=$cur_inode last_line=$last_line cur_lines=$cur_lines start_line=$start_line backfill_lines=$BOOTSTRAP_BACKFILL_LINES"
fi
else
start_line=$((last_line + 1))
if [ $DEBUG -eq 1 ]; then
log "DEBUG incremental last_line=$last_line cur_lines=$cur_lines start_line=$start_line"
fi
fi
fi
sed -n "${start_line},\$p" "$LOG_SRC" > "$tmp_slice" 2>/dev/null
fi
slice_lines="$(wc -l < "$tmp_slice" 2>/dev/null | tr -d ' ')"
[ -z "$slice_lines" ] && slice_lines=0
if [ $DEBUG -eq 1 ]; then
log "DEBUG slice_path=$tmp_slice slice_lines=$slice_lines"
fi
# Totals (MAC repeats allowed)
auth_events_total="$(grep -E "$re_auth" "$tmp_slice" | wc -l | tr -d ' ')"
assoc_events_total="$(grep -E "$re_assoc" "$tmp_slice" | wc -l | tr -d ' ')"
disassoc_events_total="$(grep -E "$re_disassoc" "$tmp_slice" | wc -l | tr -d ' ')"
# Extract MAC right after token "STA" (from hostapd lines)
extract_sta_macs() {
awk '
{
for (i=1; i<=NF; i++) {
if ($i=="STA") {
print $(i+1);
break;
}
}
}
'
}
# Extract MACs in brackets: [aa:bb:cc:dd:ee:ff]
extract_bracket_macs() {
grep -oE '\[[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}\]' | tr -d '[]'
}
# Count unique valid MACs from stdin
uniq_count() {
grep -E '^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$' \
| sort -u \
| wc -l | tr -d ' '
}
# Unique MACs per event type (hostapd)
unique_macs_authenticated="$(
grep -E "$re_auth" "$tmp_slice" \
| extract_sta_macs \
| uniq_count
)"
unique_macs_associated="$(
grep -E "$re_assoc" "$tmp_slice" \
| extract_sta_macs \
| uniq_count
)"
unique_macs_disassociated="$(
grep -E "$re_disassoc" "$tmp_slice" \
| extract_sta_macs \
| uniq_count
)"
# Unique MACs seen in "any relevant activity" (union: auth/assoc/disassoc + selected kernel)
unique_macs_seen_any="$(
(
grep -E 'hostapd.*IEEE 802\.11: (authenticated|associated|disassociated)' "$tmp_slice" \
| extract_sta_macs
grep -E "$re_kernel_seen" "$tmp_slice" \
| extract_bracket_macs
) | uniq_count
)"
########################################################################
# B-class metrics (ephemeral state in /tmp; reboot loss accepted)
########################################################################
SESSION_STATE="/tmp/connstats.sessions.state"
SESSION_STATE_TMP="$TMPDIR/connstats.sessions.state.$$"
SESSION_STATE_TTL_SEC=21600
AUTH_ASSOC_MAX_SAMPLES=50
now_epoch_b="$(date -u +%s 2>/dev/null | tr -d ' ')"
[ -z "$now_epoch_b" ] && now_epoch_b=0
# Output file (temporary, removed each run)
B_OUT_TMP="$TMPDIR/connstats.b.out.$$"
TZ=UTC awk -v state_in="$SESSION_STATE" \
-v state_out="$SESSION_STATE_TMP" \
-v now="$now_epoch_b" \
-v ttl="$SESSION_STATE_TTL_SEC" \
-v maxs="$AUTH_ASSOC_MAX_SAMPLES" '
function mon2num(m) {
if (m=="Jan") return 1
if (m=="Feb") return 2
if (m=="Mar") return 3
if (m=="Apr") return 4
if (m=="May") return 5
if (m=="Jun") return 6
if (m=="Jul") return 7
if (m=="Aug") return 8
if (m=="Sep") return 9
if (m=="Oct") return 10
if (m=="Nov") return 11
if (m=="Dec") return 12
return 0
}
function header_epoch( mon,day,tod,year,m,hh,mm,ss,t) {
mon=$2; day=$3; tod=$4
year=strftime("%Y")
m=mon2num(mon)
if (m==0) return -1
split(tod, t, ":")
if (length(t) != 3) return -1
hh=t[1]+0; mm=t[2]+0; ss=t[3]+0
return mktime(sprintf("%d %02d %02d %02d %02d %02d", year, m, (day+0), hh, mm, ss))
}
function extract_sta_mac( i) {
for (i=1; i<=NF; i++) {
if ($i=="STA") return $(i+1)
}
return ""
}
function mac_valid(m) {
return (m ~ /^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$/)
}
BEGIN {
if (state_in != "") {
while ((getline line < state_in) > 0) {
n = split(line, a, " ")
if (n >= 2) {
m=a[1]; e=a[2]+0
if (mac_valid(m) && e > 0) last_auth[m]=e
}
}
close(state_in)
}
}
{
line=$0
if (line !~ /hostapd.*IEEE 802\.11: (authenticated|associated|disassociated)/) next
e = header_epoch()
if (e < 0) next
mac = extract_sta_mac()
if (!mac_valid(mac)) next
if (line ~ /IEEE 802\.11: authenticated/) {
last_auth[mac]=e
next
}
if (line ~ /IEEE 802\.11: associated/) {
if (mac in last_auth) {
d = e - last_auth[mac]
if (d >= 0) {
ms = d * 1000
if (sample_count < maxs) {
if (samples == "") samples = ms
else samples = samples "," ms
sample_count++
}
}
}
next
}
if (line ~ /IEEE 802\.11: disassociated/) {
if (mac in last_auth) {
d = e - last_auth[mac]
if (d >= 0) {
full_sessions_total++
sum_len += d
if (d < 60) early_drop_sessions_total++
delete last_auth[mac]
}
}
next
}
}
END {
if (full_sessions_total > 0) avg_session_length = int((sum_len / full_sessions_total) + 0.5)
else avg_session_length = 0
if (state_out != "") {
for (m in last_auth) {
e = last_auth[m] + 0
if (e > 0 && (now <= 0 || (e >= (now - ttl)))) {
print m " " e > state_out
}
}
close(state_out)
}
print full_sessions_total "\t" avg_session_length "\t" early_drop_sessions_total "\t" samples
}
' "$tmp_slice" > "$B_OUT_TMP" 2>/dev/null
# Default B fields
full_sessions_total=0
avg_session_length=0
early_drop_sessions_total=0
auth_to_assoc_time_ms_samples=""
# Parse awk output
if [ -s "$B_OUT_TMP" ]; then
b_out_line="$(head -n 1 "$B_OUT_TMP" 2>/dev/null)"
full_sessions_total="$(echo "$b_out_line" | awk -F'\t' '{print $1}' | tr -d ' ')"
avg_session_length="$(echo "$b_out_line" | awk -F'\t' '{print $2}' | tr -d ' ')"
early_drop_sessions_total="$(echo "$b_out_line" | awk -F'\t' '{print $3}' | tr -d ' ')"
auth_to_assoc_time_ms_samples="$(echo "$b_out_line" | awk -F'\t' '{print $4}')"
fi
rm -f "$B_OUT_TMP" 2>/dev/null
[ -z "$full_sessions_total" ] && full_sessions_total=0
[ -z "$avg_session_length" ] && avg_session_length=0
[ -z "$early_drop_sessions_total" ] && early_drop_sessions_total=0
# Move state into place (ephemeral but persistent across runs until reboot)
if [ -f "$SESSION_STATE_TMP" ]; then
mv -f "$SESSION_STATE_TMP" "$SESSION_STATE" 2>/dev/null
else
rm -f "$SESSION_STATE_TMP" 2>/dev/null
fi
########################################################################
# C-class metrics (kernel kick reason codes 1..8)
########################################################################
# Only count kernel "station kicked out ..." (avoids PRS DRIVER_LOG "reason" lines)
re_kick_reason_base='kernel.*station kicked out.*reason[[:space:]]+'
reason_code_events_1="$(grep -E "${re_kick_reason_base}1([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
reason_code_events_2="$(grep -E "${re_kick_reason_base}2([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
reason_code_events_3="$(grep -E "${re_kick_reason_base}3([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
reason_code_events_4="$(grep -E "${re_kick_reason_base}4([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
reason_code_events_5="$(grep -E "${re_kick_reason_base}5([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
reason_code_events_6="$(grep -E "${re_kick_reason_base}6([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
reason_code_events_7="$(grep -E "${re_kick_reason_base}7([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
reason_code_events_8="$(grep -E "${re_kick_reason_base}8([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')"
[ -z "$reason_code_events_1" ] && reason_code_events_1=0
[ -z "$reason_code_events_2" ] && reason_code_events_2=0
[ -z "$reason_code_events_3" ] && reason_code_events_3=0
[ -z "$reason_code_events_4" ] && reason_code_events_4=0
[ -z "$reason_code_events_5" ] && reason_code_events_5=0
[ -z "$reason_code_events_6" ] && reason_code_events_6=0
[ -z "$reason_code_events_7" ] && reason_code_events_7=0
[ -z "$reason_code_events_8" ] && reason_code_events_8=0
########################################################################
# D-class metrics (flaps between ath0 and ath1 within the bucket)
########################################################################
D_OUT_TMP="$TMPDIR/connstats.d.out.$$"
awk '
function extract_sta_mac( i) {
for (i=1; i<=NF; i++) if ($i=="STA") return $(i+1)
return ""
}
function mac_valid(m) {
return (m ~ /^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$/)
}
function extract_iface( i, v) {
for (i=1; i<=NF; i++) {
v = $i
if (v ~ /^ath[0-9]+:$/) { sub(/:$/,"",v); return v }
}
return ""
}
{
line=$0
if (line !~ /hostapd.*IEEE 802\.11: associated/) next
iface = extract_iface()
if (iface != "ath0" && iface != "ath1") next
mac = extract_sta_mac()
if (!mac_valid(mac)) next
if (mac in last_iface) {
prev = last_iface[mac]
if (prev != iface) {
if (prev=="ath0" && iface=="ath1") {
flap_events_0_to_1++
uniq_0_to_1[mac]=1
} else if (prev=="ath1" && iface=="ath0") {
flap_events_1_to_0++
uniq_1_to_0[mac]=1
}
}
}
last_iface[mac]=iface
}
END {
for (m in uniq_0_to_1) unique_0_to_1++
for (m in uniq_1_to_0) unique_1_to_0++
if (unique_0_to_1+0 < 0) unique_0_to_1=0
if (flap_events_0_to_1+0 < 0) flap_events_0_to_1=0
if (unique_1_to_0+0 < 0) unique_1_to_0=0
if (flap_events_1_to_0+0 < 0) flap_events_1_to_0=0
print unique_0_to_1 "\t" flap_events_0_to_1 "\t" unique_1_to_0 "\t" flap_events_1_to_0
}
' "$tmp_slice" > "$D_OUT_TMP" 2>/dev/null
unique_macs_flap_ath0_to_ath1=0
flap_events_total_ath0_to_ath1=0
unique_macs_flap_ath1_to_ath0=0
flap_events_total_ath1_to_ath0=0
if [ -s "$D_OUT_TMP" ]; then
d_out_line="$(head -n 1 "$D_OUT_TMP" 2>/dev/null)"
unique_macs_flap_ath0_to_ath1="$(echo "$d_out_line" | awk -F'\t' '{print $1}' | tr -d ' ')"
flap_events_total_ath0_to_ath1="$(echo "$d_out_line" | awk -F'\t' '{print $2}' | tr -d ' ')"
unique_macs_flap_ath1_to_ath0="$(echo "$d_out_line" | awk -F'\t' '{print $3}' | tr -d ' ')"
flap_events_total_ath1_to_ath0="$(echo "$d_out_line" | awk -F'\t' '{print $4}' | tr -d ' ')"
fi
rm -f "$D_OUT_TMP" 2>/dev/null
[ -z "$unique_macs_flap_ath0_to_ath1" ] && unique_macs_flap_ath0_to_ath1=0
[ -z "$flap_events_total_ath0_to_ath1" ] && flap_events_total_ath0_to_ath1=0
[ -z "$unique_macs_flap_ath1_to_ath0" ] && unique_macs_flap_ath1_to_ath0=0
[ -z "$flap_events_total_ath1_to_ath0" ] && flap_events_total_ath1_to_ath0=0
########################################################################
# Emit one summary line
########################################################################
log "connstats bucket_run slice_lines=$slice_lines unique_macs_seen_any=$unique_macs_seen_any unique_macs_authenticated=$unique_macs_authenticated auth_events_total=$auth_events_total unique_macs_associated=$unique_macs_associated assoc_events_total=$assoc_events_total unique_macs_disassociated=$unique_macs_disassociated disassoc_events_total=$disassoc_events_total full_sessions_total=$full_sessions_total avg_session_length=$avg_session_length early_drop_sessions_total=$early_drop_sessions_total auth_to_assoc_time_ms_samples=$auth_to_assoc_time_ms_samples reason_code_events_1=$reason_code_events_1 reason_code_events_2=$reason_code_events_2 reason_code_events_3=$reason_code_events_3 reason_code_events_4=$reason_code_events_4 reason_code_events_5=$reason_code_events_5 reason_code_events_6=$reason_code_events_6 reason_code_events_7=$reason_code_events_7 reason_code_events_8=$reason_code_events_8 unique_macs_flap_ath0_to_ath1=$unique_macs_flap_ath0_to_ath1 flap_events_total_ath0_to_ath1=$flap_events_total_ath0_to_ath1 unique_macs_flap_ath1_to_ath0=$unique_macs_flap_ath1_to_ath0 flap_events_total_ath1_to_ath0=$flap_events_total_ath1_to_ath0"
if [ $DEBUG -eq 1 ]; then
log "DEBUG dump_auth_lines_begin"
grep -E "$re_auth" "$tmp_slice" | sed 's/^/DEBUG AUTH: /' >> "$OUT_LOG"
log "DEBUG dump_assoc_lines_begin"
grep -E "$re_assoc" "$tmp_slice" | sed 's/^/DEBUG ASSOC: /' >> "$OUT_LOG"
log "DEBUG dump_disassoc_lines_begin"
grep -E "$re_disassoc" "$tmp_slice" | sed 's/^/DEBUG DISASSOC: /' >> "$OUT_LOG"
log "DEBUG dump_kick_reason_lines_begin"
grep -E "${re_kick_reason_base}[1-8]([^0-9]|$)" "$tmp_slice" | sed 's/^/DEBUG KICK: /' >> "$OUT_LOG"
log "DEBUG dump_flap_assoc_lines_begin"
grep -E 'hostapd.*IEEE 802\.11: associated' "$tmp_slice" | sed 's/^/DEBUG FLAP_ASSOC: /' >> "$OUT_LOG"
log "DEBUG dump_flap_events_begin"
awk '
function extract_sta_mac( i) { for (i=1; i<=NF; i++) if ($i=="STA") return $(i+1); return "" }
function mac_valid(m) { return (m ~ /^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$/) }
function extract_iface( i, v) {
for (i=1; i<=NF; i++) { v=$i; if (v ~ /^ath[0-9]+:$/) { sub(/:$/,"",v); return v } }
return ""
}
{
if ($0 !~ /hostapd.*IEEE 802\.11: associated/) next
iface=extract_iface()
if (iface!="ath0" && iface!="ath1") next
mac=extract_sta_mac()
if (!mac_valid(mac)) next
if (mac in last_iface) {
prev=last_iface[mac]
if (prev!=iface) {
if ((prev=="ath0" && iface=="ath1") || (prev=="ath1" && iface=="ath0")) {
print "mac=" mac " from=" prev " to=" iface
}
}
}
last_iface[mac]=iface
}
' "$tmp_slice" | sed 's/^/DEBUG FLAP: /' >> "$OUT_LOG"
fi
# Update state to current end-of-file line count and inode
{
echo "inode=$cur_inode"
echo "line=$cur_lines"
} > "$STATE"
rm -f "$tmp_slice" 2>/dev/null
exit 0