MTU 9000 Rollout Script Fails at Host 73
A platform engineer wrote a 70-line bash script to roll out MTU 9000 on the storage network across 200 hosts. It reads hosts from a CSV, loops for h in $(cat hosts.txt), runs ssh -o BatchMode=yes root@$h "ip link set ens5 mtu 9000 && systemctl restart nfs-client", and starts with set -euo pipefail. There is no per-host timeout and no logging beyond stdout. On the first run, host 73 has an SSH host key mismatch, the script exits, and hosts 1-72 are already changed while 74-200 are untouched; nobody knows which is which. The storage array rejects jumbo frames from any host whose switch port is still at MTU 1500, causing the NFS mounts on the changed hosts to hang. A second run is requested immediately. The inventory file contains one hostname with a trailing carriage return and two hosts that were decommissioned last month.
1. Identify the failure modes in the script. Why is `set -e` combined with a remote `&&` chain a poor fit for this kind of fleet automation?
Read the worked answer
Failure modes, in order of impact:
set -eat fleet scope. One host's failure stops the rollout, producing a half-changed fleet with no record of which hosts were changed. There is no trap and no summary.- No per-host timeout.
sshwithoutConnectTimeoutand server-alive settings can hang for minutes or hours; one unresponsive host stops everything. - Remote
&&chain hides partial success. Ifip link setsucceeds andsystemctl restart nfs-clientfails, the host is left with MTU 9000 and a broken mount, and the script only sees a nonzero exit. - Word splitting and CRLF.
for h in $(cat hosts.txt)splits on whitespace, performs globbing, and keeps\r. The carriage-return host fails DNS or SSH with a confusing error. Unquotedroot@$his also an injection risk if the file contains options. - No preflight. Jumbo frames require every hop to support 9000. Changing the host before verifying end-to-end guarantees the array will drop the larger frames and NFS will hang.
- Non-idempotent. A second run re-applies the MTU and restarts NFS on hosts that are already correct, causing avoidable outages, and still dies on the same host key mismatch.
- Host key policy.
BatchMode=yesdoes not disableStrictHostKeyChecking; the mismatch is expected behavior and should be a handled exception, not a global abort.
set -e plus a remote && chain is a poor fit because the unit of success is a host, not an individual command. You need per-host status capture, continue-on-error, and an explicit summary.
2. Rewrite the core loop as an idempotent, per-host-failure-tolerant script with timeouts, logging, and a jumbo-frame preflight.
Read the worked answer
Separate the per-host action from the orchestration and record status per host:
#!/usr/bin/env bash
set -uo pipefail
inventory=${1:?usage: rollout.sh inventory.txt}
log=mtu-rollout-$(date +%Y%m%dT%H%M%S).log
: >"$log"
check_and_set() {
local host=${1%$'\r'} out rc
[[ -z $host || $host == \#* ]] && return 0
out=$(timeout 45 ssh -o BatchMode=yes -o ConnectTimeout=8 \
-o ServerAliveInterval=5 -o ServerAliveCountMax=2 \
-o StrictHostKeyChecking=accept-new -- "$host" '
set -e
cur=$(cat /sys/class/net/ens5/mtu)
if [ "$cur" -eq 9000 ]; then echo already-9000; exit 0; fi
ping -M do -c 1 -W 2 -s 8972 198.51.100.20 >/dev/null || \
{ echo jumbo-preflight-failed; exit 20; }
ip link set ens5 mtu 9000
systemctl restart nfs-client
echo mtu-ok
' 2>&1); rc=$?
printf '%s\t%s\t%s\n' "$host" "$rc" "$(tr '\n' ' ' <<<"$out")" | tee -a "$log"
return 0
}
export -f check_and_set; export log
tr -d '\r' <"$inventory" | xargs -r -P 8 -n 1 bash -c 'check_and_set "$@"' _
Key points:
set -uo pipefailwithout-e: failures are recorded, not fatal to the run.timeoutbounds each host, and the SSH options bound the connection and dead-peer detection.- The remote script is idempotent and preflights jumbo reachability to the array before changing the MTU, so a switch port still at 1500 fails safely with a distinct exit code.
ip link setis runtime only; persist it with NetworkManager (nmcli con mod ... 802-3-ethernet.mtu 9000) or a systemd-networkd.networkfile, otherwise it reverts on reboot.- For a true fleet preflight, first run a read-only pass that collects
ip -o link show ens5,ip route get 198.51.100.20, and switch port MTU from inventory or LLDP data.
3. How would you make the second run safe and verify state, including rollback of the hosts already changed?
Read the worked answer
Make the second run state-driven, not hope-driven.
- Reconcile the inventory first: strip CR, drop comments and blanks, and validate each host with
getent hostsand a TCP check. Remove the two decommissioned hosts so they do not consume retries. - Build a state file from the first log. Classify each host as unchanged, changed and healthy, or changed and broken. Distinct remote exit codes (20 = jumbo preflight failed, 21 = NFS restart failed) make classification deterministic.
- For changed-but-broken hosts, roll back first:
ip link set ens5 mtu 1500andsystemctl restart nfs-client, then verify withmountpoint -q /mnt/storageandstat /mnt/storage. Rollback does not require jumbo to work, so it is safe even when NFS is hung. Retry the change only after the mount is healthy. - Run a dry-run pass with the same code but no
ip link set. It should reportalready-9000for successful hosts and the exact preflight failure for the rest. That is your idempotency check. - Apply in waves of 10-20 with a pause and a storage-array health check between waves. Watch switch counters for giants, CRC errors, and drops; a port left at 1500 shows up on the array side.
- Fix host 73's host key before retrying: verify the new key out of band, then
ssh-keygen -R <host>and re-add. After the bootstrap, move toStrictHostKeyChecking=yeswith a managed known_hosts. - Keep the before/after MTU and the per-host log as the audit trail. The script should exit nonzero only if the final summary contains failures, not on the first one.