#!/usr/bin/env bash ######################################################################################### #### service-dependency-mapper.sh — Map systemd service dependencies #### #### Shows what depends on a service and what it requires #### #### #### #### Author: Phil Connor #### #### Contact: contact@mylinux.work #### #### License: MIT #### #### Version 1.00 #### #### #### #### Usage: #### #### ./service-dependency-mapper.sh nginx #### #### ./service-dependency-mapper.sh --all --format tree #### #### ./service-dependency-mapper.sh postgresql --reverse #### #### ./service-dependency-mapper.sh --critical #### #### #### #### See --help for all options. #### ######################################################################################### set -euo pipefail # ── Defaults ────────────────────────────────────────────────────────── SERVICE="" MODE="forward" MAP_ALL="false" CRITICAL="false" DEPTH=3 FORMAT="tree" COLOR="auto" # ── State ───────────────────────────────────────────────────────────── SCRIPT_NAME="$(basename "$0")" readonly SCRIPT_NAME # ── Colors ──────────────────────────────────────────────────────────── RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET="" setup_colors() { if [[ "$COLOR" == "never" ]]; then RED="" GREEN="" YELLOW="" BOLD="" DIM="" CYAN="" RESET="" return fi if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m' else RED="" GREEN="" YELLOW="" BOLD="" DIM="" CYAN="" RESET="" fi } # ── Logging ─────────────────────────────────────────────────────────── err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; } warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; } # ══════════════════════════════════════════════════════════════════════ # USAGE # ══════════════════════════════════════════════════════════════════════ usage() { cat </dev/null || echo "unknown" } # Get load state of a unit get_load_state() { systemctl show -p LoadState --value "$1" 2>/dev/null || echo "unknown" } # Get unit description get_description() { systemctl show -p Description --value "$1" 2>/dev/null || echo "" } # Color a unit name based on its active state color_unit() { local unit="$1" local state state="$(get_active_state "$unit")" case "$state" in active) echo -e "${GREEN}${unit}${RESET}" ;; failed) echo -e "${RED}${unit}${RESET}" ;; inactive) echo -e "${YELLOW}${unit}${RESET}" ;; *) echo -e "${DIM}${unit}${RESET}" ;; esac } # Get list of all active units (services, sockets, timers, targets) get_active_units() { systemctl list-units --type=service,socket,timer,target --state=active \ --no-pager --no-legend --plain 2>/dev/null | awk '{print $1}' } # ══════════════════════════════════════════════════════════════════════ # DEPENDENCY COLLECTION # ══════════════════════════════════════════════════════════════════════ # Collect dependencies using systemctl list-dependencies # Args: unit, direction (forward|reverse), max_depth # Outputs lines: "depth unit" (tab-indented raw from systemctl, parsed) collect_deps() { local unit="$1" local direction="$2" local max_depth="$3" local reverse_flag="" [[ "$direction" == "reverse" ]] && reverse_flag="--reverse" # systemctl list-dependencies outputs an indented tree with unicode chars # We parse the depth from leading whitespace and extract the unit name systemctl list-dependencies "$unit" $reverse_flag --no-pager --plain 2>/dev/null | \ tail -n +2 | while IFS= read -r line; do # Count leading spaces (each level = 2 spaces) local stripped="${line#"${line%%[! ]*}"}" local spaces=$(( ${#line} - ${#stripped} )) local depth=$(( spaces / 2 )) # Respect depth limit if [[ $depth -ge $max_depth ]]; then continue fi # Clean unit name (remove any tree-drawing characters) local dep_unit dep_unit="$(echo "$stripped" | sed 's/[●○]//g' | xargs)" if [[ -n "$dep_unit" ]]; then echo "${depth} ${dep_unit}" fi done } # ══════════════════════════════════════════════════════════════════════ # SERVICE INFO # ══════════════════════════════════════════════════════════════════════ print_service_info() { local unit="$1" local active_state load_state description active_state="$(get_active_state "$unit")" load_state="$(get_load_state "$unit")" description="$(get_description "$unit")" echo "" echo -e "${BOLD}Service Info${RESET}" echo " ──────────────────────────────────────────────────────────────" echo -e " Name: ${BOLD}${unit}${RESET}" echo -e " Description: ${description}" echo -e " Load State: ${load_state}" case "$active_state" in active) echo -e " Active State: ${GREEN}${active_state}${RESET}" ;; failed) echo -e " Active State: ${RED}${active_state}${RESET}" ;; inactive) echo -e " Active State: ${YELLOW}${active_state}${RESET}" ;; *) echo -e " Active State: ${DIM}${active_state}${RESET}" ;; esac echo "" } # ══════════════════════════════════════════════════════════════════════ # OUTPUT FORMATTERS # ══════════════════════════════════════════════════════════════════════ # ── Tree output ─────────────────────────────────────────────────────── output_tree() { local unit="$1" local direction="$2" local deps deps="$(collect_deps "$unit" "$direction" "$DEPTH")" if [[ "$direction" == "forward" ]]; then echo -e "${BOLD}Forward Dependencies${RESET} (what ${unit} requires):" else echo -e "${BOLD}Reverse Dependencies${RESET} (what depends on ${unit}):" fi echo " ──────────────────────────────────────────────────────────────" if [[ -z "$deps" ]]; then echo " (none)" echo "" return fi while IFS= read -r line; do local depth="${line%% *}" local dep_unit="${line#* }" local indent="" for (( i=0; i \"${dep_unit}\";" else echo " \"${dep_unit}\" -> \"${unit}\";" fi done <<< "$deps" fi echo "}" } # ── JSON output ─────────────────────────────────────────────────────── output_json() { local unit="$1" local direction="$2" local deps active_state load_state description deps="$(collect_deps "$unit" "$direction" "$DEPTH")" active_state="$(get_active_state "$unit")" load_state="$(get_load_state "$unit")" description="$(get_description "$unit")" echo "{" echo " \"unit\": \"${unit}\"," echo " \"active_state\": \"${active_state}\"," echo " \"load_state\": \"${load_state}\"," echo " \"description\": \"${description}\"," echo " \"direction\": \"${direction}\"," echo " \"depth_limit\": ${DEPTH}," echo " \"dependencies\": [" local first="true" if [[ -n "$deps" ]]; then while IFS= read -r line; do local depth="${line%% *}" local dep_unit="${line#* }" local dep_state dep_state="$(get_active_state "$dep_unit")" if [[ "$first" == "true" ]]; then first="false" else echo "," fi printf ' {"unit": "%s", "depth": %s, "active_state": "%s"}' \ "$dep_unit" "$depth" "$dep_state" done <<< "$deps" echo "" fi echo " ]" echo "}" } # ══════════════════════════════════════════════════════════════════════ # IMPACT SUMMARY # ══════════════════════════════════════════════════════════════════════ print_impact_summary() { local unit="$1" local reverse_deps reverse_deps="$(collect_deps "$unit" "reverse" "$DEPTH")" local count=0 if [[ -n "$reverse_deps" ]]; then count="$(echo "$reverse_deps" | wc -l)" fi echo -e "${BOLD}Impact Summary${RESET}" echo " ──────────────────────────────────────────────────────────────" if [[ $count -eq 0 ]]; then echo -e " Stopping ${BOLD}${unit}${RESET} would affect ${GREEN}0${RESET} other units." elif [[ $count -le 3 ]]; then echo -e " Stopping ${BOLD}${unit}${RESET} would affect ${YELLOW}${count}${RESET} other unit(s)." else echo -e " Stopping ${BOLD}${unit}${RESET} would affect ${RED}${count}${RESET} other unit(s)." fi echo "" } # ══════════════════════════════════════════════════════════════════════ # CRITICAL MODE # ══════════════════════════════════════════════════════════════════════ run_critical() { echo "" echo -e "${BOLD}Critical Services — ranked by reverse dependency count${RESET}" echo -e "${DIM}(services with the most units depending on them)${RESET}" echo " ══════════════════════════════════════════════════════════════" echo "" local units units="$(get_active_units)" if [[ -z "$units" ]]; then err "No active units found" exit 1 fi # Collect reverse dep counts local -a results=() while IFS= read -r unit; do local reverse_deps count=0 reverse_deps="$(collect_deps "$unit" "reverse" "$DEPTH" 2>/dev/null)" if [[ -n "$reverse_deps" ]]; then count="$(echo "$reverse_deps" | wc -l)" fi if [[ $count -gt 0 ]]; then results+=("${count} ${unit}") fi done <<< "$units" # Sort by count descending, show top 20 printf '%s\n' "${results[@]}" | sort -rn | head -20 | while IFS= read -r line; do local count="${line%% *}" local unit="${line#* }" local colored colored="$(color_unit "$unit")" if [[ $count -ge 10 ]]; then printf " ${RED}%4d${RESET} %b\n" "$count" "$colored" elif [[ $count -ge 5 ]]; then printf " ${YELLOW}%4d${RESET} %b\n" "$count" "$colored" else printf " ${GREEN}%4d${RESET} %b\n" "$count" "$colored" fi done echo "" echo -e "${DIM} Top 20 shown. Higher count = more impact if stopped.${RESET}" echo "" } # ══════════════════════════════════════════════════════════════════════ # ALL MODE # ══════════════════════════════════════════════════════════════════════ run_all() { local units units="$(get_active_units)" if [[ -z "$units" ]]; then err "No active units found" exit 1 fi echo "" echo -e "${BOLD}All Active Service Dependencies${RESET} (${MODE} / depth ${DEPTH})" echo " ══════════════════════════════════════════════════════════════" echo "" while IFS= read -r unit; do case "$FORMAT" in tree) echo -e "${CYAN}■ ${unit}${RESET}" output_tree "$unit" "$MODE" ;; flat) echo -e "# ${unit}" output_flat "$unit" "$MODE" echo "" ;; dot) output_dot "$unit" "$MODE" ;; json) output_json "$unit" "$MODE" ;; esac done <<< "$units" } # ══════════════════════════════════════════════════════════════════════ # SINGLE SERVICE MODE # ══════════════════════════════════════════════════════════════════════ run_single() { local unit unit="$(normalize_unit "$SERVICE")" # Verify the unit exists local load_state load_state="$(get_load_state "$unit")" if [[ "$load_state" == "not-found" ]]; then err "Unit '${unit}' not found" exit 1 fi case "$FORMAT" in tree) echo "" echo -e "${BOLD}Service Dependency Map — ${unit}${RESET}" echo -e "${DIM}$(date -u '+%Y-%m-%d %H:%M:%S UTC')${RESET}" print_service_info "$unit" if [[ "$MODE" == "forward" || "$MODE" == "both" ]]; then output_tree "$unit" "forward" fi if [[ "$MODE" == "reverse" || "$MODE" == "both" ]]; then output_tree "$unit" "reverse" fi # Always show both directions for single-service tree mode if [[ "$MODE" == "forward" ]]; then output_tree "$unit" "reverse" elif [[ "$MODE" == "reverse" ]]; then output_tree "$unit" "forward" fi print_impact_summary "$unit" ;; flat) output_flat "$unit" "$MODE" ;; dot) output_dot "$unit" "$MODE" ;; json) output_json "$unit" "$MODE" ;; esac } # ══════════════════════════════════════════════════════════════════════ # MAIN # ══════════════════════════════════════════════════════════════════════ main() { parse_args "$@" setup_colors if [[ "$CRITICAL" == "true" ]]; then run_critical elif [[ "$MAP_ALL" == "true" ]]; then run_all else run_single fi } main "$@"