Compare commits

..
Author SHA1 Message Date
Mike Reeves 1dfdae9ea3 Postgres: run pg_partman maintenance over the local socket and detect failed runs 2026-11-10 11:38:23 -05:00
Jorge Reyes b7b00f8233 Merge pull request #16378 from Security-Onion-Solutions/reyesj2/fix-16369
surface ilm policy errors in so-elasticsearch-ilm-policy-load output
2026-11-05 14:05:29 -06:00
Jorge Reyes 45d2b29a42 Merge pull request #16374 from Security-Onion-Solutions/fix/skip-mismatched-iso-registry
Skip leftover ISO registry.tar when tags do not match SOVERSION
2026-11-05 11:11:45 -06:00
Doug Burks 7b494439e7 Merge pull request #16377 from Security-Onion-Solutions/feature/desktop-so-crates
Install securityonion-so-crates on Security Onion Desktop
2026-11-04 15:56:12 -05:00
Doug Burks 9a8dc097b2 Install securityonion-so-crates on Security Onion Desktop 2026-11-04 15:51:16 -05:00
reyesj2 274da18929 Surface ILM policy validation errors in highstate
Fail 4xx ILM PUTs immediately (retry 429/5xx/connect), print the
Elasticsearch reason in the salt summary, and retry the load on the
next highstate via /opt/so/state/esilm.txt when the last run failed.

Fixes https://github.com/Security-Onion-Solutions/sos/issues/277
2026-11-04 13:18:31 -06:00
reyesj2 99a8ad2d4b Wait for Elasticsearch and retry ILM policy loads
Require wait_for_so-elasticsearch, check ES is responsive before
dispatch, and retry each policy PUT so soup no longer fails with
empty output when ES is not ready yet.

Fixes https://github.com/Security-Onion-Solutions/securityonion/issues/16369
2026-11-04 11:14:12 -06:00
Mike Reeves bd9cdeadef Merge pull request #16373 from Security-Onion-Solutions/foundry/mw1jl4boa6hshh
Remove x86-64-v3 CPU pre-flight check from soup
2026-11-03 22:50:54 -05:00
Mike Reeves bdf01a5a5a Remove x86-64-v3 CPU check from soup 2026-11-03 22:13:58 -05:00
reyesj2 1849c74cea Skip leftover ISO registry.tar when tags do not match SOVERSION
Fixes #16372
2026-11-03 21:07:29 -06:00
Mike Reeves 021b069706 Merge pull request #16367 from Security-Onion-Solutions/TOoSmOotH-patch-1
Bump version from 3.4.0 to 3.5.0
2026-11-02 11:52:06 -05:00
Mike Reeves 824d674eec Add version 3.5.0 to discussion template 2026-11-02 11:50:49 -05:00
Mike Reeves d43791c63e Bump version from 3.4.0 to 3.5.0 2026-11-02 11:50:08 -05:00
14 changed files with 277 additions and 135 deletions

No files matched your search

+1
View File
@@ -14,6 +14,7 @@ body:
- 3.2.0
- 3.3.0
- 3.4.0
- 3.5.0
- Other (please provide detail below)
validations:
required: true
+1 -1
View File
@@ -1 +1 @@
3.4.0
3.5.0
+16
View File
@@ -17,6 +17,7 @@ import time
EXPECTED_CONTAINERS_FILE = "/opt/so/conf/so-status/so-status.conf"
HIGHSTATE_COMPLETE_FILE = "/opt/so/log/salt/lasthighstate"
UPTIME_FILE = "/proc/uptime"
POSTGRES_MAINTENANCE_STATUS_FILE = "/opt/so/log/postgres/maintenance.status"
def showUsage(options, args):
print('Usage: {} [OPTIONS]'.format(sys.argv[0]))
@@ -149,6 +150,20 @@ def check_container_status(options, console):
return code, container_list
def check_postgres_maintenance(code, container_list):
try:
with open(POSTGRES_MAINTENANCE_STATUS_FILE, "r") as status_file:
status = status_file.readline().strip()
except OSError:
return code
if status.startswith("failed"):
reason = status[len("failed"):].lstrip(":").strip()
container_list.append({ "Name": "so-postgres-maintenance", "Status": "failed", "Details": reason })
if code == 0:
code = 1
return code
def check_status(options, console):
container_list = []
code = check_installation_status(options, console)
@@ -160,6 +175,7 @@ def check_status(options, console):
# on the Grid until a highstate completes.
if code == 0 or code == 2:
container_code, container_list = check_container_status(options, console)
container_code = check_postgres_maintenance(container_code, container_list)
if code == 0:
code = container_code
output(options, console, code, container_list)
+4
View File
@@ -438,6 +438,10 @@ install_networkminer:
pkg.latest:
- name: securityonion-networkminer
install_so_crates:
pkg.latest:
- name: securityonion-so-crates
{% else %}
desktop_packages_os_fail:
+9 -2
View File
@@ -107,6 +107,12 @@ so-elasticsearch-system-indices-patch:
- file: so-elasticsearch-system-indices-patch-script
{% endif %}
so-elasticsearch-ilm-policy-reload:
file.absent:
- name: /opt/so/state/esilm.txt
- onchanges:
- file: so-elasticsearch-ilm-policy-load-script
# heavynodes will only load ILM policies for SO managed indices. (Indicies defined in elasticsearch/defaults.yaml)
so-elasticsearch-ilm-policy-load:
cmd.run:
@@ -115,8 +121,9 @@ so-elasticsearch-ilm-policy-load:
- require:
- docker_container: so-elasticsearch
- file: so-elasticsearch-ilm-policy-load-script
- onchanges:
- file: so-elasticsearch-ilm-policy-load-script
- http: wait_for_so-elasticsearch
- file: so-elasticsearch-ilm-policy-reload
- unless: test -f /opt/so/state/esilm.txt
so-elasticsearch-templates-reload:
file.absent:
@@ -7,11 +7,17 @@
. /usr/sbin/so-common
MAX_JOBS=${MAX_ILM_JOBS:-10}
ILM_STATEFILE=/opt/so/state/esilm.txt
rm -f "$ILM_STATEFILE"
check_elasticsearch_responsive
ILM_TMP_DIR=$(mktemp -d)
trap 'rm -rf "$ILM_TMP_DIR"' EXIT
# Lock used to serialize block writes so concurrent jobs never interleave their output.
ILM_OUTPUT_LOCK=$(mktemp)
ILM_FAIL_FILE=$(mktemp)
trap 'rm -f "$ILM_OUTPUT_LOCK" "$ILM_FAIL_FILE"' EXIT
ILM_OUTPUT_LOCK="$ILM_TMP_DIR/output.lock"
ILM_FAIL_FILE="$ILM_TMP_DIR/failed"
touch "$ILM_FAIL_FILE"
# Policies are loaded concurrently (up to MAX_JOBS at a time) for speed. Each policy's block is
# printed the moment its curl returns, so output appears in COMPLETION ORDER, not the order
@@ -20,13 +26,30 @@ echo "Loading ILM policies concurrently; output below appears in completion orde
echo
put_policy() {
local desc="$1" policyname="$2" data="$3" result rc=0
if ! result=$(curl -K /opt/so/conf/elasticsearch/curl.config -s -k -L --fail \
-X PUT "https://localhost:9200/_ilm/policy/${policyname}" \
-H 'Content-Type: application/json' -d"${data}" 2>&1); then
rc=1
elif ! jq -e '.acknowledged == true' <<<"$result" >/dev/null 2>&1; then
rc=1
local desc="$1" policyname="$2" data="$3" rc=1 attempt out http_code body result reason tmpdata
tmpdata=$(mktemp "$ILM_TMP_DIR/${policyname}.XXXXXX.json")
printf '%s' "$data" >"$tmpdata"
for attempt in 1 2 3; do
out=$(so-elasticsearch-query "_ilm/policy/${policyname}" -d@"${tmpdata}" -XPUT -w '\n%{http_code}' 2>/dev/null)
http_code=${out##*$'\n'} body=${out%$'\n'*}
if [[ "$http_code" == 200 ]] && jq -e '.acknowledged == true' <<<"$body" >/dev/null 2>&1; then
rc=0
break
fi
# 429 = ES rejected/throttled (may succeed later); other 4xx will not
[[ "$http_code" == 4?? && "$http_code" != 429 ]] && break
(( attempt < 3 )) && sleep 3
done
if (( rc == 0 )); then
result='{"acknowledged":true}'
else
result=${body:-"HTTP ${http_code:-000}"}
# flatten tabs/newlines so the TAB fail-file stays one record
reason=$(jq -r '(if (.error|type) == "object" then (.error.reason // .error.type) elif (.error|type) == "string" then .error else empty end) // empty | tostring | gsub("[\t\r\n]+"; " ")' <<<"$body" 2>/dev/null)
reason="HTTP ${http_code:-000}${reason:+: ${reason}}"
fi
# curl above ran in parallel; serialize just this block write so concurrent jobs never interleave.
@@ -34,7 +57,7 @@ put_policy() {
flock 200
printf 'Setting up %s policy...\n%s\n\n' "${desc}" "${result}"
if (( rc != 0 )); then
printf '%s\n' "${policyname}" >>"$ILM_FAIL_FILE"
printf '%s\t%s\n' "${policyname}" "${reason}" >>"$ILM_FAIL_FILE"
fi
} 200>>"${ILM_OUTPUT_LOCK}"
@@ -82,10 +105,15 @@ wait || true
if [[ -s "$ILM_FAIL_FILE" ]]; then
echo "ERROR: Failed to load ILM policy(s):"
while read -r POLICY; do
echo " - $POLICY"
while IFS=$'\t' read -r POLICY REASON; do
if [[ -n "$REASON" ]]; then
echo " - ${POLICY}: ${REASON}"
else
echo " - ${POLICY}"
fi
done < "$ILM_FAIL_FILE"
exit 1
else
echo "Successfully loaded all ILM policies."
touch "$ILM_STATEFILE"
fi
+15 -96
View File
@@ -121,9 +121,6 @@ check_err() {
161)
echo 'Required intermediate Elasticsearch upgrade not complete'
;;
162)
echo 'One or more Elastic Agent nodes do not support the x86-64-v3 CPU instruction set'
;;
170)
echo "Intermediate upgrade completed successfully to $next_step_so_version, but next soup to Security Onion $originally_requested_so_version could not be started automatically."
echo "Start soup again manually to continue the upgrade to Security Onion $originally_requested_so_version."
@@ -351,83 +348,6 @@ check_cluster_health() {
exit 0
}
no_soup_for_you() {
echo ""
echo "No soup for you!"
exit 162
}
check_cpu_compatibility() {
# Roles running a container built from the so-elastic-agent image; mirrors the
# elasticagent and elasticfleet entries in salt/reactor/pillar_push_map.yaml.
local cpu_target='G@role:so-heavynode or G@role:so-eval or G@role:so-fleet or G@role:so-import or G@role:so-manager or G@role:so-managerhype or G@role:so-managersearch or G@role:so-standalone'
local expected_nodes cpu_results node result confirm
local -a unsupported=() offline=()
echo "Checking that Elastic Agent nodes support the x86-64-v3 CPU instruction set now required by Elastic."
if [[ "$SKIP_CPU_CHECK" == "true" ]]; then
printf "\nSkipping the x86-64-v3 CPU check because --skip-cpu-check was specified.\n\n"
return
fi
# Nodes that never answer are absent from the results, so diff against who should have.
expected_nodes=$(salt -C "$cpu_target" --preview-target --out=json 2>/dev/null | jq -r '.[]?') || true
if [[ -z "$expected_nodes" ]]; then
printf "\nCould not determine which nodes run the Elastic Agent, so the x86-64-v3 CPU check cannot run.\n"
no_soup_for_you
fi
cpu_results=$(salt -t 30 -C "$cpu_target" cmd.run "/lib64/ld-linux-x86-64.so.2 --help | grep x86-64-v3" --out=json 2>/dev/null) || true
while IFS= read -r node; do
[[ -z "$node" ]] && continue
result=$(jq -r --arg node "$node" '.[$node] // empty' <<< "$cpu_results" 2>/dev/null)
if [[ -z "$result" || "$result" == *"did not return"* ]]; then
offline+=("$node")
elif [[ "$result" != *"x86-64-v3 (supported"* ]]; then
# glibc appends "(supported, searched)" only when supported; the open paren keeps
# this from matching a future "(unsupported".
unsupported+=("$node")
fi
done <<< "$expected_nodes"
if [[ ${#unsupported[@]} -eq 0 && ${#offline[@]} -eq 0 ]]; then
printf "\nAll Elastic Agent nodes support x86-64-v3. We can proceed with SOUP.\n\n"
return
fi
echo ""
if [[ ${#unsupported[@]} -gt 0 ]]; then
echo "The following node(s) do NOT support the x86-64-v3 CPU instruction set:"
printf ' %s\n' "${unsupported[@]}"
echo ""
echo "Upstream Elastic now builds its binaries for x86-64-v3, so these nodes can no"
echo "longer run Elastic. Upgrading them WILL BREAK them."
echo ""
fi
if [[ ${#offline[@]} -gt 0 ]]; then
echo "The following node(s) did not respond and could not be checked:"
printf ' %s\n' "${offline[@]}"
echo ""
echo "These nodes are offline, so we cannot confirm they support x86-64-v3, which"
echo "upstream Elastic now requires."
echo ""
fi
if [[ -n $UNATTENDED ]]; then
echo "Unattended mode cannot prompt for an override. Re-run soup interactively, or pass --skip-cpu-check to bypass this check."
no_soup_for_you
fi
read -rp "Type 'override' to continue anyway, or press Enter to exit: " confirm
if [[ "${confirm,,}" == "override" ]]; then
printf "\nOverride accepted. Continuing at your own risk.\n\n"
else
no_soup_for_you
fi
}
check_fleet_server() {
echo "Checking that Elastic Fleet Server is responding."
# Modeled on the wait_for_so-elastic-fleet state check in elasticfleet/enabled.sls,
@@ -626,6 +546,7 @@ postupgrade_changes() {
[[ "$POSTVERSION" == "3.1.0" ]] && post_to_3.2.0
[[ "$POSTVERSION" == "3.2.0" ]] && post_to_3.3.0
[[ "$POSTVERSION" == "3.3.0" ]] && post_to_3.4.0
[[ "$POSTVERSION" == "3.4.0" ]] && post_to_3.5.0
# All applicable post-upgrade steps completed; clear the resume marker.
rm -f "$POSTVERSION_FILE"
true
@@ -1149,11 +1070,14 @@ telegraf_repair() {
echo "Checking Telegraf metric partitions."
local status=0
"$repair" --check >> "$SOUP_LOG" 2>&1 || status=$?
local max_age
max_age=$(lookup_pillar maintenance_max_age_hours postgres:telegraf 2>/dev/null) || true
max_age=${max_age:-3}
MAINTENANCE_MAX_AGE_HOURS="$max_age" "$repair" --check >> "$SOUP_LOG" 2>&1 || status=$?
case "$status" in
0) echo " Telegraf partitions are healthy; nothing to repair." ;;
1) echo " Repairing stalled Telegraf partitions."
"$repair" --yes \
MAINTENANCE_MAX_AGE_HOURS="$max_age" "$repair" --yes \
|| echo " warning: so-telegraf-repair failed; run it manually" >&2 ;;
*) echo " Skipping; Telegraf is not storing metrics in Postgres on this host." ;;
esac
@@ -1282,6 +1206,14 @@ add_detection_runtime_fields() {
}
### 3.4.0 End ###
### 3.5.0 Scripts ###
post_to_3.5.0() {
telegraf_repair
set_postversion 3.5.0
}
### 3.5.0 End ###
repo_sync() {
echo "Sync the local repo."
@@ -2168,8 +2100,6 @@ main() {
echo "Let's see if we need to update Security Onion."
upgrade_check
check_cpu_compatibility
upgrade_space
echo "Verifying Elasticsearch version compatibility across the grid before upgrading."
@@ -2448,17 +2378,6 @@ fi
echo "### soup has been served at $(date) ###"
}
SKIP_CPU_CHECK=false
declare -a SOUP_ARGS=()
for arg in "$@"; do
if [[ "$arg" == "--skip-cpu-check" ]]; then
SKIP_CPU_CHECK=true
else
SOUP_ARGS+=("$arg")
fi
done
set -- "${SOUP_ARGS[@]}"
while getopts ":b:f:y" opt; do
case ${opt} in
b )
@@ -2482,7 +2401,7 @@ while getopts ":b:f:y" opt; do
ISOLOC="$OPTARG"
;;
\? )
echo "Usage: soup [-b] [-y] [-f <iso location>] [--skip-cpu-check]"
echo "Usage: soup [-b] [-y] [-f <iso location>]"
exit 1
;;
: )
+4
View File
@@ -2,6 +2,7 @@ postgres:
enabled: True
telegraf:
retention_days: 14
maintenance_max_age_hours: 3
config:
listen_addresses: '*'
port: 5432
@@ -17,3 +18,6 @@ postgres:
log_min_messages: 'warning'
shared_preload_libraries: pg_cron
cron.database_name: postgres
# pg_hba.conf only trusts local socket connections; over TCP pg_cron has no
# password to send and every job fails to connect.
cron.host: /run/postgresql
+9
View File
@@ -24,6 +24,15 @@ so_postgres_backup:
- identifier: so_postgres_backup
- user: root
so_postgres_maintenance_status:
cron.absent:
- identifier: so_postgres_maintenance_status
- user: root
so_postgres_maintenance_status_file:
file.absent:
- name: /opt/so/log/postgres/maintenance.status
{% else %}
{{sls}}_state_not_allowed:
+24
View File
@@ -8,6 +8,7 @@
{% from 'vars/globals.map.jinja' import GLOBALS %}
{% from 'ca/trust.map.jinja' import CATRUST %}
{% from 'docker/docker.map.jinja' import DOCKERMERGED %}
{% from 'telegraf/map.jinja' import TELEGRAFMERGED %}
{% set SO_POSTGRES_USER = salt['pillar.get']('postgres:auth:users:so_postgres_user:user', 'so_postgres') %}
include:
@@ -122,6 +123,29 @@ so_postgres_backup:
- month: '*'
- dayweek: '*'
{% if TELEGRAFMERGED.output | upper in ['POSTGRES', 'BOTH'] %}
{% set max_age = salt['pillar.get']('postgres:telegraf:maintenance_max_age_hours', 3) | int %}
so_postgres_maintenance_status:
cron.present:
- name: MAINTENANCE_MAX_AGE_HOURS={{ max_age }} /usr/sbin/so-telegraf-postgres status > /dev/null 2>&1
- identifier: so_postgres_maintenance_status
- user: root
- minute: '*/15'
- hour: '*'
- daymonth: '*'
- month: '*'
- dayweek: '*'
{% else %}
so_postgres_maintenance_status:
cron.absent:
- identifier: so_postgres_maintenance_status
- user: root
so_postgres_maintenance_status_file:
file.absent:
- name: /opt/so/log/postgres/maintenance.status
{% endif %}
{% else %}
{{sls}}_state_not_allowed:
+10
View File
@@ -9,6 +9,11 @@ postgres:
description: Number of days of Telegraf metrics to keep in the so_telegraf database. Older partitions are dropped hourly by pg_partman.
forcedType: int
helpLink: postgres
maintenance_max_age_hours:
description: Number of hours without a successful run of the hourly pg_partman maintenance job before the Telegraf metrics database is reported as unhealthy in grid status and repaired by soup.
forcedType: int
advanced: True
helpLink: postgres
config:
max_connections:
description: Maximum number of concurrent PostgreSQL connections.
@@ -87,3 +92,8 @@ postgres:
global: True
advanced: True
helpLink: postgres
cron.host:
description: Unix socket directory pg_cron uses to run scheduled jobs. Must be a local socket directory so jobs authenticate with the local trust rule; a TCP host requires a password and jobs will fail.
global: True
advanced: True
helpLink: postgres
+75 -2
View File
@@ -12,7 +12,11 @@ set -e
# retention Reconcile partman retention and premake on telegraf parents.
# Env: RETENTION_DAYS.
# maintenance Drain default partitions and run partman maintenance.
# check Report partition health. Non-zero if any parent is unhealthy.
# check Report partition and maintenance health. Non-zero if any parent
# is unhealthy or the maintenance job has no successful run within
# the last MAINTENANCE_MAX_AGE_HOURS (default 3).
# status Run check and write `ok` or `failed: <reason>` to
# /opt/so/log/postgres/maintenance.status for so-status.
#
# A default partition holding rows for a day blocks creating that day's child,
# so maintenance drains defaults before calling partman. Use so-telegraf-repair
@@ -189,6 +193,11 @@ SELECT cron.schedule_in_database(
'CALL so_admin.telegraf_maintenance()',
'so_telegraf'
);
-- pg_cron copies cron.host into nodename only when a job is first created, so
-- jobs registered before cron.host changed keep connecting to the old host.
UPDATE cron.job SET nodename = current_setting('cron.host')
WHERE jobname = 'telegraf-partman-maintenance'
AND nodename IS DISTINCT FROM current_setting('cron.host');
\else
\echo 'pg_cron metadata database is not `postgres` yet; skipping job registration.'
\endif
@@ -278,16 +287,80 @@ LIMIT 5;
\echo 'pg_cron is not installed in this database.'
\endif
EOSQL
max_age="${MAINTENANCE_MAX_AGE_HOURS:-3}"
[[ "$max_age" =~ ^[0-9]+$ ]] || { echo "MAINTENANCE_MAX_AGE_HOURS must be an integer" >&2; exit 1; }
failures=()
unhealthy=$(docker exec so-postgres psql -U postgres -d so_telegraf -tAc \
"SELECT count(*) FROM so_admin.telegraf_partition_status()
WHERE coalesce(default_rows, 0) > 0 OR coalesce(days_ahead, -1) < 1")
if [ "${unhealthy:-1}" != "0" ]; then
echo "so-telegraf-postgres check: $unhealthy telegraf parent(s) unhealthy" >&2
failures+=("$unhealthy telegraf parent(s) unhealthy")
fi
has_cron=$(docker exec so-postgres psql -U postgres -d postgres -tAc \
"SELECT to_regclass('cron.job_run_details') IS NOT NULL")
if [ "$has_cron" != "t" ]; then
failures+=("pg_cron is not installed in the postgres database")
else
runs=$(docker exec so-postgres psql -U postgres -d postgres -tA -F '|' -c \
"SELECT (SELECT count(*) FROM cron.job WHERE jobname = 'telegraf-partman-maintenance'),
(SELECT count(*) FROM cron.job WHERE jobname = 'telegraf-partman-maintenance' AND active),
(SELECT count(*) FROM pg_stat_activity
WHERE backend_type ILIKE '%pg_cron%' OR application_name ILIKE '%pg_cron%'),
(SELECT count(*) FROM pg_stat_activity
WHERE (backend_type ILIKE '%pg_cron%' OR application_name ILIKE '%pg_cron%')
AND backend_start < now() - make_interval(hours => $max_age)),
count(*),
count(*) FILTER (WHERE d.status = 'succeeded'
AND d.start_time > now() - make_interval(hours => $max_age)),
coalesce((array_agg(format('last run %s at %s: %s', d.status, d.start_time,
regexp_replace(coalesce(d.return_message, ''), '\s+', ' ', 'g'))
ORDER BY d.start_time DESC))[1], '')
FROM cron.job j
JOIN cron.job_run_details d ON d.jobid = j.jobid
WHERE j.jobname = 'telegraf-partman-maintenance'") || runs=""
IFS='|' read -r jobs active launchers old_launchers run_count recent_ok last_run <<< "$runs"
# With no runs recorded, a live launcher younger than N hours means the
# job was only just scheduled (fresh install or a restart for a config
# change) and has not reached its first :17 yet. A launcher older than
# that should have fired it by now. Tradeoff: a job first scheduled long
# after the launcher started reports failed until its first run.
if [ -z "$runs" ]; then
failures+=("could not read telegraf-partman-maintenance runs from cron.job_run_details")
elif [ "$jobs" = "0" ]; then
failures+=("telegraf-partman-maintenance is not scheduled")
elif [ "$active" = "0" ]; then
failures+=("telegraf-partman-maintenance is inactive")
elif [ "$launchers" = "0" ]; then
failures+=("pg_cron launcher is not running; telegraf-partman-maintenance cannot run")
elif [ "$run_count" = "0" ]; then
if [ "$old_launchers" != "0" ]; then
failures+=("telegraf-partman-maintenance has never run although pg_cron has been up for more than $max_age hour(s)")
fi
elif [ "$recent_ok" = "0" ]; then
failures+=("no successful telegraf-partman-maintenance run in the last $max_age hour(s); $last_run")
fi
fi
if [ "${#failures[@]}" -gt 0 ]; then
msg=$(printf '%s; ' "${failures[@]}")
echo "so-telegraf-postgres check: ${msg%; }" >&2
exit 1
fi
echo "so-telegraf-postgres check: all telegraf parents healthy"
;;
status)
status_file=/opt/so/log/postgres/maintenance.status
tmp=$(mktemp "$status_file.XXXXXX")
if reason=$("$0" check 2>&1 >/dev/null); then
echo ok > "$tmp"
else
reason=$(printf '%s\n' "$reason" | tail -n 1)
echo "failed: ${reason#so-telegraf-postgres check: }" > "$tmp"
fi
chmod 644 "$tmp"
mv -f "$tmp" "$status_file"
;;
*)
echo "Unknown subcommand: $cmd" >&2
exit 1
+30 -6
View File
@@ -15,6 +15,9 @@
# --yes Skip the confirmation prompt (for soup and other automation).
# --no-restart Never restart so-postgres, even if pg_cron's launcher is dead.
#
# Env: MAINTENANCE_MAX_AGE_HOURS (default 3) is how long the maintenance job may
# go without a successful run before repair is needed.
#
# Exit status:
# 0 healthy, or repair completed
# 1 repair is needed (--check only)
@@ -25,6 +28,8 @@ set -e
# Matches p_premake in telegraf.conf's create_parent template.
PREMAKE=7
JOB_NAME=telegraf-partman-maintenance
MAX_AGE_HOURS="${MAINTENANCE_MAX_AGE_HOURS:-3}"
[[ "$MAX_AGE_HOURS" =~ ^[0-9]+$ ]] || { echo "MAINTENANCE_MAX_AGE_HOURS must be an integer" >&2; exit 2; }
CHECK_ONLY=false
ASSUME_YES=false
@@ -118,14 +123,30 @@ launcher=$(psql_pg -tAc \
# so_telegraf before the postgres state lands, postgres after.
cron_db=$(docker exec so-postgres psql -U postgres -tAc \
"SELECT current_setting('cron.database_name', true)" | tr -d '[:space:]')
run_count=0
recent_ok=0
last_run=never
if [[ -n "$cron_db" ]]; then
last_run=$(docker exec so-postgres psql -U postgres -d "$cron_db" -tAc \
"SELECT coalesce(max(d.start_time)::text, 'never')
has_run_details=$([[ -n "$cron_db" ]] && docker exec so-postgres psql -U postgres -d "$cron_db" -tAc \
"SELECT to_regclass('cron.job_run_details') IS NOT NULL" 2>/dev/null || echo f)
if [[ "$has_run_details" == t ]]; then
runs=$(docker exec so-postgres psql -U postgres -d "$cron_db" -tA -F '|' -c \
"SELECT count(*),
count(*) FILTER (WHERE d.status = 'succeeded'
AND d.start_time > now() - make_interval(hours => $MAX_AGE_HOURS)),
coalesce((array_agg(format('%s at %s', d.status, d.start_time)
ORDER BY d.start_time DESC))[1], 'never')
FROM cron.job_run_details d JOIN cron.job j USING (jobid)
WHERE j.jobname = '$JOB_NAME'" 2>/dev/null | tr -d '[:space:]' || echo unknown)
[[ -n "$last_run" ]] || last_run=never
WHERE j.jobname = '$JOB_NAME'" 2>/dev/null) \
|| { echo "Could not read $JOB_NAME runs from cron.job_run_details; cannot judge maintenance health." >&2; exit 2; }
IFS='|' read -r run_count recent_ok last_run <<< "$runs"
fi
# An unreadable run history exits 2 rather than 1: soup repairs on 1, which
# truncates the default partitions, and a transient query error must not do that.
# No runs at all means a freshly scheduled job (or pg_cron not loaded, handled
# by restart_needed); the partition checks catch a job that never fires. Runs
# with no recent success mean the job fires but fails, e.g. cannot connect.
maintenance_failing=false
[[ "$run_count" -gt 0 && "$recent_ok" -eq 0 ]] && maintenance_failing=true
# A grid that has never written a metric has nothing to recover, and an empty
# cron_db means pg_cron is not loaded at all, which no restart fixes.
@@ -137,6 +158,7 @@ repair_needed=false
[[ "$behind" -gt 0 ]] && repair_needed=true
[[ "$misconfigured" -gt 0 ]] && repair_needed=true
$restart_needed && repair_needed=true
$maintenance_failing && repair_needed=true
echo "Telegraf partition status:"
psql_tg -c "$REPORT"
@@ -157,6 +179,7 @@ if $CHECK_ONLY; then
[[ "$behind" -gt 0 ]] && echo " * $behind parent(s) with no partition for the current window"
[[ "$misconfigured" -gt 0 ]] && echo " * $misconfigured parent(s) with stale partman settings"
$restart_needed && echo " * pg_cron's launcher is dead; maintenance is not running at all"
$maintenance_failing && echo " * no successful $JOB_NAME run in the last $MAX_AGE_HOURS hour(s)"
echo
echo "Re-run without --check to repair."
exit 1
@@ -241,5 +264,6 @@ EOSQL
echo
echo "Telegraf partition status after repair:"
psql_tg -c "$REPORT"
echo "The $JOB_NAME job runs hourly at :17. Confirm it fired with:"
echo "The $JOB_NAME job runs hourly at :17. Until its next run succeeds, grid status"
echo "may still report so-postgres-maintenance as failed. Confirm it fired with:"
echo " so-telegraf-repair --check"
+38 -15
View File
@@ -976,27 +976,50 @@ docker_seed_update() {
set_progress_str "$docker_seed_update_percent" "Downloading $name"
}
registry_tar_has_soversion() {
local tarfile=$1
local version=$2
[ -n "$version" ] || return 1
[ -f "$tarfile" ] || return 1
# grep -q in a pipe SIGPIPEs tar; under pipefail a match would look like failure.
grep -qF "/_manifests/tags/${version}/" < <(tar -tf "$tarfile" 2>/dev/null)
}
docker_seed_registry() {
local VERSION="$SOVERSION"
local REGISTRY_TAR=/nsm/docker-registry/docker/registry.tar
if [ -f /nsm/docker-registry/docker/registry.tar ]; then
logCmd "tar xvf /nsm/docker-registry/docker/registry.tar -C /nsm/docker-registry/docker"
logCmd "rm /nsm/docker-registry/docker/registry.tar"
if [ -f "$REGISTRY_TAR" ] && registry_tar_has_soversion "$REGISTRY_TAR" "$VERSION"; then
info "Extracting $REGISTRY_TAR"
if ! tar xvf "$REGISTRY_TAR" -C /nsm/docker-registry/docker >> "$setup_log" 2>&1; then
error "Failed to extract $REGISTRY_TAR"
return 1
fi
logCmd "rm -f $REGISTRY_TAR"
return 0
elif [ -f "$REGISTRY_TAR" ]; then
info "Not using $REGISTRY_TAR; it has no images tagged '${VERSION}'"
if [[ "$is_airgap" == true ]]; then
error "Airgap install: $REGISTRY_TAR does not match Security Onion version '${VERSION}'. Use the ISO for this version."
return 1
fi
elif [[ -d /nsm/docker-registry/docker/registry && ( -f /etc/SOCLOUD || "$is_airgap" == true ) ]]; then
echo "Using existing docker registry content"
else
if [ "$install_type" == 'IMPORT' ]; then
container_list 'so-import'
else
container_list
fi
docker_seed_update_percent=25
update_docker_containers 'netinstall' '' 'docker_seed_update' '/dev/stdout' 2>&1 | tee -a "$setup_log"
# Use pipe exit status of 'update_docker_containers' for return code
return ${PIPESTATUS[0]}
return 0
fi
if [ "$install_type" == 'IMPORT' ]; then
container_list 'so-import'
else
container_list
fi
docker_seed_update_percent=25
update_docker_containers 'netinstall' '' 'docker_seed_update' '/dev/stdout' 2>&1 | tee -a "$setup_log"
# Use pipe exit status of 'update_docker_containers' for return code
return ${PIPESTATUS[0]}
}
elasticfleet_pillar() {