|
| 1 | +#!/bin/bash |
| 2 | +# Select servers from the deployment's inventory by tag. |
| 3 | +# |
| 4 | +# inventory.sh [--tag T]... [--not-tag T]... [--field F] [--file P] [--count] |
| 5 | +# |
| 6 | +# Lookup order, first hit wins: --file, $DEVNET_INVENTORY, ./devnet.inventory, |
| 7 | +# <scripts dir>/devnet.inventory. Copy devnet.inventory.example to |
| 8 | +# devnet.inventory (gitignored) and fill it in; that file documents the format. |
| 9 | +# |
| 10 | +# --tag T keep rows carrying T. Repeatable, and a single argument may hold |
| 11 | +# several whitespace-separated tags. All of them must match (AND). |
| 12 | +# --not-tag T drop rows carrying T. Same repeat/multi-value rules. |
| 13 | +# --field F name | ip | tags | nodes | subnets | all. Default prints name + |
| 14 | +# ip aligned; `all` prints every column aligned. |
| 15 | +# --count print how many rows matched, nothing else. |
| 16 | +# |
| 17 | +# `validator` is a DERIVED tag: "has a devnet-* tag and is not tagged aggregator". |
| 18 | +# It is computed, not read, so it can never disagree with the aggregator tag. |
| 19 | +# `--tag validator` is exactly `--tag <that devnet> --not-tag aggregator`. |
| 20 | +# |
| 21 | +# An unknown tag is an ERROR, not an empty result. A typo ('devnet5' for |
| 22 | +# 'devnet-5') that quietly returns no hosts turns `for h in $(inventory.sh ...)` |
| 23 | +# into a loop that does nothing and reports success, which is the same class of |
| 24 | +# failure devnet-env.sh guards against: acting on the wrong deployment, silently. |
| 25 | +# A tag that IS known but matches nothing is likewise exit 1 -- a real fleet has |
| 26 | +# no empty groups, so an empty match means the inventory is stale. |
| 27 | +set -u |
| 28 | + |
| 29 | +usage() { sed -n '2,26p' "$0" | sed 's/^# \{0,1\}//'; exit "${1:-0}"; } |
| 30 | + |
| 31 | +file=""; field="default"; count=0 |
| 32 | +tags=(); nottags=() |
| 33 | + |
| 34 | +while [ $# -gt 0 ]; do |
| 35 | + case $1 in |
| 36 | + --tag) [ $# -ge 2 ] || { echo "$1 needs a value" >&2; exit 2; } |
| 37 | + # Split on whitespace so --tag 'devnet-5 aggregator' is two tags. |
| 38 | + for t in $2; do tags+=("$t"); done; shift 2 ;; |
| 39 | + --not-tag) [ $# -ge 2 ] || { echo "$1 needs a value" >&2; exit 2; } |
| 40 | + for t in $2; do nottags+=("$t"); done; shift 2 ;; |
| 41 | + --field) [ $# -ge 2 ] || { echo "$1 needs a value" >&2; exit 2; } |
| 42 | + field=$2; shift 2 ;; |
| 43 | + --file) [ $# -ge 2 ] || { echo "$1 needs a value" >&2; exit 2; } |
| 44 | + file=$2; shift 2 ;; |
| 45 | + --count) count=1; shift ;; |
| 46 | + -h|--help) usage 0 ;; |
| 47 | + *) echo "unknown argument: $1" >&2; usage 2 ;; |
| 48 | + esac |
| 49 | +done |
| 50 | + |
| 51 | +case $field in |
| 52 | + name|ip|tags|nodes|subnets|all|default) ;; |
| 53 | + *) echo "--field $field: expected name|ip|tags|nodes|subnets|all" >&2; exit 2 ;; |
| 54 | +esac |
| 55 | + |
| 56 | +# An explicit path that doesn't exist is a typo, not a reason to fall back to some |
| 57 | +# other inventory and act on the wrong fleet -- same rule devnet-env.sh applies. |
| 58 | +if [ -n "$file" ]; then |
| 59 | + [ -f "$file" ] || { echo "--file $file does not exist" >&2; exit 2; } |
| 60 | +elif [ -n "${DEVNET_INVENTORY:-}" ]; then |
| 61 | + file=$DEVNET_INVENTORY |
| 62 | + [ -f "$file" ] || { echo "DEVNET_INVENTORY=$file does not exist" >&2; exit 2; } |
| 63 | +else |
| 64 | + dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 65 | + for candidate in "./devnet.inventory" "$dir/devnet.inventory"; do |
| 66 | + [ -f "$candidate" ] && { file=$candidate; break; } |
| 67 | + done |
| 68 | + [ -n "$file" ] || { |
| 69 | + echo "no inventory file found (tried \$DEVNET_INVENTORY, ./devnet.inventory, $dir/devnet.inventory)" >&2 |
| 70 | + echo "copy $dir/devnet.inventory.example to $dir/devnet.inventory and fill it in" >&2 |
| 71 | + exit 2 |
| 72 | + } |
| 73 | +fi |
| 74 | + |
| 75 | +awk -v want="${tags[*]-}" -v nowant="${nottags[*]-}" \ |
| 76 | + -v field="$field" -v docount="$count" -v src="$file" ' |
| 77 | + # Tags are matched against ",a,b," so "agg" never matches "aggregator". |
| 78 | + function hastag(t, x) { |
| 79 | + if (x == "validator") |
| 80 | + return (index(t, ",devnet-") > 0 && index(t, ",aggregator,") == 0) |
| 81 | + return index(t, "," x ",") > 0 |
| 82 | + } |
| 83 | + BEGIN { nw = split(want, W, " "); nn = split(nowant, NW, " ") } |
| 84 | +
|
| 85 | + { sub(/^[[:space:]]+/, "") } |
| 86 | + /^#/ || /^$/ { next } |
| 87 | +
|
| 88 | + { |
| 89 | + if (NF < 3) { |
| 90 | + # Named rather than skipped in silence: a row the parser drops is a host |
| 91 | + # that vanishes from every tag it belonged to. |
| 92 | + printf "%s:%d: ignoring '\''%s'\'', need at least name/ip/tags\n", src, FNR, $0 > "/dev/stderr" |
| 93 | + next |
| 94 | + } |
| 95 | + name = $1; ip = $2; tags = $3 |
| 96 | + nodes = (NF >= 4 ? $4 : "-") |
| 97 | + subnets = (NF >= 5 ? $5 : "-") |
| 98 | + t = "," tags "," |
| 99 | +
|
| 100 | + # Every literal tag in the file, so a typo can be told from a real absence. |
| 101 | + n = split(tags, TT, ",") |
| 102 | + for (i = 1; i <= n; i++) if (TT[i] != "") seen[TT[i]] = 1 |
| 103 | +
|
| 104 | + for (i = 1; i <= nw; i++) if (!hastag(t, W[i])) next |
| 105 | + for (i = 1; i <= nn; i++) if ( hastag(t, NW[i])) next |
| 106 | +
|
| 107 | + m++ |
| 108 | + N[m] = name; I[m] = ip; T[m] = tags; O[m] = nodes; S[m] = subnets |
| 109 | + if (length(name) > wN) wN = length(name) |
| 110 | + if (length(ip) > wI) wI = length(ip) |
| 111 | + if (length(tags) > wT) wT = length(tags) |
| 112 | + if (length(nodes) > wO) wO = length(nodes) |
| 113 | + } |
| 114 | +
|
| 115 | + END { |
| 116 | + bad = 0 |
| 117 | + for (i = 1; i <= nw; i++) |
| 118 | + if (W[i] != "validator" && !(W[i] in seen)) { |
| 119 | + printf "no such tag in %s: %s\n", src, W[i] > "/dev/stderr"; bad = 1 |
| 120 | + } |
| 121 | + for (i = 1; i <= nn; i++) |
| 122 | + if (NW[i] != "validator" && !(NW[i] in seen)) { |
| 123 | + printf "no such tag in %s: %s\n", src, NW[i] > "/dev/stderr"; bad = 1 |
| 124 | + } |
| 125 | + if (bad) { |
| 126 | + printf "known tags: " > "/dev/stderr" |
| 127 | + for (k in seen) printf "%s ", k > "/dev/stderr" |
| 128 | + printf "(+ derived: validator)\n" > "/dev/stderr" |
| 129 | + exit 2 |
| 130 | + } |
| 131 | +
|
| 132 | + if (docount) { print m + 0; exit (m ? 0 : 1) } |
| 133 | +
|
| 134 | + for (i = 1; i <= m; i++) { |
| 135 | + if (field == "name") print N[i] |
| 136 | + else if (field == "ip") print I[i] |
| 137 | + else if (field == "tags") print T[i] |
| 138 | + else if (field == "nodes") print O[i] |
| 139 | + else if (field == "subnets") print S[i] |
| 140 | + else if (field == "all") |
| 141 | + printf "%-*s %-*s %-*s %-*s %s\n", wN, N[i], wI, I[i], wT, T[i], wO, O[i], S[i] |
| 142 | + else |
| 143 | + printf "%-*s %s\n", wN, N[i], I[i] |
| 144 | + } |
| 145 | +
|
| 146 | + if (m == 0) { |
| 147 | + printf "no host in %s matches%s%s\n", src, \ |
| 148 | + (nw ? " tags: " want : ""), (nn ? " not: " nowant : "") > "/dev/stderr" |
| 149 | + exit 1 |
| 150 | + } |
| 151 | + } |
| 152 | +' "$file" |
0 commit comments