mirror of
https://github.com/Security-Onion-Solutions/securityonion.git
synced 2026-08-11 18:36:37 +02:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7400e3dffa | ||
|
|
a87a910585 | ||
|
|
539389c78e | ||
|
|
65e81d3b3a | ||
|
|
546462c77f | ||
|
|
a7ddb7a975 | ||
|
|
ee1d2167e8 | ||
|
|
2d0ea48c39 | ||
|
|
23d92316c1 | ||
|
|
e5346af068 | ||
|
|
9762523849 | ||
|
|
e8ab6433ab | ||
|
|
9c20ef60f4 | ||
|
|
6abf382ea8 | ||
|
|
36833fdad1 | ||
|
|
a92d10a1e3 | ||
|
|
d3da6b3939 |
File diff suppressed because one or more lines are too long
@@ -220,6 +220,16 @@ logrotate:
|
||||
- extension .log
|
||||
- dateext
|
||||
- dateyesterday
|
||||
/opt/so/log/salt/so-soup-grid-highstate:
|
||||
- daily
|
||||
- rotate 14
|
||||
- missingok
|
||||
- copytruncate
|
||||
- compress
|
||||
- create
|
||||
- extension .log
|
||||
- dateext
|
||||
- dateyesterday
|
||||
/nsm/idh/*_x_log:
|
||||
- daily
|
||||
- rotate 14
|
||||
|
||||
@@ -140,6 +140,13 @@ logrotate:
|
||||
multiline: True
|
||||
global: True
|
||||
forcedType: "[]string"
|
||||
"/opt/so/log/salt/so-soup-grid-highstate":
|
||||
description: List of logrotate options for this file.
|
||||
title: /opt/so/log/salt/so-soup-grid-highstate
|
||||
advanced: True
|
||||
multiline: True
|
||||
global: True
|
||||
forcedType: "[]string"
|
||||
"/nsm/idh/*_x_log":
|
||||
description: List of logrotate options for this file.
|
||||
title: /nsm/idh/*.log
|
||||
|
||||
@@ -138,6 +138,8 @@ function getinstallinfo() {
|
||||
log "ERROR" "Failed to source install variables"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "INFO" "Fetched install info for $MINION_ID (node type: ${NODETYPE:-unset})"
|
||||
}
|
||||
|
||||
function pcapspace() {
|
||||
@@ -483,6 +485,7 @@ function add_sensoroni_with_analyze_to_minion() {
|
||||
|
||||
# Sensor settings for the minion pillar
|
||||
function add_sensor_to_minion() {
|
||||
log "INFO" "Writing sensor configuration for $MINION_ID (interface: ${INTERFACE:-unset})"
|
||||
{
|
||||
echo "sensor:"
|
||||
echo " interface: '$INTERFACE'"
|
||||
@@ -509,6 +512,8 @@ function add_sensor_to_minion() {
|
||||
log "ERROR" "Failed to add sensor configuration to $PILLARFILE"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "INFO" "Wrote sensor configuration for $MINION_ID"
|
||||
}
|
||||
|
||||
function add_elastalert_to_minion() {
|
||||
@@ -581,11 +586,14 @@ function add_telegraf_to_minion() {
|
||||
# generates a password on first add and is a no-op on re-add so the cred
|
||||
# is stable across repeated so-minion runs. postgres.telegraf_users on the
|
||||
# manager creates/updates the DB role from the same pillar.
|
||||
so-telegraf-cred add "$MINION_ID"
|
||||
if [ $? -ne 0 ]; then
|
||||
log "ERROR" "Failed to provision postgres telegraf cred for $MINION_ID"
|
||||
return 1
|
||||
fi
|
||||
log "INFO" "Provisioning postgres telegraf credential for $MINION_ID"
|
||||
so-telegraf-cred add "$MINION_ID"
|
||||
local result=$?
|
||||
if [ $result -ne 0 ]; then
|
||||
log "ERROR" "Failed to provision postgres telegraf cred for $MINION_ID (exit code: $result)"
|
||||
return 1
|
||||
fi
|
||||
log "INFO" "Provisioned postgres telegraf credential for $MINION_ID"
|
||||
}
|
||||
|
||||
function add_influxdb_to_minion() {
|
||||
@@ -1043,7 +1051,7 @@ function updateMineAndApplyStates() {
|
||||
}
|
||||
|
||||
function setupMinionFiles() {
|
||||
log "INFO" "Setting up minion files for $MINION_ID"
|
||||
log "INFO" "Setting up minion files for $MINION_ID (pillar: $PILLARFILE)"
|
||||
|
||||
# Check to see if nodetype is set
|
||||
if [ -z $NODETYPE ]; then
|
||||
@@ -1069,7 +1077,10 @@ function setupMinionFiles() {
|
||||
fi
|
||||
|
||||
# Create node-specific configuration
|
||||
create$NODETYPE || return 1
|
||||
create$NODETYPE || {
|
||||
log "ERROR" "Failed to create $NODETYPE configuration for $MINION_ID"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Ensure proper ownership after all content is written
|
||||
ensure_socore_ownership || return 1
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
||||
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
|
||||
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||
# Elastic License 2.0.
|
||||
|
||||
# so-soup-grid-highstate
|
||||
# ======================
|
||||
# Drives a batched, role-tiered highstate across every non-manager minion in the
|
||||
# grid. soup fires this (detached) after it finishes upgrading the manager so the
|
||||
# rest of the grid converges immediately instead of waiting for its own scheduled
|
||||
# highstate -- which, since the schedule moved from 15 minutes to 120 minutes
|
||||
# (salt:schedule:highstate_interval_minutes), could otherwise leave nodes on the
|
||||
# old version for up to ~2.5 hours (interval + splay) while the manager runs new code.
|
||||
#
|
||||
# Work is done by the existing orch.push_batch orchestration (salt/orch/push_batch.sls),
|
||||
# the same runner the active-push drainer uses, so batching/queueing behavior matches.
|
||||
# Tiers are dispatched in declaration order: searchnodes/heavynodes (Elasticsearch data
|
||||
# nodes) first, then receivers, then everything else -- so the data tier converges before
|
||||
# the ingest tier before sensors/fleet/idh/etc.
|
||||
#
|
||||
# When soup also upgraded Salt itself, remote minions must first highstate onto the new
|
||||
# salt-minion package (top.sls gates every real state on G@saltversion, so a stale-version
|
||||
# minion only gets salt.minion until it upgrades and reconnects). --salt-upgraded runs that
|
||||
# preliminary pass and waits for the fleet to settle before the tiered pass.
|
||||
#
|
||||
# This is best-effort: soup has already completed by the time this runs, and the 120-minute
|
||||
# scheduled highstate remains the backstop for any node that is offline or missed a batch.
|
||||
|
||||
LOG_FILE=/opt/so/log/salt/so-soup-grid-highstate
|
||||
LOCK_FILE=/opt/so/state/so-soup-grid-highstate.lock
|
||||
SETTLE_MAX_WAIT=${GRID_HIGHSTATE_SETTLE_WAIT:-900} # backstop for the post-salt-upgrade settle loop
|
||||
SETTLE_INTERVAL=15
|
||||
SETTLE_STABLE_CHECKS=3
|
||||
# salt-minion on an upgraded node restarts ~30s after the upgrade state runs
|
||||
# (salt/salt/minion/init.sls start_minion_post_upgrade); wait past that before sampling
|
||||
# so the settle loop sees the drop-off instead of settling on the pre-restart set.
|
||||
SETTLE_INITIAL_WAIT=${GRID_HIGHSTATE_SETTLE_INITIAL_WAIT:-45}
|
||||
|
||||
BATCH=""
|
||||
BATCH_WAIT=""
|
||||
SALT_UPGRADED=false
|
||||
REASON="manual"
|
||||
|
||||
log() {
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') | $*" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "Usage: so-soup-grid-highstate [--batch <spec>] [--batch-wait <sec>] [--salt-upgraded] [--reason <text>]"
|
||||
exit 1
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--batch) BATCH="$2"; shift 2 ;;
|
||||
--batch-wait) BATCH_WAIT="$2"; shift 2 ;;
|
||||
--salt-upgraded) SALT_UPGRADED=true; shift ;;
|
||||
--reason) REASON="$2"; shift 2 ;;
|
||||
-h|--help) usage ;;
|
||||
*) echo "Unknown option: $1"; usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
mkdir -p "$(dirname "$LOG_FILE")" "$(dirname "$LOCK_FILE")"
|
||||
|
||||
# Serialize: a second invocation (e.g. two soups, or a manual run overlapping soup's)
|
||||
# should not dispatch a competing set of batches.
|
||||
exec 9>"$LOCK_FILE"
|
||||
if ! flock -n 9; then
|
||||
log "another so-soup-grid-highstate is already running (lock $LOCK_FILE held); exiting"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Resolve batch settings from the salt:auto_apply pillar when not overridden on the
|
||||
# command line, falling back to the same defaults orch.push_batch/salt.defaults use.
|
||||
if [ -z "$BATCH" ]; then
|
||||
BATCH=$(salt-call --out=newline_values_only pillar.get salt:auto_apply:batch 2>/dev/null)
|
||||
[ -z "$BATCH" ] && BATCH='25%'
|
||||
fi
|
||||
if [ -z "$BATCH_WAIT" ]; then
|
||||
BATCH_WAIT=$(salt-call --out=newline_values_only pillar.get salt:auto_apply:batch_wait 2>/dev/null)
|
||||
[ -z "$BATCH_WAIT" ] && BATCH_WAIT=15
|
||||
fi
|
||||
|
||||
MINIONID=$(salt-call --local --out=newline_values_only grains.get id 2>/dev/null)
|
||||
[ -z "$MINIONID" ] && MINIONID=$(cat /etc/salt/minion_id 2>/dev/null)
|
||||
if [ -z "$MINIONID" ]; then
|
||||
log "could not determine this minion's id; aborting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Single-node grids (eval/standalone/import with no other accepted keys) have nothing
|
||||
# remote to push -- the manager already highstated during soup.
|
||||
NUM_ACCEPTED=$(salt-key --out=json --list=accepted 2>/dev/null | jq -r '.minions | length' 2>/dev/null)
|
||||
NUM_ACCEPTED=${NUM_ACCEPTED:-0}
|
||||
if [ "$NUM_ACCEPTED" -le 1 ]; then
|
||||
log "single node grid ($NUM_ACCEPTED accepted minion(s)); nothing to push (reason=$REASON)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "starting grid highstate: reason=$REASON minion=$MINIONID accepted=$NUM_ACCEPTED batch=$BATCH batch_wait=$BATCH_WAIT salt_upgraded=$SALT_UPGRADED"
|
||||
|
||||
# Count minions currently responsive on the bus (includes this manager).
|
||||
count_up() {
|
||||
salt-run manage.up --out=json 2>/dev/null \
|
||||
| python3 -c 'import sys,json; print(len(json.load(sys.stdin)))' 2>/dev/null
|
||||
}
|
||||
|
||||
# Dispatch a single synchronous orch.push_batch run for the given actions JSON.
|
||||
# Synchronous is fine: soup launched us detached, so blocking here does not hold soup up.
|
||||
# expect_restart=true marks a dispatch (the salt-upgrade pass) where a non-zero rc is normal
|
||||
# because targets restart salt-minion mid-run -- so we don't log a misleading failure warning.
|
||||
dispatch() {
|
||||
local desc="$1"
|
||||
local actions="$2"
|
||||
local expect_restart="${3:-false}"
|
||||
local rc
|
||||
log "dispatching $desc"
|
||||
salt-run state.orchestrate orch.push_batch pillar="{\"actions\": $actions}" >>"$LOG_FILE" 2>&1
|
||||
rc=$?
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
log "$desc dispatch completed (rc=0)"
|
||||
elif [ "$expect_restart" = "true" ]; then
|
||||
log "$desc returned rc=$rc; this is expected during a salt upgrade (targets restart salt-minion mid-run). Waiting for them to reconnect before the tiered pass."
|
||||
else
|
||||
log "WARNING: $desc dispatch returned rc=$rc; nodes it missed will converge on the scheduled highstate"
|
||||
fi
|
||||
}
|
||||
|
||||
# Wait for the reachable minion set to recover to its pre-upgrade size and hold steady.
|
||||
# Used after the salt-upgrade pass, where targets restart salt-minion (~30s delayed, see
|
||||
# salt/salt/minion/init.sls) and drop off the bus before reconnecting on the new version.
|
||||
# target = how many minions were reachable just before the pass; requiring up >= target keeps
|
||||
# us from releasing the tiered pass while nodes are still down for their restart (settling on
|
||||
# the not-yet-restarted subset). We deliberately compare against the pre-upgrade reachable
|
||||
# count, not accepted keys, so a node an operator intentionally powered off never stalls us.
|
||||
# Bounded by SETTLE_MAX_WAIT.
|
||||
wait_for_settle() {
|
||||
local target="$1"
|
||||
local elapsed=0 prev=-1 stable=0 up=0
|
||||
# Let the delayed salt-minion restart begin before we start counting stability, otherwise
|
||||
# we could see the pre-restart set as "stable" and settle before the drop-off even happens.
|
||||
sleep "$SETTLE_INITIAL_WAIT"
|
||||
elapsed=$SETTLE_INITIAL_WAIT
|
||||
while [ "$elapsed" -lt "$SETTLE_MAX_WAIT" ]; do
|
||||
up=$(count_up); up=${up:-0}
|
||||
if [ "$up" -ge "$target" ] && [ "$up" -eq "$prev" ]; then
|
||||
stable=$((stable + 1))
|
||||
[ "$stable" -ge "$SETTLE_STABLE_CHECKS" ] && break
|
||||
else
|
||||
stable=0
|
||||
fi
|
||||
prev=$up
|
||||
sleep "$SETTLE_INTERVAL"
|
||||
elapsed=$((elapsed + SETTLE_INTERVAL))
|
||||
done
|
||||
if [ "$up" -ge "$target" ]; then
|
||||
log "fleet recovered to ${up} minions up (>= pre-upgrade ${target}) after ${elapsed}s"
|
||||
else
|
||||
log "WARNING: ${SETTLE_MAX_WAIT}s settle backstop hit; only ${up}/${target} pre-upgrade minions back up; proceeding (stragglers converge on the scheduled highstate)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Pass 0: when Salt itself was upgraded, remote minions still on the old version only match
|
||||
# top.sls's 'not G@saltversion' block (salt.minion, which performs the package upgrade). Push
|
||||
# an untiered highstate so they upgrade+reconnect, then wait for them to come back before the
|
||||
# real tiered pass applies the new version's states.
|
||||
if [ "$SALT_UPGRADED" = "true" ]; then
|
||||
PRE_UP=$(count_up); PRE_UP=${PRE_UP:-1}
|
||||
log "pre-upgrade reachable minions (incl. this manager): $PRE_UP"
|
||||
dispatch "salt-upgrade pass (all remote minions)" \
|
||||
"[{\"highstate\": true, \"tgt\": \"not $MINIONID\", \"tgt_type\": \"compound\", \"batch\": \"$BATCH\", \"batch_wait\": $BATCH_WAIT}]" \
|
||||
true
|
||||
log "waiting for minions to reconnect on the new salt version"
|
||||
wait_for_settle "$PRE_UP"
|
||||
fi
|
||||
|
||||
# Tiered pass: Elasticsearch data nodes first, then receivers, then the remainder. The last
|
||||
# tier is defined as the complement of the earlier tiers (and of this manager) so coverage is
|
||||
# exhaustive -- sensors, fleet, idh, desktop, hypervisor, and any future role are all included.
|
||||
TIER_TGTS=(
|
||||
"( *_searchnode or *_heavynode ) and not $MINIONID"
|
||||
"*_receiver and not $MINIONID"
|
||||
"not $MINIONID and not *_searchnode and not *_heavynode and not *_receiver"
|
||||
)
|
||||
|
||||
# Count minions a compound target matches, using the master's key/cache data (no execution).
|
||||
tier_count() {
|
||||
salt --out=json -C "$1" --preview-target 2>/dev/null | jq 'length' 2>/dev/null
|
||||
}
|
||||
|
||||
# Build the actions JSON, including only tiers that actually match minions. An empty target
|
||||
# would make orch.push_batch's salt.state step return "No minions returned" -- a failure --
|
||||
# even though nothing needed to run, and grids commonly lack a tier (no receiver, etc.).
|
||||
# Keep the JSON on a single line: salt parses `pillar=<value>` kwargs with a non-DOTALL
|
||||
# regex, so an embedded newline makes it treat the whole token as a positional saltenv
|
||||
# instead ("No matching salt environment for environment 'pillar=...'").
|
||||
actions=""
|
||||
for tgt in "${TIER_TGTS[@]}"; do
|
||||
n=$(tier_count "$tgt"); n=${n:-0}
|
||||
if [ "$n" -ge 1 ]; then
|
||||
[ -n "$actions" ] && actions="$actions, "
|
||||
actions="$actions{\"highstate\": true, \"tgt\": \"$tgt\", \"tgt_type\": \"compound\", \"batch\": \"$BATCH\", \"batch_wait\": $BATCH_WAIT}"
|
||||
log "tier matched $n minion(s): $tgt"
|
||||
else
|
||||
log "tier matched 0 minions, skipping: $tgt"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$actions" ]; then
|
||||
log "no remote minions matched any tier; nothing to push (reason=$REASON)"
|
||||
exit 0
|
||||
fi
|
||||
dispatch "tiered pass (searchnodes/heavynodes -> receivers -> remainder)" "[$actions]"
|
||||
|
||||
log "grid highstate complete (reason=$REASON)"
|
||||
exit 0
|
||||
@@ -24,7 +24,10 @@ else
|
||||
POSTVERSION=$INSTALLEDVERSION
|
||||
fi
|
||||
INSTALLEDSALTVERSION=$(salt --versions-report | grep Salt: | awk '{print $2}')
|
||||
BATCHSIZE=5
|
||||
# Optional -b override for the grid highstate batch size (a count like "5" or a
|
||||
# percentage like "25%"). Empty means so-soup-grid-highstate uses the salt:auto_apply:batch
|
||||
# pillar default.
|
||||
BATCHSIZE=
|
||||
SOUP_LOG=/root/soup.log
|
||||
SOUP_DEBUG_LOG=/root/soup-debug.log
|
||||
WHATWOULDYOUSAYYAHDOHERE=soup
|
||||
@@ -452,6 +455,31 @@ highstate() {
|
||||
salt-call state.highstate -l info queue=True
|
||||
}
|
||||
|
||||
push_grid_highstate() {
|
||||
# Drive a batched, role-tiered highstate across the rest of the grid so remote minions
|
||||
# pick up this upgrade now instead of waiting up to ~2.5 hours for their own scheduled
|
||||
# highstate (the schedule moved from 15 to 120 minutes). so-soup-grid-highstate does the work
|
||||
# via orch.push_batch; it only exists once the manager highstate has deployed this
|
||||
# version's sbin files, so guard on it. Launch fully detached (setsid) so it survives an
|
||||
# SSH drop, and never let it affect soup's exit status -- it is best-effort with the
|
||||
# scheduled highstate as backstop.
|
||||
if [[ ! -x /usr/sbin/so-soup-grid-highstate ]]; then
|
||||
echo "so-soup-grid-highstate not present; remote nodes will converge on their scheduled highstate."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local extra_args=()
|
||||
if [[ $SALTUPGRADED == true || $UPGRADESALT -eq 1 ]]; then
|
||||
extra_args+=(--salt-upgraded)
|
||||
fi
|
||||
if [[ -n "$BATCHSIZE" ]]; then
|
||||
extra_args+=(--batch "$BATCHSIZE")
|
||||
fi
|
||||
|
||||
echo "Dispatching a grid-wide highstate to remote nodes. Progress: /opt/so/log/salt/so-soup-grid-highstate"
|
||||
setsid nohup /usr/sbin/so-soup-grid-highstate --reason soup "${extra_args[@]}" >/dev/null 2>&1 &
|
||||
}
|
||||
|
||||
masterlock() {
|
||||
echo "Locking Salt Master"
|
||||
mv -v $TOPFILE $BACKUPTOPFILE
|
||||
@@ -1013,30 +1041,9 @@ up_to_3.3.0() {
|
||||
INSTALLEDVERSION=3.3.0
|
||||
}
|
||||
|
||||
telegraf_repair() {
|
||||
# Only grids whose Telegraf partitions stalled need this; --check exits 1
|
||||
# when there is something to repair, so everyone else is left alone.
|
||||
local repair=/usr/sbin/so-telegraf-repair
|
||||
[[ -x "$repair" ]] || return 0
|
||||
docker ps --format '{{.Names}}' | grep -qx so-postgres || return 0
|
||||
|
||||
echo "Checking Telegraf metric partitions."
|
||||
local status=0
|
||||
"$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 \
|
||||
|| echo " warning: so-telegraf-repair failed; run it manually" >&2 ;;
|
||||
*) echo " Skipping; Telegraf is not storing metrics in Postgres on this host." ;;
|
||||
esac
|
||||
}
|
||||
|
||||
post_to_3.3.0() {
|
||||
# Recollate again since some internal DBs were excluded during 3.2.0 soup
|
||||
recollate_postgres
|
||||
|
||||
telegraf_repair
|
||||
}
|
||||
### 3.3.0 End ###
|
||||
|
||||
@@ -1969,6 +1976,9 @@ main() {
|
||||
# rather than reporting "already latest". The soversion/pillar writes in
|
||||
# update_version are no-ops here since the version is unchanged for a hotfix.
|
||||
update_version
|
||||
# Push the hotfix out to the rest of the grid rather than waiting for the scheduled
|
||||
# highstate. Hotfixes never upgrade Salt, so no --salt-upgraded pass is needed.
|
||||
push_grid_highstate
|
||||
else
|
||||
SOUP_UPGRADE_STARTED=true
|
||||
echo ""
|
||||
@@ -2135,13 +2145,18 @@ main() {
|
||||
|
||||
if [[ $NUM_MINIONS -gt 1 ]]; then
|
||||
|
||||
# Actively drive the rest of the grid to this version now. The scheduled highstate
|
||||
# runs only every 120 minutes (salt:schedule:highstate_interval_minutes), so without
|
||||
# this remote nodes could sit on the old version for a couple of hours after soup finishes.
|
||||
push_grid_highstate
|
||||
|
||||
cat << EOF
|
||||
|
||||
|
||||
|
||||
This appears to be a distributed deployment. Other nodes should update themselves at the next Salt highstate (typically within 15 minutes). Do not manually restart anything until you know that all the search/heavy nodes in your deployment are updated. This is especially important if you are using true clustering for Elasticsearch.
|
||||
This appears to be a distributed deployment. soup has dispatched a batched, grid-wide highstate to update the other nodes now: Elasticsearch data nodes (search/heavy nodes) first, then receivers, then sensors and the remaining nodes. Progress is logged to /opt/so/log/salt/so-soup-grid-highstate, and you can watch nodes update from the Grid section of SOC. Do not manually restart anything until you know that all the search/heavy nodes in your deployment are updated. This is especially important if you are using true clustering for Elasticsearch.
|
||||
|
||||
Each minion is on a random 15 minute check-in period and things like network bandwidth can be a factor in how long the actual upgrade takes. If you have a heavy node on a slow link, it is going to take a while to get the containers to it. Depending on what changes happened between the versions, Elasticsearch might not be able to talk to said heavy node until the update is complete.
|
||||
Nodes are updated in batches, and things like network bandwidth can be a factor in how long the actual upgrade takes. If you have a heavy node on a slow link, it is going to take a while to get the containers to it. Depending on what changes happened between the versions, Elasticsearch might not be able to talk to said heavy node until the update is complete. Any node that is offline or missed a batch will converge on its own scheduled highstate (every 120 minutes by default).
|
||||
|
||||
If it looks like you’re missing data after the upgrade, please avoid restarting services and instead make sure at least one search node has completed its upgrade. The best way to do this is to run 'sudo salt-call state.highstate' from a search node and make sure there are no errors. Typically if it works on one node it will work on the rest. Sensor nodes are less complex and will update as they check in so you can monitor those from the Grid section of SOC.
|
||||
|
||||
@@ -2181,8 +2196,10 @@ while getopts ":b:f:y" opt; do
|
||||
case ${opt} in
|
||||
b )
|
||||
BATCHSIZE="$OPTARG"
|
||||
if ! [[ "$BATCHSIZE" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "Batch size must be a number greater than 0."
|
||||
# Accept either a plain count (e.g. 5) or a percentage (e.g. 25%); passed through
|
||||
# to so-soup-grid-highstate --batch, which salt's batch/batch_wait accepts in both forms.
|
||||
if ! [[ "$BATCHSIZE" =~ ^[1-9][0-9]*%?$ ]]; then
|
||||
echo "Batch size must be a number greater than 0, optionally with a trailing % (e.g. 5 or 25%)."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
@@ -16,4 +16,4 @@ postgres:
|
||||
logging_collector: 'off'
|
||||
log_min_messages: 'warning'
|
||||
shared_preload_libraries: pg_cron
|
||||
cron.database_name: postgres
|
||||
cron.database_name: so_telegraf
|
||||
|
||||
@@ -83,7 +83,7 @@ postgres:
|
||||
advanced: True
|
||||
helpLink: postgres
|
||||
cron.database_name:
|
||||
description: Database pg_cron keeps its job metadata in. Must already exist when PostgreSQL starts, because pg_cron's launcher connects to it at startup and never retries if it is missing. The maintenance job itself targets so_telegraf.
|
||||
description: Database pg_cron schedules jobs in. Must be so_telegraf so partman maintenance runs in the right database context.
|
||||
global: True
|
||||
advanced: True
|
||||
helpLink: postgres
|
||||
|
||||
@@ -47,12 +47,7 @@ trap 'rm -f "$TMPFILE"' EXIT
|
||||
|
||||
# Dump all databases and roles, compress. Write to a temp file so the final
|
||||
# filename only ever appears for a complete, verified backup.
|
||||
#
|
||||
# so_telegraf is excluded: it is transient metrics on a short retention window,
|
||||
# it dominates the dump size, and it is rebuilt automatically after a restore --
|
||||
# init-db.sh recreates the database and Telegraf recreates its tables on first
|
||||
# write. Roles are globals, so the per-minion telegraf logins are still dumped.
|
||||
if ! docker exec so-postgres pg_dumpall -U postgres --exclude-database=so_telegraf | gzip > "$TMPFILE"; then
|
||||
if ! docker exec so-postgres pg_dumpall -U postgres | gzip > "$TMPFILE"; then
|
||||
log "ERROR: pg_dumpall/gzip failed; backup aborted"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -5,18 +5,11 @@ set -e
|
||||
# Usage: so-telegraf-postgres <subcommand>
|
||||
# create_db Ensure the so_telegraf database exists.
|
||||
# group_role Provision the so_telegraf group role, telegraf/partman schemas,
|
||||
# pg_partman, the so_admin maintenance routines, and the hourly
|
||||
# pg_cron maintenance job.
|
||||
# pg_partman, pg_cron, and the hourly partman maintenance job.
|
||||
# user Create or update a per-minion login role granted to so_telegraf.
|
||||
# Env: ROLE_USER, ROLE_PASS.
|
||||
# retention Reconcile partman retention and premake on telegraf parents.
|
||||
# retention Reconcile partman retention 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.
|
||||
#
|
||||
# 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
|
||||
# on a grid already stuck in that state.
|
||||
|
||||
cmd="${1:?subcommand required}"
|
||||
|
||||
@@ -43,6 +36,7 @@ CREATE SCHEMA IF NOT EXISTS telegraf AUTHORIZATION so_telegraf;
|
||||
GRANT USAGE, CREATE ON SCHEMA telegraf TO so_telegraf;
|
||||
CREATE SCHEMA IF NOT EXISTS partman;
|
||||
CREATE EXTENSION IF NOT EXISTS pg_partman SCHEMA partman;
|
||||
CREATE EXTENSION IF NOT EXISTS pg_cron;
|
||||
-- Telegraf (running as so_telegraf) calls partman.create_parent()
|
||||
-- on first write of each metric, which needs USAGE on the partman
|
||||
-- schema, EXECUTE on its functions/procedures, and write access to
|
||||
@@ -57,141 +51,12 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA partman
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO so_telegraf;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA partman
|
||||
GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO so_telegraf;
|
||||
|
||||
-- pg_cron runs these as postgres, so they must not sit in a schema any
|
||||
-- Telegraf role can create objects in.
|
||||
CREATE SCHEMA IF NOT EXISTS so_admin AUTHORIZATION postgres;
|
||||
REVOKE ALL ON SCHEMA so_admin FROM PUBLIC;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE so_admin.telegraf_maintenance()
|
||||
LANGUAGE plpgsql
|
||||
AS $proc$
|
||||
DECLARE
|
||||
r record;
|
||||
v_default text;
|
||||
v_rows bigint;
|
||||
BEGIN
|
||||
-- No per-parent EXCEPTION handler: partition_data_proc commits internally,
|
||||
-- and COMMIT is illegal while a subtransaction is active. A failing parent
|
||||
-- aborts the run and the next pass retries.
|
||||
FOR r IN
|
||||
SELECT parent_table, retention
|
||||
FROM partman.part_config
|
||||
WHERE parent_table LIKE 'telegraf.%'
|
||||
ORDER BY parent_table
|
||||
LOOP
|
||||
v_default := format('%I.%I',
|
||||
split_part(r.parent_table, '.', 1),
|
||||
split_part(r.parent_table, '.', 2) || '_default');
|
||||
|
||||
CONTINUE WHEN to_regclass(v_default) IS NULL;
|
||||
|
||||
EXECUTE format('SELECT count(*) FROM %s', v_default) INTO v_rows;
|
||||
CONTINUE WHEN v_rows = 0;
|
||||
|
||||
RAISE WARNING 'so_admin.telegraf_maintenance: % rows stranded in %, draining',
|
||||
v_rows, v_default;
|
||||
|
||||
-- Cheaper to delete expired rows than to repartition and then drop them.
|
||||
IF r.retention IS NOT NULL THEN
|
||||
EXECUTE format('DELETE FROM %s WHERE "time" < now() - %L::interval',
|
||||
v_default, r.retention);
|
||||
COMMIT;
|
||||
END IF;
|
||||
|
||||
-- Bounded so a large backlog drains across several runs.
|
||||
CALL partman.partition_data_proc(
|
||||
p_parent_table := r.parent_table,
|
||||
p_loop_count := 200,
|
||||
p_source_table := v_default
|
||||
);
|
||||
COMMIT;
|
||||
END LOOP;
|
||||
|
||||
CALL partman.run_maintenance_proc();
|
||||
END;
|
||||
$proc$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION so_admin.telegraf_partition_status()
|
||||
RETURNS TABLE (
|
||||
parent_table text,
|
||||
oldest_child date,
|
||||
newest_child date,
|
||||
days_ahead int,
|
||||
retention text,
|
||||
default_rows bigint,
|
||||
default_size text
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
AS $func$
|
||||
DECLARE
|
||||
r record;
|
||||
v_default regclass;
|
||||
BEGIN
|
||||
FOR r IN
|
||||
SELECT pc.parent_table AS pt, pc.retention AS ret
|
||||
FROM partman.part_config pc
|
||||
WHERE pc.parent_table LIKE 'telegraf.%'
|
||||
ORDER BY pc.parent_table
|
||||
LOOP
|
||||
parent_table := r.pt;
|
||||
retention := r.ret;
|
||||
|
||||
SELECT min(d), max(d) INTO oldest_child, newest_child
|
||||
FROM (
|
||||
SELECT to_date(substring(c.relname FROM '_p(\d{8})$'), 'YYYYMMDD') AS d
|
||||
FROM pg_inherits i
|
||||
JOIN pg_class c ON c.oid = i.inhrelid
|
||||
WHERE i.inhparent = r.pt::regclass
|
||||
AND pg_get_expr(c.relpartbound, c.oid) <> 'DEFAULT'
|
||||
) s;
|
||||
|
||||
days_ahead := newest_child - current_date;
|
||||
|
||||
v_default := to_regclass(format('%I.%I',
|
||||
split_part(r.pt, '.', 1),
|
||||
split_part(r.pt, '.', 2) || '_default'));
|
||||
IF v_default IS NULL THEN
|
||||
default_rows := NULL;
|
||||
default_size := NULL;
|
||||
ELSE
|
||||
EXECUTE format('SELECT count(*) FROM %s', v_default::text) INTO default_rows;
|
||||
default_size := pg_size_pretty(pg_total_relation_size(v_default));
|
||||
END IF;
|
||||
|
||||
RETURN NEXT;
|
||||
END LOOP;
|
||||
END;
|
||||
$func$;
|
||||
|
||||
-- Drop the registration older releases left in so_telegraf.
|
||||
SELECT CASE
|
||||
WHEN current_setting('cron.database_name', true) IS DISTINCT FROM current_database()
|
||||
AND EXISTS (SELECT 1 FROM pg_catalog.pg_extension WHERE extname = 'pg_cron')
|
||||
THEN 'true' ELSE 'false'
|
||||
END AS drop_stale_cron \gset
|
||||
\if :drop_stale_cron
|
||||
DROP EXTENSION pg_cron CASCADE;
|
||||
\endif
|
||||
EOSQL
|
||||
|
||||
# Guarded on the live GUC so applying this before the postgresql.conf change
|
||||
# has restarted the container skips rather than failing.
|
||||
docker exec -i so-postgres psql -v ON_ERROR_STOP=1 -U postgres -d postgres <<'EOSQL'
|
||||
SELECT CASE WHEN current_setting('cron.database_name', true) = current_database()
|
||||
THEN 'true' ELSE 'false' END AS cron_here \gset
|
||||
\if :cron_here
|
||||
CREATE EXTENSION IF NOT EXISTS pg_cron;
|
||||
-- cron.schedule_in_database is idempotent by jobname.
|
||||
SELECT cron.schedule_in_database(
|
||||
-- Hourly partman maintenance. cron.schedule is idempotent by jobname.
|
||||
SELECT cron.schedule(
|
||||
'telegraf-partman-maintenance',
|
||||
'17 * * * *',
|
||||
'CALL so_admin.telegraf_maintenance()',
|
||||
'so_telegraf'
|
||||
'CALL partman.run_maintenance_proc()'
|
||||
);
|
||||
\else
|
||||
\echo 'pg_cron metadata database is not `postgres` yet; skipping job registration.'
|
||||
\endif
|
||||
EOSQL
|
||||
;;
|
||||
|
||||
@@ -225,8 +90,6 @@ EOSQL
|
||||
: "${RETENTION_DAYS:?RETENTION_DAYS is required}"
|
||||
# \gset + \if guards against a missing pg_partman without using a DO
|
||||
# block (psql :var substitution doesn't reach into dollar-quoted code).
|
||||
# premake is reconciled here because telegraf.conf only applies it to
|
||||
# parents created from now on.
|
||||
docker exec -i so-postgres psql \
|
||||
-v ON_ERROR_STOP=1 \
|
||||
-v retention_days="$RETENTION_DAYS" \
|
||||
@@ -234,60 +97,14 @@ EOSQL
|
||||
SELECT CASE WHEN EXISTS (SELECT 1 FROM pg_catalog.pg_extension WHERE extname = 'pg_partman')
|
||||
THEN 'true' ELSE 'false' END AS has_partman \gset
|
||||
\if :has_partman
|
||||
-- infinite_time_partitions so a gap in metrics does not stop partman from
|
||||
-- premaking forward, which is what leaves everything in the default.
|
||||
UPDATE partman.part_config
|
||||
SET retention = :'retention_days' || ' days',
|
||||
retention_keep_table = false,
|
||||
premake = 7,
|
||||
infinite_time_partitions = true
|
||||
retention_keep_table = false
|
||||
WHERE parent_table LIKE 'telegraf.%';
|
||||
\endif
|
||||
EOSQL
|
||||
;;
|
||||
|
||||
maintenance)
|
||||
docker exec -i so-postgres psql -v ON_ERROR_STOP=1 -U postgres -d so_telegraf <<'EOSQL'
|
||||
SELECT CASE WHEN to_regproc('so_admin.telegraf_maintenance') IS NOT NULL
|
||||
THEN 'true' ELSE 'false' END AS has_proc \gset
|
||||
\if :has_proc
|
||||
CALL so_admin.telegraf_maintenance();
|
||||
\else
|
||||
\echo 'so_admin.telegraf_maintenance() is missing; run so-telegraf-postgres group_role first.'
|
||||
\endif
|
||||
EOSQL
|
||||
;;
|
||||
|
||||
check)
|
||||
docker exec -i so-postgres psql -U postgres -d so_telegraf <<'EOSQL'
|
||||
\pset border 2
|
||||
SELECT * FROM so_admin.telegraf_partition_status();
|
||||
EOSQL
|
||||
docker exec -i so-postgres psql -U postgres -d postgres <<'EOSQL'
|
||||
\pset border 2
|
||||
SELECT CASE WHEN to_regclass('cron.job_run_details') IS NOT NULL
|
||||
THEN 'true' ELSE 'false' END AS has_cron \gset
|
||||
\if :has_cron
|
||||
SELECT d.status, d.return_message, d.start_time
|
||||
FROM cron.job_run_details d
|
||||
JOIN cron.job j ON j.jobid = d.jobid
|
||||
WHERE j.jobname = 'telegraf-partman-maintenance'
|
||||
ORDER BY d.start_time DESC
|
||||
LIMIT 5;
|
||||
\else
|
||||
\echo 'pg_cron is not installed in this database.'
|
||||
\endif
|
||||
EOSQL
|
||||
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
|
||||
exit 1
|
||||
fi
|
||||
echo "so-telegraf-postgres check: all telegraf parents healthy"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown subcommand: $cmd" >&2
|
||||
exit 1
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
||||
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
|
||||
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||
# Elastic License 2.0.
|
||||
|
||||
# Put Telegraf metrics storage back in service on a grid where pg_partman
|
||||
# maintenance stalled: raises premake, discards the rows stranded in default
|
||||
# partitions, restarts so-postgres if pg_cron's launcher is dead, and runs
|
||||
# maintenance once. Healthy grids are reported and left alone.
|
||||
#
|
||||
# Usage: so-telegraf-repair [--check] [--yes] [--no-restart]
|
||||
# --check Report health and change nothing.
|
||||
# --yes Skip the confirmation prompt (for soup and other automation).
|
||||
# --no-restart Never restart so-postgres, even if pg_cron's launcher is dead.
|
||||
#
|
||||
# Exit status:
|
||||
# 0 healthy, or repair completed
|
||||
# 1 repair is needed (--check only)
|
||||
# 2 cannot run here: so-postgres, so_telegraf or pg_partman is missing
|
||||
|
||||
set -e
|
||||
|
||||
# Matches p_premake in telegraf.conf's create_parent template.
|
||||
PREMAKE=7
|
||||
JOB_NAME=telegraf-partman-maintenance
|
||||
|
||||
CHECK_ONLY=false
|
||||
ASSUME_YES=false
|
||||
NO_RESTART=false
|
||||
|
||||
usage() { sed -n '/^# Usage:/,/^# 2 /p' "$0" | sed 's/^# \?//'; }
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--check|--dry-run) CHECK_ONLY=true ;;
|
||||
--yes|-y) ASSUME_YES=true ;;
|
||||
--no-restart) NO_RESTART=true ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
skip() { echo "$*"; exit 2; }
|
||||
|
||||
psql_tg() { docker exec -i so-postgres psql -U postgres -d so_telegraf "$@"; }
|
||||
psql_pg() { docker exec -i so-postgres psql -U postgres -d postgres "$@"; }
|
||||
|
||||
# query_to_xml so the per-table row counts need no helper function installed.
|
||||
REPORT="
|
||||
WITH parents AS (
|
||||
SELECT pc.parent_table,
|
||||
pc.premake,
|
||||
split_part(pc.parent_table, '.', 1) AS sch,
|
||||
split_part(pc.parent_table, '.', 2) AS tbl
|
||||
FROM partman.part_config pc
|
||||
WHERE pc.parent_table LIKE 'telegraf.%'
|
||||
), children AS (
|
||||
SELECT p.parent_table,
|
||||
max(to_date(substring(c.relname FROM '_p(\d{8})\$'), 'YYYYMMDD')) AS newest_child
|
||||
FROM parents p
|
||||
JOIN pg_class pt ON pt.oid = p.parent_table::regclass
|
||||
JOIN pg_inherits i ON i.inhparent = pt.oid
|
||||
JOIN pg_class c ON c.oid = i.inhrelid
|
||||
WHERE pg_get_expr(c.relpartbound, c.oid) <> 'DEFAULT'
|
||||
GROUP BY p.parent_table
|
||||
), defaults AS (
|
||||
SELECT p.parent_table,
|
||||
p.premake,
|
||||
format('%I.%I', p.sch, p.tbl || '_default') AS default_table,
|
||||
to_regclass(format('%I.%I', p.sch, p.tbl || '_default')) AS default_oid
|
||||
FROM parents p
|
||||
)
|
||||
SELECT d.parent_table,
|
||||
c.newest_child,
|
||||
(c.newest_child - current_date) AS days_ahead,
|
||||
d.premake,
|
||||
CASE WHEN d.default_oid IS NULL THEN NULL ELSE
|
||||
(xpath('/row/cnt/text()',
|
||||
query_to_xml(format('SELECT count(*) AS cnt FROM %s', d.default_table),
|
||||
false, true, '')))[1]::text::bigint
|
||||
END AS default_rows,
|
||||
CASE WHEN d.default_oid IS NULL THEN NULL
|
||||
ELSE pg_size_pretty(pg_total_relation_size(d.default_oid)) END AS default_size
|
||||
FROM defaults d
|
||||
LEFT JOIN children c ON c.parent_table = d.parent_table
|
||||
ORDER BY 1
|
||||
"
|
||||
|
||||
docker ps --format '{{.Names}}' | grep -qx so-postgres \
|
||||
|| skip "so-postgres is not running; nothing to repair."
|
||||
docker exec so-postgres psql -U postgres -tAc \
|
||||
"SELECT 1 FROM pg_database WHERE datname='so_telegraf'" | grep -q 1 \
|
||||
|| skip "The so_telegraf database does not exist; Telegraf is not writing to Postgres."
|
||||
psql_tg -tAc "SELECT 1 FROM pg_extension WHERE extname='pg_partman'" | grep -q 1 \
|
||||
|| skip "pg_partman is not installed in so_telegraf; nothing to repair."
|
||||
|
||||
parents=$(psql_tg -tAc \
|
||||
"SELECT count(*) FROM partman.part_config WHERE parent_table LIKE 'telegraf.%'")
|
||||
stranded=$(psql_tg -tAc "SELECT coalesce(sum(default_rows), 0) FROM ( $REPORT ) t")
|
||||
behind=$(psql_tg -tAc \
|
||||
"SELECT count(*) FROM ( $REPORT ) t WHERE coalesce(days_ahead, -1) < 1")
|
||||
# premake < 7, or infinite_time_partitions off: without the latter partman
|
||||
# refuses to premake forward across the gap the stall left behind.
|
||||
misconfigured=$(psql_tg -tAc \
|
||||
"SELECT count(*) FROM partman.part_config
|
||||
WHERE parent_table LIKE 'telegraf.%'
|
||||
AND (premake < $PREMAKE OR NOT infinite_time_partitions)")
|
||||
|
||||
# Both columns are matched because which one carries the launcher's name varies
|
||||
# with the pg_cron version.
|
||||
launcher=$(psql_pg -tAc \
|
||||
"SELECT count(*) FROM pg_stat_activity
|
||||
WHERE backend_type ILIKE '%pg_cron%' OR application_name ILIKE '%pg_cron%'")
|
||||
|
||||
# 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:]')
|
||||
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')
|
||||
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
|
||||
fi
|
||||
|
||||
# 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.
|
||||
restart_needed=false
|
||||
[[ "$launcher" -eq 0 && "$parents" -gt 0 && -n "$cron_db" ]] && restart_needed=true
|
||||
|
||||
repair_needed=false
|
||||
[[ "$stranded" -gt 0 ]] && repair_needed=true
|
||||
[[ "$behind" -gt 0 ]] && repair_needed=true
|
||||
[[ "$misconfigured" -gt 0 ]] && repair_needed=true
|
||||
$restart_needed && repair_needed=true
|
||||
|
||||
echo "Telegraf partition status:"
|
||||
psql_tg -c "$REPORT"
|
||||
echo "Rows stranded in default partitions: $stranded"
|
||||
echo "pg_cron metadata database: ${cron_db:-unset}"
|
||||
echo "pg_cron launcher running: $([[ "$launcher" -gt 0 ]] && echo yes || echo no)"
|
||||
echo "Last $JOB_NAME run: $last_run"
|
||||
echo
|
||||
|
||||
if ! $repair_needed; then
|
||||
echo "Telegraf partitions are healthy. Nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if $CHECK_ONLY; then
|
||||
echo "Repair is needed:"
|
||||
[[ "$stranded" -gt 0 ]] && echo " * $stranded row(s) stranded in default partitions"
|
||||
[[ "$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"
|
||||
echo
|
||||
echo "Re-run without --check to repair."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$stranded" -gt 0 ]] && ! $ASSUME_YES; then
|
||||
echo "This will permanently discard the $stranded stranded row(s) above."
|
||||
$restart_needed && ! $NO_RESTART && \
|
||||
echo "so-postgres will also be restarted, which briefly interrupts SOC."
|
||||
[[ -t 0 ]] || { echo "Not a terminal; re-run with --yes to confirm." >&2; exit 2; }
|
||||
read -r -p "Continue? [y/N] " answer
|
||||
[[ "$answer" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; }
|
||||
fi
|
||||
|
||||
if [[ "$misconfigured" -gt 0 ]]; then
|
||||
echo "Reconciling partman settings on $misconfigured parent(s)."
|
||||
# GREATEST so an operator who raised premake further keeps their value.
|
||||
psql_tg -v ON_ERROR_STOP=1 -c \
|
||||
"UPDATE partman.part_config
|
||||
SET premake = GREATEST(premake, $PREMAKE),
|
||||
infinite_time_partitions = true
|
||||
WHERE parent_table LIKE 'telegraf.%'"
|
||||
fi
|
||||
|
||||
if [[ "$stranded" -gt 0 ]]; then
|
||||
echo "Clearing default partitions."
|
||||
# One transaction: Telegraf is still writing, so a default emptied without
|
||||
# its partition in place is refilled before maintenance can attach one.
|
||||
psql_tg -v ON_ERROR_STOP=1 <<'EOSQL'
|
||||
DO $$
|
||||
DECLARE
|
||||
r record;
|
||||
BEGIN
|
||||
FOR r IN
|
||||
SELECT pc.parent_table,
|
||||
format('%I.%I', n.nspname, c.relname) AS default_table
|
||||
FROM partman.part_config pc
|
||||
JOIN pg_class p ON p.oid = pc.parent_table::regclass
|
||||
JOIN pg_inherits i ON i.inhparent = p.oid
|
||||
JOIN pg_class c ON c.oid = i.inhrelid
|
||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE pc.parent_table LIKE 'telegraf.%'
|
||||
AND pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT'
|
||||
LOOP
|
||||
EXECUTE format('TRUNCATE TABLE %s', r.default_table);
|
||||
PERFORM partman.create_partition_time(
|
||||
r.parent_table, ARRAY[date_trunc('day', now())]::timestamptz[]);
|
||||
END LOOP;
|
||||
END
|
||||
$$;
|
||||
EOSQL
|
||||
fi
|
||||
|
||||
if $restart_needed; then
|
||||
if $NO_RESTART; then
|
||||
echo "WARNING: pg_cron's launcher is dead and --no-restart was given."
|
||||
echo " Maintenance will not run on its own until so-postgres is restarted."
|
||||
else
|
||||
echo "Restarting so-postgres to revive pg_cron's launcher."
|
||||
docker restart so-postgres >/dev/null
|
||||
for _ in $(seq 1 60); do
|
||||
docker exec so-postgres pg_isready -U postgres -q 2>/dev/null && break
|
||||
sleep 2
|
||||
done
|
||||
docker exec so-postgres pg_isready -U postgres -q \
|
||||
|| { echo "so-postgres did not come back; check 'docker logs so-postgres'." >&2; exit 1; }
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Running partition maintenance."
|
||||
# so_admin.telegraf_maintenance() only exists once the postgres state has landed.
|
||||
psql_tg -v ON_ERROR_STOP=1 <<'EOSQL'
|
||||
SELECT CASE WHEN to_regproc('so_admin.telegraf_maintenance') IS NOT NULL
|
||||
THEN 'true' ELSE 'false' END AS has_proc \gset
|
||||
\if :has_proc
|
||||
CALL so_admin.telegraf_maintenance();
|
||||
\else
|
||||
CALL partman.run_maintenance_proc();
|
||||
\endif
|
||||
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 " so-telegraf-repair --check"
|
||||
@@ -36,8 +36,8 @@ soc:
|
||||
icon: fa-stream
|
||||
target: ''
|
||||
links:
|
||||
- '/joblookup?esid={:soc_id}&time={:@timestamp}&gridId={gridId}'
|
||||
- '/joblookup?ncid={:network.community_id}&time={:@timestamp}&gridId={gridId}'
|
||||
- '/api/joblookup?esid={:soc_id}&time={:@timestamp}&gridId={gridId}'
|
||||
- '/api/joblookup?ncid={:network.community_id}&time={:@timestamp}&gridId={gridId}'
|
||||
categories:
|
||||
- hunt
|
||||
- alerts
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
create_templates = [
|
||||
'''CREATE TABLE IF NOT EXISTS {{ .table }} ({{ .columns }}) PARTITION BY RANGE ("time")''',
|
||||
'''ALTER TABLE {{ .table }} ALTER COLUMN "time" SET NOT NULL''',
|
||||
'''SELECT partman.create_parent(p_parent_table := {{ printf "%s.%s" .table.Schema .table.Name | quoteLiteral }}, p_control := 'time', p_type := 'range', p_interval := '1 day', p_premake := 7) WHERE NOT EXISTS (SELECT 1 FROM partman.part_config WHERE parent_table = {{ printf "%s.%s" .table.Schema .table.Name | quoteLiteral }})'''
|
||||
'''SELECT partman.create_parent(p_parent_table := {{ printf "%s.%s" .table.Schema .table.Name | quoteLiteral }}, p_control := 'time', p_type := 'range', p_interval := '1 day', p_premake := 3) WHERE NOT EXISTS (SELECT 1 FROM partman.part_config WHERE parent_table = {{ printf "%s.%s" .table.Schema .table.Name | quoteLiteral }})'''
|
||||
]
|
||||
tag_table_create_templates = [
|
||||
'''CREATE TABLE IF NOT EXISTS {{ .table }} ({{ .columns }}, PRIMARY KEY (tag_id))'''
|
||||
|
||||
+2
-2
@@ -971,8 +971,8 @@ docker_seed_registry() {
|
||||
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"
|
||||
elif [ -d /nsm/docker-registry/docker/registry ] && [ -f /etc/SOCLOUD ]; then
|
||||
echo "Using existing docker registry content for cloud install"
|
||||
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'
|
||||
|
||||
Reference in New Issue
Block a user