mirror of
https://github.com/andsyrovatko/s4k-ip-manager.git
synced 2026-04-21 22:18:53 +02:00
Initial release: Billing infrastructure automation (IP manager).
This commit is contained in:
Executable
+386
@@ -0,0 +1,386 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Script Name : ip_manager.sh
|
||||
# Description : Manage customer IPs in IPSET sets (allow / restrict)
|
||||
# Called by BOSS4 billing system on customer state changes.
|
||||
#
|
||||
# Commands (also PARAMETERS):
|
||||
# NEW IP — new customer: add to ALLOW set
|
||||
# DELETE IP — delete/prototype: remove from BOTH sets
|
||||
# RESTRICT IP — no payment: add to BOTH sets (allow + restrict)
|
||||
# SUSPEND IP — manual suspend: remove from BOTH sets
|
||||
# RESUME IP — resume: remove from restrict, add to allow (if not there)
|
||||
# UPDATE IP_OLD IP_NEW — IP/tariff change: replace OLD with NEW in each set
|
||||
#
|
||||
# Usage:
|
||||
# ./ip_manager.sh COMMAND IP # for NEW/DELETE/RESTRICT/SUSPEND/RESUME
|
||||
# ./ip_manager.sh UPDATE IP_OLD IP_NEW # for UPDATE
|
||||
#
|
||||
# Author : syr4ok (Andrii Syrovatko)
|
||||
# Version : 2.0.1r
|
||||
# =============================================================================
|
||||
|
||||
# --- STRICT MODE ---
|
||||
set -uo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
# --- Configuration Loader ---
|
||||
CONFIG_FILE="$(dirname "$0")/ip_manager.conf"
|
||||
|
||||
if [[ -f "$CONFIG_FILE" ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
source "$CONFIG_FILE"
|
||||
else
|
||||
echo "Error: Configuration file not found. Create ip_manager.conf from example."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Environment & Tools ---
|
||||
IPSET_BIN=$(which ipset 2>/dev/null || true)
|
||||
MAIL_BIN=$(which mail 2>/dev/null || true)
|
||||
|
||||
if [[ -z "$IPSET_BIN" ]]; then
|
||||
echo "Error: 'ipset' utility not found. Please install it."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$MAIL_BIN" ]]; then
|
||||
echo "Warning: 'mail' utility not found. Notifications will be disabled."
|
||||
fi
|
||||
|
||||
# --- LOG COLORS ---
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
# INIT — ensure log directory and file exist
|
||||
mkdir -p "${LOG_DIR}" 2>/dev/null || true
|
||||
CAN_LOG_TO_FILE=0
|
||||
if touch "${LOG_FILE}" 2>/dev/null; then
|
||||
CAN_LOG_TO_FILE=1
|
||||
echo -e "${BLUE}Info: Log file (${LOG_FILE}) created or exists. Logging to file enabled.${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}Warning: Cannot write to log file (${LOG_FILE}). Logging to file disabled.${NC}"
|
||||
fi
|
||||
# --- LOGGING ---
|
||||
log() {
|
||||
local level="$1"; shift
|
||||
local msg="$*"
|
||||
local ts
|
||||
ts=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# Clear colors for file logging (keep colors in console, but remove them from file logs)
|
||||
local plain_msg
|
||||
plain_msg=$(echo -e "${msg}" | sed 's/\x1B\[[0-9;]*[mK]//g')
|
||||
|
||||
# Write logs to file if possible
|
||||
if [[ "${CAN_LOG_TO_FILE}" -eq 1 ]]; then
|
||||
echo "${ts} [${level}] ${plain_msg}" >> "${LOG_FILE}"
|
||||
fi
|
||||
|
||||
# Type logs tto console forever
|
||||
echo -e "${ts} [${level}] ${msg}"
|
||||
}
|
||||
|
||||
log_info() { log "INFO " "${GREEN}${*}${NC}"; }
|
||||
log_warn() { log "WARN " "${YELLOW}${*}${NC}"; }
|
||||
log_error() { log "ERROR" "${RED}${*}${NC}"; }
|
||||
log_debug() { log "DEBUG" "${CYAN}${*}${NC}"; }
|
||||
log_sep() {
|
||||
log "INFO " "${BLUE}------------------------------------------------------------------------------${NC}"
|
||||
}
|
||||
|
||||
# LOCK — single instance with wait queue (flock-based)
|
||||
acquire_lock() {
|
||||
local call_args
|
||||
call_args="$(IFS=" "; echo "$*")"
|
||||
exec 9>"${LOCK_FILE}"
|
||||
log_debug "Waiting for lock (timeout: ${LOCK_TIMEOUT}s) PID=$$"
|
||||
if ! flock -w "${LOCK_TIMEOUT}" 9; then
|
||||
log_error "Could not acquire lock after ${LOCK_TIMEOUT}s — another instance hung? Aborting!"
|
||||
|
||||
if [[ -n "${MAIL_BIN}" ]]; then
|
||||
echo -e "WARN: $0\nProcess for [${call_args}] timed out by lockfile (${LOCK_FILE})\nafter allowed ${LOCK_TIMEOUT}s waiting.\nexit 9" \
|
||||
| "${MAIL_BIN}" -s "WARN: Process timed out by lockfile after ${LOCK_TIMEOUT}s" "$MAIL_RECEIVER"
|
||||
fi
|
||||
exit 9
|
||||
fi
|
||||
echo $$ >&9
|
||||
log_debug "Lock acquired by PID=$$"
|
||||
}
|
||||
|
||||
release_lock() {
|
||||
flock -u 9
|
||||
exec 9>&-
|
||||
log_debug "Lock released by PID=$$"
|
||||
}
|
||||
|
||||
# --- HELPERS ---
|
||||
usage() {
|
||||
echo -e "${BOLD}Usage:${NC}"
|
||||
echo -e " $0 ${CYAN}NEW${NC} IP"
|
||||
echo -e " $0 ${CYAN}DELETE${NC} IP"
|
||||
echo -e " $0 ${CYAN}RESTRICT${NC} IP"
|
||||
echo -e " $0 ${CYAN}SUSPEND${NC} IP"
|
||||
echo -e " $0 ${CYAN}RESUME${NC} IP"
|
||||
echo -e " $0 ${CYAN}UPDATE${NC} IP_OLD IP_NEW"
|
||||
echo
|
||||
}
|
||||
|
||||
strip_mask() {
|
||||
local val="${1}"
|
||||
val="${val// /}" # trim all spaces
|
||||
echo "${val%%/32}"
|
||||
}
|
||||
|
||||
# Validate IPv4 address (without mask)
|
||||
is_valid_ip() {
|
||||
local ip="$1"
|
||||
local octet='(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)'
|
||||
[[ "${ip}" =~ ^${octet}\.${octet}\.${octet}\.${octet}$ ]]
|
||||
}
|
||||
|
||||
ipset_exists() {
|
||||
local set="$1"
|
||||
local ip="$2"
|
||||
# grep -q closes pipe early after first match → ipset gets SIGPIPE → pipefail returns 141
|
||||
# fix: read full ipset output first, then grep from variable
|
||||
local dump
|
||||
dump=$("${IPSET_BIN}" -L "${set}" 2>/dev/null)
|
||||
grep -qw "^${ip}$" <<< "${dump}"
|
||||
}
|
||||
|
||||
ipset_add() {
|
||||
local set="$1"
|
||||
local ip="$2"
|
||||
|
||||
if ipset_exists "${set}" "${ip}"; then
|
||||
log_warn "IP ${ip} already in set [${set}] — skipping add"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_debug "ipset add ${set} ${ip}"
|
||||
if [[ "${DRY_RUN}" -eq 1 ]]; then
|
||||
log_warn "[DRY-RUN] Would ADD ${ip} to [${set}]"
|
||||
return 0
|
||||
fi
|
||||
if ! "${IPSET_BIN}" add "${set}" "${ip}" 2>&1 | tee -a "${LOG_FILE}"; then
|
||||
log_error "Failed to ADD ${ip} to [${set}]"
|
||||
return 1
|
||||
fi
|
||||
log_info "Added ${ip} → [${set}]"
|
||||
}
|
||||
|
||||
ipset_del() {
|
||||
local set="$1"
|
||||
local ip="$2"
|
||||
|
||||
if ! ipset_exists "${set}" "${ip}"; then
|
||||
log_warn "IP ${ip} not found in set [${set}] — skipping del"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_debug "ipset del ${set} ${ip}"
|
||||
if [[ "${DRY_RUN}" -eq 1 ]]; then
|
||||
log_warn "[DRY-RUN] Would DEL ${ip} from [${set}]"
|
||||
return 0
|
||||
fi
|
||||
if ! "${IPSET_BIN}" del "${set}" "${ip}" 2>&1 | tee -a "${LOG_FILE}"; then
|
||||
log_error "Failed to DEL ${ip} from [${set}]"
|
||||
return 1
|
||||
fi
|
||||
log_info "Removed ${ip} ← [${set}]"
|
||||
}
|
||||
|
||||
ipset_save() {
|
||||
log_debug "Saving ipset state to ${IPSET_SAVE_FILE}"
|
||||
if [[ "${DRY_RUN}" -eq 1 ]]; then
|
||||
log_warn "[DRY-RUN] Would save ipset state to ${IPSET_SAVE_FILE}"
|
||||
return 0
|
||||
fi
|
||||
if ! "${IPSET_BIN}" save > "${IPSET_SAVE_FILE}" 2>&1; then
|
||||
log_error "Failed to save ipset state to ${IPSET_SAVE_FILE}"
|
||||
return 1
|
||||
fi
|
||||
log_info "ipset state saved → ${IPSET_SAVE_FILE}"
|
||||
}
|
||||
|
||||
# --- COMMAND HANDLERS ---
|
||||
cmd_new() {
|
||||
local ip="$1"
|
||||
log_info "[NEW] New customer IP: ${ip} — adding to allow set"
|
||||
ipset_add "${ALLOW_SET}" "${ip}"
|
||||
ipset_save
|
||||
}
|
||||
|
||||
cmd_delete() {
|
||||
local ip="$1"
|
||||
log_info "[DELETE] Removing ${ip} from BOTH sets"
|
||||
ipset_del "${ALLOW_SET}" "${ip}"
|
||||
ipset_del "${RESTRICT_SET}" "${ip}"
|
||||
ipset_save
|
||||
}
|
||||
|
||||
cmd_restrict() {
|
||||
local ip="$1"
|
||||
log_info "[RESTRICT] Adding ${ip} to ALLOW + RESTRICT sets (no payment)"
|
||||
ipset_add "${ALLOW_SET}" "${ip}"
|
||||
ipset_add "${RESTRICT_SET}" "${ip}"
|
||||
ipset_save
|
||||
}
|
||||
|
||||
cmd_suspend() {
|
||||
local ip="$1"
|
||||
log_info "[SUSPEND] Removing ${ip} from BOTH sets (manual suspend)"
|
||||
ipset_del "${ALLOW_SET}" "${ip}"
|
||||
ipset_del "${RESTRICT_SET}" "${ip}"
|
||||
ipset_save
|
||||
}
|
||||
|
||||
cmd_resume() {
|
||||
local ip="$1"
|
||||
log_info "[RESUME] Resuming ${ip}: remove from restrict, ensure in allow"
|
||||
ipset_del "${RESTRICT_SET}" "${ip}"
|
||||
ipset_add "${ALLOW_SET}" "${ip}"
|
||||
ipset_save
|
||||
}
|
||||
|
||||
cmd_update() {
|
||||
local ip_old="$1"
|
||||
local ip_new="$2"
|
||||
log_info "[UPDATE] Replacing ${ip_old} → ${ip_new}"
|
||||
|
||||
local changed=0
|
||||
|
||||
if ipset_exists "${ALLOW_SET}" "${ip_old}"; then
|
||||
log_debug "${ip_old} found in [${ALLOW_SET}] — swapping"
|
||||
ipset_del "${ALLOW_SET}" "${ip_old}"
|
||||
ipset_add "${ALLOW_SET}" "${ip_new}"
|
||||
changed=1
|
||||
else
|
||||
log_warn "${ip_old} not found in [${ALLOW_SET}] — no change in this set"
|
||||
fi
|
||||
|
||||
if ipset_exists "${RESTRICT_SET}" "${ip_old}"; then
|
||||
log_debug "${ip_old} found in [${RESTRICT_SET}] — swapping"
|
||||
ipset_del "${RESTRICT_SET}" "${ip_old}"
|
||||
ipset_add "${RESTRICT_SET}" "${ip_new}"
|
||||
changed=1
|
||||
else
|
||||
log_warn "${ip_old} not found in [${RESTRICT_SET}] — no change in this set"
|
||||
fi
|
||||
|
||||
if [[ "${changed}" -eq 0 ]]; then
|
||||
log_warn "[UPDATE] ${ip_old} was not found in ANY set — nothing was changed"
|
||||
fi
|
||||
|
||||
ipset_save
|
||||
}
|
||||
|
||||
# MAIN EXIT HELPER — always logs final status + releases lock before return
|
||||
main_exit() {
|
||||
local code="$1"
|
||||
if [[ "${code}" -eq 0 ]]; then
|
||||
log_info "Done. Exit 0 (OK)"
|
||||
else
|
||||
log_error "Finished with errors (code: ${code})"
|
||||
fi
|
||||
# log_sep
|
||||
release_lock
|
||||
# sleep 45 # DEBUG: keep lock released for a while to allow testing concurrent runs and lock timeout
|
||||
return "${code}"
|
||||
}
|
||||
|
||||
# MAIN
|
||||
main() {
|
||||
local error_code=0
|
||||
|
||||
log_sep
|
||||
log_info "Invoked: $0"
|
||||
log_info "Arguments ($#): $(IFS=" "; echo "$*")"
|
||||
acquire_lock "$@"
|
||||
[[ "${DRY_RUN}" -eq 1 ]] && log_warn "=== DRY-RUN MODE — no real ipset actions will be performed ==="
|
||||
|
||||
if [[ $# -lt 2 ]]; then
|
||||
log_error "Not enough arguments! At least 2 required, got $#"
|
||||
usage
|
||||
main_exit 1; return
|
||||
fi
|
||||
|
||||
local raw_command="${1^^}"
|
||||
local ip_arg1
|
||||
local ip_arg2
|
||||
|
||||
ip_arg1=$(strip_mask "${2:-}")
|
||||
ip_arg2=$(strip_mask "${3:-}")
|
||||
|
||||
log_info "COMMAND: ${raw_command} | IP1: ${ip_arg1:-—} | IP2: ${ip_arg2:-—}"
|
||||
|
||||
# Validate IP1
|
||||
if [[ -z "${ip_arg1}" ]]; then
|
||||
log_error "IP1 is empty or blank!"
|
||||
main_exit 1; return
|
||||
fi
|
||||
if ! is_valid_ip "${ip_arg1}"; then
|
||||
log_error "IP1 is not a valid IPv4 address: '${ip_arg1}'"
|
||||
main_exit 1; return
|
||||
fi
|
||||
|
||||
# Validate IP2 only for UPDATE
|
||||
if [[ "${raw_command}" == "UPDATE" ]]; then
|
||||
if [[ $# -ne 3 ]]; then
|
||||
log_error "UPDATE requires exactly 2 IPs: IP_OLD IP_NEW, got $(( $# - 1 )) IP(s)"
|
||||
usage
|
||||
main_exit 1; return
|
||||
fi
|
||||
if [[ -z "${ip_arg2}" ]]; then
|
||||
log_warn "[UPDATE] IP_NEW is empty or blank — nothing to do"
|
||||
main_exit 0; return
|
||||
fi
|
||||
if ! is_valid_ip "${ip_arg2}"; then
|
||||
log_error "IP2 (IP_NEW) is not a valid IPv4 address: '${ip_arg2}'"
|
||||
main_exit 1; return
|
||||
fi
|
||||
if [[ "${ip_arg1}" == "${ip_arg2}" ]]; then
|
||||
log_warn "[UPDATE] IP_OLD == IP_NEW (${ip_arg1}) — nothing to do"
|
||||
main_exit 0; return
|
||||
fi
|
||||
fi
|
||||
|
||||
case "${raw_command}" in
|
||||
|
||||
NEW | DELETE | RESTRICT | SUSPEND | RESUME)
|
||||
if [[ $# -gt 2 ]]; then
|
||||
log_error "Command ${raw_command} accepts exactly 1 IP, but got $(( $# - 1 )): ${*:2}"
|
||||
usage
|
||||
main_exit 1; return
|
||||
fi
|
||||
case "${raw_command}" in
|
||||
NEW) cmd_new "${ip_arg1}" ;;
|
||||
DELETE) cmd_delete "${ip_arg1}" ;;
|
||||
RESTRICT) cmd_restrict "${ip_arg1}" ;;
|
||||
SUSPEND) cmd_suspend "${ip_arg1}" ;;
|
||||
RESUME) cmd_resume "${ip_arg1}" ;;
|
||||
esac
|
||||
;;
|
||||
|
||||
UPDATE)
|
||||
cmd_update "${ip_arg1}" "${ip_arg2}"
|
||||
;;
|
||||
|
||||
*)
|
||||
log_error "Unknown command: '${raw_command}'"
|
||||
usage
|
||||
main_exit 5; return
|
||||
;;
|
||||
esac
|
||||
|
||||
main_exit "${error_code}"
|
||||
}
|
||||
|
||||
main "$@"; _ec=$?
|
||||
[[ "${DRY_RUN}" -eq 1 ]] && exit 0 || exit "${_ec}"
|
||||
Reference in New Issue
Block a user