Last active
August 27, 2025 17:07
-
-
Save supermarsx/d44516c6d386720c4b24ff4d54c74d83 to your computer and use it in GitHub Desktop.
Docker Bridges cleaner, cleanup all configured docker bridges on network manager
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # docker-nmcli-bridge-cleaner.sh | |
| # ------------------------------------------------------------ | |
| # Delete NetworkManager connections whose NAME matches a regex | |
| # (defaults to "^br-" for Docker-style bridges). Shows a pretty | |
| # preview table, detects Docker/Podman/Libvirt/LXD/Incus, warns | |
| # when a connection is active or likely managed by those stacks, | |
| # supports dry-run, backups, and safety skips. | |
| # | |
| # Usage: | |
| # ./nmcli-bridge-cleaner.sh # preview + prompt | |
| # ./nmcli-bridge-cleaner.sh -y # no prompt (force) | |
| # ./nmcli-bridge-cleaner.sh -n # dry-run (what-if) | |
| # ./nmcli-bridge-cleaner.sh -p '^br-|virbr' # custom regex | |
| # ./nmcli-bridge-cleaner.sh -b backups/ # export/backup first | |
| # ./nmcli-bridge-cleaner.sh --detect-only # just detect/preview | |
| # ./nmcli-bridge-cleaner.sh --skip-origins docker,libvirt \ | |
| # --ignore-active # advanced safety | |
| # | |
| # Notes: | |
| # - Requires: nmcli, awk. (column is optional for nicer tables.) | |
| # - We detect services and bridge origins by heuristics & commands: | |
| # * Docker: br-<12 chars of network ID>; verified via `docker network ls`. | |
| # * Podman: cni-podman0/podman[0-9]? heuristics. | |
| # * Libvirt: virbr* heuristic (virsh optional). | |
| # * LXD/Incus: lxdbr*/incusbr* (lxc/incus optional). | |
| # - Deleting an ACTIVE connection can disrupt networking. By default | |
| # we SKIP active ones unless --ignore-active is set. | |
| # ------------------------------------------------------------ | |
| set -euo pipefail | |
| IFS=$' | |
| ' | |
| shopt -s extglob | |
| # --------------------------- Styling --------------------------- | |
| BOLD="[1m"; DIM="[2m"; RESET="[0m" | |
| GREEN="[32m"; YELLOW="[33m"; RED="[31m"; CYAN="[36m"; GRAY="[90m" | |
| CHECK="✔"; CROSS="✖"; INFO="ℹ"; WARN="⚠"; DOT="•" | |
| # ----------------------------------------------------------------------------- | |
| # Function: banner | |
| # Purpose : Print a decorative header to stdout. | |
| # Globals : CYAN, BOLD, RESET | |
| # Returns : 0 | |
| # ----------------------------------------------------------------------------- | |
| banner() { | |
| printf "${CYAN}%s${RESET} | |
| " "┌───────────────────────────────────────────────────────────┐" | |
| printf "${CYAN}%s${RESET} | |
| " "│ ${BOLD}NMCLI BRIDGE CLEANER${RESET} — smart, safe, stylish │" | |
| printf "${CYAN}%s${RESET} | |
| " "└───────────────────────────────────────────────────────────┘" | |
| } | |
| # ----------------------------------------------------------------------------- | |
| # Logging helpers | |
| # Purpose : Uniform, colorized logs for info/warn/ok/error/dim traces. | |
| # Usage : log_info "message"; log_err "fatal"; etc. | |
| # Globals : color codes above | |
| # Returns : 0 | |
| # ----------------------------------------------------------------------------- | |
| log_info() { printf "${CYAN}${INFO}${RESET} %s | |
| " "$*"; } | |
| log_warn() { printf "${YELLOW}${WARN}${RESET} %s | |
| " "$*"; } | |
| log_ok() { printf "${GREEN}${CHECK}${RESET} %s | |
| " "$*"; } | |
| log_err() { printf "${RED}${CROSS}${RESET} %s | |
| " "$*" >&2; } | |
| log_dim() { printf "${GRAY}${DOT}${RESET} %s | |
| " "$*"; } | |
| # ----------------------------------------------------------------------------- | |
| # Function: die | |
| # Purpose : Print an error via log_err and exit non‑zero. | |
| # Args : $* Error message | |
| # Returns : Exits 1 | |
| # ----------------------------------------------------------------------------- | |
| die() { log_err "$*"; exit 1; } | |
| # --------------------------- Args ------------------------------ | |
| DEFAULT_REGEX='^br-' | |
| REGEX="$DEFAULT_REGEX" | |
| DRY_RUN=false | |
| FORCE=false | |
| BACKUP_DIR="" | |
| VERBOSE=false | |
| DETECT_ONLY=false | |
| IGNORE_ACTIVE=false | |
| SKIP_ORIGINS="" # csv: docker,libvirt,lxd,incus,podman,k8s,other | |
| # ----------------------------------------------------------------------------- | |
| # Function: usage | |
| # Purpose : Print CLI help/usage text. | |
| # Returns : 0 | |
| # ----------------------------------------------------------------------------- | |
| usage() { | |
| cat <<USAGE | |
| ${BOLD}nmcli-bridge-cleaner.sh${RESET} | |
| Delete NetworkManager connections whose NAME matches a regex (default: ${BOLD}$DEFAULT_REGEX${RESET}). | |
| ${BOLD}Options${RESET} | |
| -p, --pattern <regex> Regex against connection NAME (default: ^br-) | |
| -n, --dry-run Show what would be deleted; do not delete | |
| -y, --yes, --force Do not prompt for confirmation | |
| -b, --backup-dir <dir> Export each connection to <dir> before deletion | |
| -v, --verbose Chatty output | |
| --detect-only Do not delete; just detect/preview | |
| --ignore-active Allow deletion of ACTIVE connections (danger!) | |
| --skip-origins <csv> Skip deleting origins (e.g. docker,libvirt,lxd) | |
| -h, --help Show this help | |
| ${BOLD}Examples${RESET} | |
| ./nmcli-bridge-cleaner.sh | |
| ./nmcli-bridge-cleaner.sh -y -b ./backups | |
| ./nmcli-bridge-cleaner.sh -p '^br-|virbr' --skip-origins libvirt -n | |
| USAGE | |
| } | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -p|--pattern) REGEX="${2-}"; shift ;; | |
| -n|--dry-run|--whatif) DRY_RUN=true ;; | |
| -y|--yes|--force) FORCE=true ;; | |
| -b|--backup-dir) BACKUP_DIR="${2-}"; shift ;; | |
| -v|--verbose) VERBOSE=true ;; | |
| --detect-only) DETECT_ONLY=true ;; | |
| --ignore-active) IGNORE_ACTIVE=true ;; | |
| --skip-origins) SKIP_ORIGINS="${2-}"; shift ;; | |
| -h|--help) usage; exit 0 ;; | |
| *) die "Unknown argument: $1 (use -h for help)" ;; | |
| esac | |
| shift | |
| done | |
| # --------------------- Preflight checks ------------------------ | |
| command -v nmcli >/dev/null 2>&1 || die "nmcli not found. Install NetworkManager tools." | |
| command -v awk >/dev/null 2>&1 || die "awk not found." | |
| HAS_COLUMN=true | |
| command -v column >/dev/null 2>&1 || HAS_COLUMN=false | |
| if [[ -n "$BACKUP_DIR" ]]; then | |
| mkdir -p -- "$BACKUP_DIR" || die "Cannot create backup dir: $BACKUP_DIR" | |
| fi | |
| banner | |
| log_info "Pattern: ${BOLD}/$REGEX/${RESET}" | |
| $DRY_RUN && log_warn "Dry-run mode: no changes will be made." | |
| $DETECT_ONLY && log_warn "Detect-only mode: preview without deletion." | |
| [[ -n "$BACKUP_DIR" ]] && log_info "Backup directory: ${BOLD}$BACKUP_DIR${RESET}" | |
| [[ -n "$SKIP_ORIGINS" ]] && log_info "Skip origins: ${BOLD}$SKIP_ORIGINS${RESET}" | |
| $IGNORE_ACTIVE || log_info "Active connections will be ${BOLD}skipped${RESET} (use --ignore-active to override)." | |
| # ---------------------- Environment probe --------------------- | |
| # ----------------------------------------------------------------------------- | |
| # Function: svc_active | |
| # Purpose : Check if a systemd service (or process fallback) is active. | |
| # Args : $1 service name (e.g., docker) | |
| # Returns : 0 if active, 1 otherwise | |
| # ----------------------------------------------------------------------------- | |
| svc_active() { | |
| local s="$1" | |
| if command -v systemctl >/dev/null 2>&1; then | |
| systemctl is-active --quiet "$s" 2>/dev/null && return 0 || return 1 | |
| fi | |
| pgrep -x "$s" >/dev/null 2>&1 && return 0 || return 1 | |
| } | |
| # ----------------------------------------------------------------------------- | |
| # Function: have | |
| # Purpose : Test if a command exists in PATH. | |
| # Args : $1 binary name | |
| # Returns : 0 if found, 1 otherwise | |
| # ----------------------------------------------------------------------------- | |
| have() { command -v "$1" >/dev/null 2>&1; } | |
| # ----------------------------------------------------------------------------- | |
| # Function: print_env_status | |
| # Purpose : Detect and summarize container/virtualization stacks present. | |
| # Globals : uses have, svc_active; prints human-friendly status lines. | |
| # Returns : 0 | |
| # ----------------------------------------------------------------------------- | |
| print_env_status() { | |
| local label status | |
| declare -A MAP=( | |
| [docker]=Docker | |
| [podman]=Podman | |
| [containerd]=containerd | |
| [crio]=CRI-O | |
| [virsh]=Libvirt | |
| [lxc]=LXD | |
| [incus]=Incus | |
| [multipass]=Multipass | |
| [kubelet]=Kubernetes | |
| [k3s]=K3s | |
| ) | |
| log_dim "Detecting container/virt stacks…" | |
| for bin in "${!MAP[@]}"; do | |
| label="${MAP[$bin]}" | |
| if have "$bin"; then | |
| case "$bin" in | |
| docker) status=$(svc_active docker && echo active || echo present) ;; | |
| podman) status=$(svc_active podman && echo active || echo present) ;; | |
| containerd) status=$(svc_active containerd && echo active || echo present) ;; | |
| crio) status=$(svc_active crio && echo active || echo present) ;; | |
| virsh) status=$(svc_active libvirtd || svc_active virtqemud && echo active || echo present) ;; | |
| lxc) status=$(svc_active lxd && echo active || echo present) ;; | |
| incus) status=$(svc_active incus && echo active || echo present) ;; | |
| multipass) status=$(svc_active multipassd && echo active || echo present) ;; | |
| kubelet) status=$(svc_active kubelet && echo active || echo present) ;; | |
| k3s) status=$(svc_active k3s && echo active || echo present) ;; | |
| esac | |
| log_info "${label}: ${BOLD}$status${RESET}" | |
| fi | |
| done | |
| } | |
| print_env_status | |
| # Build Docker br-<id> → name map if possible | |
| declare -A DOCKER_BR2NAME | |
| if have docker; then | |
| if docker info >/dev/null 2>&1; then | |
| while read -r id name; do | |
| [[ -z "$id" || -z "$name" ]] && continue | |
| DOCKER_BR2NAME["br-${id:0:12}"]="$name" | |
| done < <(docker network ls --format '{{.ID}} {{.Name}}' 2>/dev/null || true) | |
| fi | |
| fi | |
| # Podman heuristics: interfaces usually cni-podman0 / podman0 | |
| PODMAN_BRIDGES=(cni-podman0 podman0) | |
| # Libvirt/LXD/Incus names are conventional | |
| # - virbr*, lxdbr*, incusbr* | |
| # ----------------------- Collection ---------------------------- | |
| log_dim "Querying NetworkManager connections…" | |
| # Fields: NAME,UUID,TYPE,DEVICE (tab-separated for easy column formatting) | |
| mapfile -t ROWS < <(nmcli -t -f NAME,UUID,TYPE,DEVICE connection show \ | |
| | awk -F: -v re="$REGEX" '$1 ~ re { printf "%s %s %s %s | |
| ", $1,$2,$3,$4 }') | |
| COUNT=${#ROWS[@]} | |
| if (( COUNT == 0 )); then | |
| log_ok "No connections match /$REGEX/. Nothing to do." | |
| exit 0 | |
| fi | |
| # ---------------------- Classification ------------------------ | |
| # ----------------------------------------------------------------------------- | |
| # Function: classify_origin | |
| # Purpose : Heuristically classify a connection NAME to an origin stack. | |
| # Args : $1 NAME (e.g., br-1a2b3c4d5e6f, virbr0, lxdbr0) | |
| # Returns : Echoes one of: docker:<net-name> | podman | libvirt | lxd | incus | k8s | other | |
| # Notes : Docker mapping uses a prebuilt br-<ID> → network name map. | |
| # ----------------------------------------------------------------------------- | |
| classify_origin() { | |
| local n="$1" | |
| if [[ "${DOCKER_BR2NAME[$n]-}" != "" ]]; then | |
| echo "docker:${DOCKER_BR2NAME[$n]}"; return | |
| fi | |
| for p in "${PODMAN_BRIDGES[@]}"; do | |
| [[ $n == "$p" ]] && { echo "podman"; return; } | |
| done | |
| [[ $n =~ ^virbr[0-9]*$ ]] && { echo "libvirt"; return; } | |
| [[ $n =~ ^lxdbr[0-9]*$ ]] && { echo "lxd"; return; } | |
| [[ $n =~ ^incusbr[0-9]*$ ]] && { echo "incus"; return; } | |
| [[ $n =~ ^cni0$|^flannel\.[0-9]+$|^weave$|^cali.*$ ]] && { echo "k8s"; return; } | |
| echo "other" | |
| } | |
| # ----------------------------------------------------------------------------- | |
| # Function: is_active_device | |
| # Purpose : Determine if a network DEVICE is operational (state UP). | |
| # Args : $1 DEVICE (e.g., br-xxxxx) | |
| # Returns : 0 if link is UP, 1 otherwise (also returns 1 for empty/"--") | |
| # ----------------------------------------------------------------------------- | |
| is_active_device() { | |
| local d="$1" | |
| [[ -z "$d" || "$d" == "--" ]] && return 1 | |
| ip -o link show "$d" 2>/dev/null | grep -q 'state UP' && return 0 || return 1 | |
| } | |
| # ----------------------------------------------------------------------------- | |
| # Function: should_skip_origin | |
| # Purpose : Check whether an origin type is in the user‑provided skip list. | |
| # Args : $1 origin token possibly with suffix after ':' (e.g., docker:bridge) | |
| # Returns : 0 if should skip, 1 if not | |
| # ----------------------------------------------------------------------------- | |
| should_skip_origin() { | |
| local tok=${1%%:*} | |
| [[ -z "$SKIP_ORIGINS" ]] && return 1 | |
| IFS=',' read -r -a arr <<<"$SKIP_ORIGINS" | |
| for x in "${arr[@]}"; do [[ "$tok" == "$x" ]] && return 0; done | |
| return 1 | |
| } | |
| # Augment rows with ORIGIN and STATE | |
| AUG=() | |
| for r in "${ROWS[@]}"; do | |
| NAME="${r%%$' '*}" | |
| REST="${r#*$' '}"; UUID="${REST%%$' '*}" | |
| REST2="${REST#*$' '}"; TYPE="${REST2%%$' '*}" | |
| DEVICE="${REST2#*$' '}" | |
| ORIGIN=$(classify_origin "$NAME") | |
| STATE="inactive" | |
| if is_active_device "$DEVICE"; then STATE="active"; fi | |
| AUG+=("$NAME $UUID $TYPE $DEVICE $ORIGIN $STATE") | |
| done | |
| # ------------------------ Preview table ------------------------ | |
| # ----------------------------------------------------------------------------- | |
| # Function: print_table | |
| # Purpose : Pretty‑print the augmented connection list as a table. | |
| # Globals : AUG, HAS_COLUMN | |
| # Returns : 0 | |
| # ----------------------------------------------------------------------------- | |
| print_table() { | |
| printf "NAME UUID TYPE DEVICE ORIGIN STATE | |
| " | |
| for r in "${AUG[@]}"; do | |
| printf "%s | |
| " "$r" | |
| done | { if $HAS_COLUMN; then column -t -s $' '; else cat; fi; } | |
| } | |
| log_info "Found ${BOLD}$COUNT${RESET} matching connection(s):" | |
| print_table | |
| $DETECT_ONLY && { log_info "Detect-only finished."; exit 0; } | |
| # ----------------------- Confirmation -------------------------- | |
| if ! $FORCE && ! $DRY_RUN; then | |
| printf " | |
| ${BOLD}Type 'delete' to remove the above %d connection(s) (respecting skips), or anything else to cancel:${RESET} " "$COUNT" | |
| read -r ANSWER || true | |
| if [[ "$ANSWER" != "delete" ]]; then | |
| log_warn "Cancelled by user." | |
| exit 0 | |
| fi | |
| fi | |
| # ------------------------- Deletion ---------------------------- | |
| DELETED=0; SKIPPED=0; FAILED=0 | |
| for r in "${AUG[@]}"; do | |
| NAME="${r%%$' '*}" | |
| REST="${r#*$' '}"; UUID="${REST%%$' '*}" | |
| REST2="${REST#*$' '}"; TYPE="${REST2%%$' '*}" | |
| REST3="${REST2#*$' '}"; DEVICE="${REST3%%$' '*}" | |
| REST4="${REST3#*$' '}"; ORIGIN="${REST4%%$' '*}" | |
| STATE="${REST4#*$' '}" | |
| if should_skip_origin "$ORIGIN"; then | |
| ((SKIPPED++)); log_warn "Skipping $NAME due to origin: ${BOLD}${ORIGIN}${RESET}"; continue | |
| fi | |
| if [[ "$STATE" == "active" && $IGNORE_ACTIVE == false ]]; then | |
| ((SKIPPED++)); log_warn "Skipping ACTIVE connection ${BOLD}$NAME${RESET} (${DEVICE}); use --ignore-active to force." | |
| continue | |
| fi | |
| if [[ -n "$BACKUP_DIR" ]]; then | |
| OUT="$BACKUP_DIR/${NAME//\//_}_${UUID}.keyfile" | |
| $VERBOSE && log_dim "Exporting $NAME → $OUT" | |
| if ! nmcli connection show "$UUID" >"$OUT" 2>/dev/null; then | |
| sudo nmcli connection show "$UUID" >"$OUT" || { log_warn "Backup failed for $NAME ($UUID)"; } | |
| fi | |
| fi | |
| if $DRY_RUN; then | |
| log_info "Would delete ${BOLD}$NAME${RESET} (${DIM}$UUID${RESET}) [${ORIGIN}/${STATE}]" | |
| ((SKIPPED++)) | |
| continue | |
| fi | |
| $VERBOSE && log_dim "Deleting $NAME ($UUID)…" | |
| if nmcli connection delete uuid "$UUID" >/dev/null 2>&1 \ | |
| || sudo nmcli connection delete uuid "$UUID" >/dev/null 2>&1; then | |
| ((DELETED++)) | |
| log_ok "Deleted ${BOLD}$NAME${RESET} (${DIM}$UUID${RESET}) [${ORIGIN}]" | |
| else | |
| ((FAILED++)) | |
| log_err "Failed to delete $NAME ($UUID)" | |
| fi | |
| done | |
| # -------------------------- Summary ---------------------------- | |
| printf " | |
| " | |
| log_info "${BOLD}Summary${RESET}: deleted=$DELETED, skipped=$SKIPPED, failed=$FAILED" | |
| if (( FAILED > 0 )); then | |
| log_warn "Some deletions failed. Try running again with sudo or check polkit rules." | |
| fi | |
| exit 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment