Skip to content

Instantly share code, notes, and snippets.

@klutchell
Last active February 10, 2026 17:23
Show Gist options
  • Select an option

  • Save klutchell/c766da0a59e11218a782d0f873f5d1bd to your computer and use it in GitHub Desktop.

Select an option

Save klutchell/c766da0a59e11218a782d0f873f5d1bd to your computer and use it in GitHub Desktop.
Generate NetworkManager .nmconnection keyfile for Hetzner dedicated servers (queries Robot API, outputs to stdout)
#!/usr/bin/env bash
# Generate a NetworkManager .nmconnection keyfile for a Hetzner dedicated server.
# Queries the Hetzner Robot API for network details and writes keyfile to stdout.
#
# Usage: ROBOT_USER=xxx ROBOT_PASS=xxx ./hetzner-nmconnection.sh <server-number>
#
# Dependencies: curl, jq
# Output: .nmconnection keyfile on stdout; progress/errors on stderr
set -euo pipefail
die() { echo "error: $*" >&2; exit 1; }
log() { echo ":: $*" >&2; }
# -- deps --
for cmd in curl jq; do
command -v "$cmd" >/dev/null 2>&1 || die "$cmd not found (apt-get install -y $cmd)"
done
# -- inputs --
[[ ${1:-} ]] || die "usage: $0 <server-number>"
SERVER_NUMBER="$1"
: "${ROBOT_USER:?set ROBOT_USER}"
: "${ROBOT_PASS:?set ROBOT_PASS}"
ROBOT_API="https://robot-ws.your-server.de"
robot_get() {
local url="${ROBOT_API}$1"
log "GET $url"
curl -sf -u "${ROBOT_USER}:${ROBOT_PASS}" "$url"
}
# -- fetch server info --
server_json=$(robot_get "/server/${SERVER_NUMBER}")
server_ip=$(jq -r '.server.server_ip' <<<"$server_json")
server_mac=$(jq -r '.server.server_mac // empty' <<<"$server_json")
[[ $server_ip != "null" ]] || die "no server_ip in response"
# fall back to MAC of the default route interface on this host
if [[ -z $server_mac ]]; then
default_iface=$(ip route show default 2>/dev/null | awk '{print $5; exit}')
if [[ -n ${default_iface:-} ]]; then
server_mac=$(ip link show "$default_iface" 2>/dev/null | awk '/ether/ {print $2; exit}')
log "mac from local interface ${default_iface}=${server_mac}"
fi
fi
log "server_ip=$server_ip mac=${server_mac:-none}"
# extract first ipv6 subnet (may not exist)
ipv6_subnet=$(jq -r '.server.subnet[]? | select(.ip | contains(":")) | .ip' <<<"$server_json" | head -1)
ipv6_mask=$(jq -r '.server.subnet[]? | select(.ip | contains(":")) | .mask' <<<"$server_json" | head -1)
if [[ -n $ipv6_subnet ]]; then
# API returns network address like 2a01:4f8:xxx::/64 — strip trailing ::
ipv6_host="${ipv6_subnet%%::*}::1"
log "ipv6=${ipv6_host}/${ipv6_mask}"
else
log "no ipv6 subnet found"
fi
# -- fetch ipv4 details --
ip_json=$(robot_get "/ip/${server_ip}")
ipv4_addr=$(jq -r '.ip.ip' <<<"$ip_json")
ipv4_gw=$(jq -r '.ip.gateway' <<<"$ip_json")
ipv4_mask=$(jq -r '.ip.mask' <<<"$ip_json")
log "ipv4=${ipv4_addr}/${ipv4_mask} gw=${ipv4_gw}"
# -- generate keyfile --
log "generating keyfile"
cat <<EOF
[connection]
id=static
type=ethernet
autoconnect-priority=1
[ipv4]
method=manual
address1=${ipv4_addr}/${ipv4_mask},${ipv4_gw}
dns=1.1.1.1;1.0.0.1;
EOF
if [[ -n ${ipv6_subnet:-} ]]; then
cat <<EOF
[ipv6]
method=manual
address1=${ipv6_host}/${ipv6_mask},fe80::1
dns=2606:4700:4700::1111;2606:4700:4700::1001;
EOF
else
cat <<EOF
[ipv6]
method=disabled
EOF
fi
if [[ -n ${server_mac:-} ]]; then
cat <<EOF
[ethernet]
mac-address=${server_mac}
EOF
else
cat <<EOF
[ethernet]
EOF
fi
log "done"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment