Setting the file. One moment. Create Capture · AKS Network Capture · microsoft/azure-skills · Skills Docs — line 104
This file
- Number
- 4.5
- Position
- 5 of 12
- Type
- Shell
- Size
- 12 KB
- Lines
- 371
scripts/create-capture.sh
Shell·371 lines·12 KB
12
CAPTURE_NAME
=
""
13DURATION="60s"
14NODE_SELECTOR="kubernetes.io/os=linux"
15NODE_NAMES=""
16POD_SELECTOR=""
17POD_NAMES=""
18POD_SELECTOR_SET=0
19POD_NAMES_SET=0
20NAMESPACE="default"
21TCPDUMP_FILTER=""
22PACKET_SIZE="0"
23SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
25usage() {
26 cat <<EOF
27Usage: $0 --name <capture-name> [options]
28
29Required:
30 --name <string> Capture name (RFC 1123 label)
31
32Target selection:
33 --node-selector <key=val> Node selector (default: kubernetes.io/os=linux)
34 --node-names <n1,n2> Comma-separated node names
35 --pod-selector <key=val> Pod selector; resolves pods to nodes and IPs
36 --pod-names <p1,p2> Comma-separated pod names
37 --namespace <string> Pod, ConfigMap, and Job namespace (default: default)
38
39Capture configuration:
40 --duration <Ns|Nm|Nh> Duration (default: 60s; maximum: 1800s)
41 --packet-size <bytes> Snapshot length (default: 0, full packet)
42 --tcpdump-filter <expr> Plain BPF expression; no flags or metacharacters
43 -h, --help Show this help
44EOF
45}
46
47usage_error() {
48 echo "Error: $*" >&2
49 usage >&2
50 exit 2
51}
52
53die() {
54 echo "Error: $*" >&2
55 exit 1
56}
57
58require_value() {
59 [ "$#" -ge 2 ] && [ -n "$2" ] || usage_error "$1 requires a value"
60}
61
62valid_single_line() {
63 case "$1" in
64 *$'\r'*|*$'\n'*) return 1 ;;
65 esac
66}
67
68valid_rfc1123() {
69 valid_single_line "$1" && [ "${#1}" -le 63 ] \
70 && printf '%s' "$1" | grep -Eq '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'
71}
72
73valid_node_name() {
74 valid_single_line "$1" && [ "${#1}" -le 253 ] \
75 && printf '%s' "$1" \
76 | grep -Eq '^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$'
77}
78
79valid_label_selector() {
80 valid_single_line "$1" \
81 && printf '%s' "$1" | grep -Eq '^[A-Za-z0-9._/=,!()-]+$'
82}
83
84valid_duration() {
85 valid_single_line "$1" && printf '%s' "$1" | grep -Eq '^[0-9]+[smh]$'
86}
87
88valid_uint() {
89 valid_single_line "$1" && printf '%s' "$1" | grep -Eq '^[0-9]+$'
90}
91
92duration_to_seconds() {
93 local duration="$1"
94 local number="${duration%[smh]}"
95 local unit="${duration##*[0-9]}"
96 number=$((10#$number))
97 case "$unit" in
98 s) printf '%s\n' "$number" ;;
99 m) printf '%s\n' "$((number * 60))" ;;
100 h) printf '%s\n' "$((number * 3600))" ;;
101 esac
102}
103
104validate_filter() {
105 local filter="$1"
106 local token
107 [ -z "$filter" ] && return 0
108 [ "${#filter}" -le 1024 ] || usage_error "--tcpdump-filter exceeds 1024 characters"
109 valid_single_line "$filter" || usage_error "--tcpdump-filter must be a single line"
110 printf '%s' "$filter" | grep -Eq '^[A-Za-z0-9 ._:/()&|<>=!-]+$' \
111 || usage_error "--tcpdump-filter contains disallowed characters"
112 for token in $filter; do
113 case "$token" in
114 -*) usage_error "--tcpdump-filter may not contain flag tokens" ;;
115 esac
116 done
117}
118
119while [ "$#" -gt 0 ]; do
120 case "$1" in
121 --name)
122 require_value "$@"; CAPTURE_NAME="$2"; shift 2 ;;
123 --duration)
124 require_value "$@"; DURATION="$2"; shift 2 ;;
125 --node-selector)
126 require_value "$@"; NODE_SELECTOR="$2"; shift 2 ;;
127 --node-names)
128 require_value "$@"; NODE_NAMES="$2"; shift 2 ;;
129 --pod-selector)
130 require_value "$@"; POD_SELECTOR_SET=1; POD_SELECTOR="$2"; shift 2 ;;
131 --pod-names)
132 require_value "$@"; POD_NAMES_SET=1; POD_NAMES="$2"; shift 2 ;;
133 --namespace)
134 require_value "$@"; NAMESPACE="$2"; shift 2 ;;
135 --tcpdump-filter)
136 require_value "$@"; TCPDUMP_FILTER="$2"; shift 2 ;;
137 --packet-size)
138 require_value "$@"; PACKET_SIZE="$2"; shift 2 ;;
139 -h|--help)
140 usage; exit 0 ;;
141 *)
142 usage_error "unknown option: $1" ;;
143 esac
144done
145
146[ -n "$CAPTURE_NAME" ] || usage_error "--name is required"
147valid_rfc1123 "$CAPTURE_NAME" || usage_error "--name must be an RFC 1123 label"
148valid_duration "$DURATION" || usage_error "--duration must look like 30s, 5m, or 1h"
149DURATION_SECONDS="$(duration_to_seconds "$DURATION")"
150[ "$DURATION_SECONDS" -ge 1 ] && [ "$DURATION_SECONDS" -le "$MAX_DURATION_SECONDS" ] \
151 || usage_error "--duration must be between 1s and ${MAX_DURATION_SECONDS}s"
152valid_uint "$PACKET_SIZE" || usage_error "--packet-size must be a non-negative integer"
153[ "$PACKET_SIZE" -le 262144 ] || usage_error "--packet-size exceeds 262144 bytes"
154valid_label_selector "$NODE_SELECTOR" || usage_error "--node-selector has invalid characters"
155[ "$POD_SELECTOR_SET" -eq 0 ] || valid_label_selector "$POD_SELECTOR" \
156 || usage_error "--pod-selector has invalid characters"
157[ "$POD_NAMES_SET" -eq 0 ] || [ -n "$POD_NAMES" ] \
158 || usage_error "--pod-names must not be empty"
159valid_rfc1123 "$NAMESPACE" || usage_error "--namespace must be an RFC 1123 label"
160validate_filter "$TCPDUMP_FILTER"
161
162selection_count=0
163[ -n "$NODE_NAMES" ] && selection_count=$((selection_count + 1))
164[ "$POD_SELECTOR_SET" -eq 1 ] && selection_count=$((selection_count + 1))
165[ "$POD_NAMES_SET" -eq 1 ] && selection_count=$((selection_count + 1))
166[ "$selection_count" -le 1 ] \
167 || usage_error "choose only one of --node-names, --pod-selector, or --pod-names"
168
169command -v kubectl >/dev/null 2>&1 || die "kubectl is not installed or not on PATH"
170command -v od >/dev/null 2>&1 || die "od is not installed or not on PATH"
171
172if ! DEPLOYED_RUNNER="$(kubectl get configmap "$CONFIGMAP_NAME" -n "$NAMESPACE" \
173 -o go-template='{{ index .data "run-capture.sh" }}')"; then
174 die "ConfigMap '$CONFIGMAP_NAME' was not found; run setup-capture-configmap first"
175fi
176if ! LOCAL_RUNNER="$(cat "${SCRIPT_DIR}/run-capture.sh")"; then
177 die "could not read the local capture runner"
178fi
179[ -n "$DEPLOYED_RUNNER" ] && [ "$DEPLOYED_RUNNER" = "$LOCAL_RUNNER" ] \
180 || die "ConfigMap '$CONFIGMAP_NAME' is stale; run setup-capture-configmap again"
181
182POD_IP_FILTER=""
183TARGET_NODES=()
184
185resolve_pod_targets() {
186 local jsonpath
187 local output
188 local pod
189 local node
190 local ip_list
191 local ip
192 local ip_count
193 local ips=""
194 local pods=()
195 jsonpath='{range .items[*]}{.metadata.name}{"|"}{.spec.nodeName}{"|"}{range .status.podIPs[*]}{.ip}{","}{end}{"\n"}{end}'
196
197 if [ "$POD_NAMES_SET" -eq 1 ]; then
198 IFS=',' read -r -a pods <<<"$POD_NAMES"
199 for pod in "${pods[@]}"; do
200 valid_rfc1123 "$pod" || usage_error "pod name '$pod' is not an RFC 1123 label"
201 done
202 if ! output="$(kubectl get pods -n "$NAMESPACE" "${pods[@]}" -o jsonpath="$jsonpath")"; then
203 die "could not resolve the selected pods"
204 fi
205 else
206 if ! output="$(kubectl get pods -n "$NAMESPACE" -l "$POD_SELECTOR" \
207 -o jsonpath="$jsonpath")"; then
208 die "could not resolve the pod selector"
209 fi
210 fi
211
212 while IFS='|' read -r pod node ip_list; do
213 [ -n "$pod" ] || continue
214 [ -n "$node" ] || die "selected pod '$pod' is not scheduled"
215 [ -n "$ip_list" ] || die "selected pod '$pod' has no assigned IP"
216 TARGET_NODES+=("$node")
217 IFS=',' read -r -a pod_ips <<<"$ip_list"
218 ip_count=0
219 for ip in "${pod_ips[@]}"; do
220 [ -n "$ip" ] || continue
221 case "$ip" in
222 *[!A-Fa-f0-9.:]*) die "pod '$pod' returned an invalid IP address" ;;
223 esac
224 ips="${ips:+$ips or }host $ip"
225 ip_count=$((ip_count + 1))
226 done
227 [ "$ip_count" -gt 0 ] || die "selected pod '$pod' has no assigned IP"
228 done <<<"$output"
229
230 [ "${#TARGET_NODES[@]}" -gt 0 ] || die "no pods matched the selection"
231 POD_IP_FILTER="($ips)"
232}
233
234if [ "$POD_SELECTOR_SET" -eq 1 ] || [ "$POD_NAMES_SET" -eq 1 ]; then
235 resolve_pod_targets
236elif [ -n "$NODE_NAMES" ]; then
237 IFS=',' read -r -a TARGET_NODES <<<"$NODE_NAMES"
238else
239 if ! node_output="$(kubectl get nodes -l "$NODE_SELECTOR" \
240 -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}')"; then
241 die "could not resolve the node selector"
242 fi
243 while IFS= read -r node; do
244 [ -n "$node" ] && TARGET_NODES+=("$node")
245 done <<<"$node_output"
246fi
247
248[ "${#TARGET_NODES[@]}" -gt 0 ] || die "no nodes matched the selection"
249UNIQUE_TARGET_NODES=()
250for node in "${TARGET_NODES[@]}"; do
251 [ -n "$node" ] || die "node selection returned an empty node name"
252 valid_node_name "$node" || usage_error "node name '$node' is invalid"
253 seen=0
254 if [ "${#UNIQUE_TARGET_NODES[@]}" -gt 0 ]; then
255 for existing in "${UNIQUE_TARGET_NODES[@]}"; do
256 if [ "$node" = "$existing" ]; then
257 seen=1
258 break
259 fi
260 done
261 fi
262 [ "$seen" -eq 1 ] || UNIQUE_TARGET_NODES+=("$node")
263done
264TARGET_NODES=("${UNIQUE_TARGET_NODES[@]}")
265
266EFFECTIVE_FILTER="$TCPDUMP_FILTER"
267if [ -n "$POD_IP_FILTER" ]; then
268 if [ -n "$EFFECTIVE_FILTER" ]; then
269 EFFECTIVE_FILTER="($EFFECTIVE_FILTER) and $POD_IP_FILTER"
270 else
271 EFFECTIVE_FILTER="$POD_IP_FILTER"
272 fi
273fi
274
275TIMESTAMP="$(date -u +%Y%m%d-%H%M%S)"
276if ! RUN_TOKEN="$(LC_ALL=C od -An -N12 -tx1 /dev/urandom | tr -d ' \n')"; then
277 die "failed to generate the capture run identity"
278fi
279[ "${#RUN_TOKEN}" -eq 24 ] || die "failed to generate the capture run identity"
280RUN_ID="${TIMESTAMP}-${RUN_TOKEN}"
281CAPTURE_OUTPUT_PATH="${OUTPUT_BASE}/${CAPTURE_NAME}/${RUN_ID}"
282CREATED_JOBS=()
283
284cleanup_partial_jobs() {
285 local status=$?
286 trap - EXIT
287 if [ "$status" -ne 0 ] && [ "${#CREATED_JOBS[@]}" -gt 0 ]; then
288 echo "Capture creation failed; removing partially created Jobs" >&2
289 if ! kubectl delete jobs -n "$NAMESPACE" "${CREATED_JOBS[@]}" \
290 --ignore-not-found >/dev/null; then
291 echo "Error: failed to remove one or more partially created Jobs" >&2
292 status=1
293 fi
294 fi
295 exit "$status"
296}
297trap cleanup_partial_jobs EXIT
298
299echo "Creating capture '$CAPTURE_NAME' on ${#TARGET_NODES[@]} node(s)"
300for node in "${TARGET_NODES[@]}"; do
301 node_slug="$(printf '%s' "$node" | tr '._' '--' | cut -c1-40 | sed 's/-*$//')"
302 capture_slug="$(printf '%s' "$CAPTURE_NAME" | cut -c1-15 | sed 's/-*$//')"
303 node_prefix="$(printf '%s' "$node_slug" | cut -c1-8 | sed 's/-*$//')"
304 job_prefix="${capture_slug}-${node_prefix}-${RUN_TOKEN}-"
305
306 if ! created_job="$(kubectl create -f - -o jsonpath='{.metadata.name}' <<EOF
307apiVersion: batch/v1
308kind: Job
309metadata:
310 generateName: ${job_prefix}
311 namespace: ${NAMESPACE}
312 labels: { app: aks-network-capture, capture-id: "${CAPTURE_NAME}", capture-node: "${node_slug}", capture-run: "${RUN_ID}" }
313spec:
314 ttlSecondsAfterFinished: 3600
315 backoffLimit: 0
316 template:
317 metadata:
318 labels: { app: aks-network-capture, capture-id: "${CAPTURE_NAME}", capture-run: "${RUN_ID}" }
319 spec:
320 hostNetwork: true
321 nodeName: "${node}"
322 restartPolicy: Never
323 terminationGracePeriodSeconds: ${MAX_DURATION_SECONDS}
324 containers:
325 - name: capture
326 image: "${CAPTURE_IMAGE}"
327 securityContext:
328 privileged: false
329 allowPrivilegeEscalation: false
330 readOnlyRootFilesystem: true
331 runAsUser: 0
332 capabilities:
333 drop: ["ALL"]
334 add: ["NET_ADMIN", "NET_RAW"]
335 env:
336 - { name: PCAP_FILTER, value: "${EFFECTIVE_FILTER}" }
337 - { name: CAPTURE_DURATION, value: "${DURATION_SECONDS}" }
338 - { name: PACKET_SIZE, value: "${PACKET_SIZE}" }
339 - { name: OUT_DIR, value: "${CAPTURE_MOUNT_PATH}" }
340 - { name: CAPTURE_ID, value: "${CAPTURE_NAME}" }
341 - { name: RUN_ID, value: "${RUN_ID}" }
342 - { name: NODE_NAME, valueFrom: { fieldRef: { fieldPath: spec.nodeName } } }
343 command: ["/bin/sh", "/capture-scripts/run-capture.sh"]
344 volumeMounts:
345 - { name: capture-output, mountPath: "${CAPTURE_MOUNT_PATH}" }
346 - { name: capture-scripts, mountPath: /capture-scripts, readOnly: true }
347 volumes:
348 - name: capture-output
349 hostPath: { path: "${CAPTURE_OUTPUT_PATH}", type: DirectoryOrCreate }
350 - name: capture-scripts
351 configMap:
352 name: ${CONFIGMAP_NAME}
353 defaultMode: 0555
354EOF
355)"; then
356 die "failed to create a capture Job for node '$node'"
357 fi
358 [ -n "$created_job" ] || die "Kubernetes did not return the created Job name"
359 CREATED_JOBS+=("$created_job")
360 echo "Job created for node: $node ($created_job)"
361done
362
363trap - EXIT
364cat <<EOF
365
366Capture started.
367Run ID: ${RUN_ID}
368Monitor: kubectl get jobs -n ${NAMESPACE} -l capture-id=${CAPTURE_NAME} -w
369Retrieve: ./scripts/retrieve-captures.sh --name ${CAPTURE_NAME} --run-id ${RUN_ID} --namespace ${NAMESPACE}
370Bundles: ${CAPTURE_OUTPUT_PATH}
371EOF