|
#!/usr/bin/env bash |
|
# |
|
# configure_ntp.sh |
|
# |
|
# Description: |
|
# Checks and configures NTP settings on a Debian-based system. Installs |
|
# required packages (ntp), updates the system’s active NTP configuration file |
|
# (which may be /etc/ntp.conf or /etc/ntpsec/ntp.conf) based on user-supplied |
|
# --server or --pool arguments, restarts the relevant service if needed, and |
|
# verifies synchronisation. If no servers or pools are specified, the script |
|
# only ensures NTP is installed and active, with no config changes performed. |
|
# |
|
# Usage: |
|
# curl -sS https://example.invalid/configure_ntp.sh | sudo bash |
|
# (Ensure you run as root or with sudo.) |
|
# |
|
# Examples: |
|
# sudo bash configure_ntp.sh --server time.example.org --pool 0.pool.ntp.org |
|
# sudo bash configure_ntp.sh # (No config changes; only ensures ntp installed) |
|
|
|
set -Eeuo pipefail |
|
|
|
############################################################################### |
|
# Globals |
|
############################################################################### |
|
readonly SCRIPT_NAME="${0##*/}" |
|
|
|
# Variables to track the actual config file and service name in use. |
|
# Some systems (ntpsec) use /etc/ntpsec/ntp.conf with the 'ntpsec' service. |
|
# Others use /etc/ntp.conf with the 'ntp' service. |
|
NTP_CONFIG_FILE="" |
|
NTP_SERVICE="ntp" |
|
|
|
# Arrays of user-supplied servers/pools |
|
declare -a NTP_SERVERS=() |
|
declare -a NTP_POOLS=() |
|
|
|
# Associative array: SERVER_IPS will map a server/pool name to a space-separated |
|
# list of resolved IP addresses (either IPv4 or IPv6). |
|
declare -A SERVER_IPS=() |
|
|
|
# Trap function to catch errors and unexpected exits. |
|
trap 'catchError $?' ERR |
|
|
|
############################################################################### |
|
# Functions |
|
############################################################################### |
|
|
|
# ----------------------------------------------------------------------------- |
|
# catchError: Print an error message if the script encounters an unexpected exit. |
|
# |
|
# Args: |
|
# $1 (int): The exit code of the command that triggered this function. |
|
# ----------------------------------------------------------------------------- |
|
catchError() { |
|
local exit_code="$1" |
|
echo "[ERROR] The script '${SCRIPT_NAME}' exited with code ${exit_code}." |
|
echo " Check the output above for details." |
|
exit "${exit_code}" |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# checkRoot: Verify that the script is run as root (or via sudo). |
|
# ----------------------------------------------------------------------------- |
|
checkRoot() { |
|
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then |
|
echo "[ERROR] This script must be run as root or with sudo privileges." |
|
exit 1 |
|
fi |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# checkDebian: Verify we're running on a Debian-based system. |
|
# ----------------------------------------------------------------------------- |
|
checkDebian() { |
|
if [[ ! -f "/etc/debian_version" ]]; then |
|
echo "[ERROR] This script is intended for Debian-based systems only." |
|
exit 1 |
|
fi |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# parseArgs: Parse command line arguments. |
|
# Multiple NTP servers can be provided with repeated --server options. |
|
# Multiple NTP pools can be provided with repeated --pool options. |
|
# ----------------------------------------------------------------------------- |
|
parseArgs() { |
|
while [[ "$#" -gt 0 ]]; do |
|
case "$1" in |
|
--server) |
|
if [[ -z "${2:-}" ]]; then |
|
echo "[ERROR] --server requires an argument (e.g. time.example.org)." |
|
exit 1 |
|
fi |
|
NTP_SERVERS+=("$2") |
|
shift 2 |
|
;; |
|
--pool) |
|
if [[ -z "${2:-}" ]]; then |
|
echo "[ERROR] --pool requires an argument (e.g. 0.pool.ntp.org)." |
|
exit 1 |
|
fi |
|
NTP_POOLS+=("$2") |
|
shift 2 |
|
;; |
|
-h|--help) |
|
showHelp |
|
exit 0 |
|
;; |
|
*) |
|
echo "[ERROR] Unknown option: $1" |
|
echo "Use --help for usage details." |
|
exit 1 |
|
;; |
|
esac |
|
done |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# showHelp: Display usage information for the script. |
|
# ----------------------------------------------------------------------------- |
|
showHelp() { |
|
cat <<EOF |
|
Usage: ${SCRIPT_NAME} [--server <server>] [--pool <pool>] ... |
|
[--server <server2>] [--pool <pool2>] ... |
|
or |
|
${SCRIPT_NAME} --help |
|
|
|
Options: |
|
--server <server> Specify an NTP server. Repeat the option to add multiple servers. |
|
--pool <pool> Specify a pool. Repeat the option to add multiple pools. |
|
--help Display this help message. |
|
|
|
If no --server or --pool parameters are provided, the script will install |
|
ntp if necessary, detect which config (if any) is in use, but make no changes, |
|
and warn that no config update was performed. |
|
|
|
Examples: |
|
sudo ./${SCRIPT_NAME} --server time.example.org --pool 0.pool.ntp.org |
|
sudo ./${SCRIPT_NAME} --pool 1.pool.ntp.org --pool 2.pool.ntp.org |
|
sudo ./${SCRIPT_NAME} --server time.internal.net |
|
sudo ./${SCRIPT_NAME} |
|
EOF |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# detectNtpConfFile: Determine which config file path and service name to use: |
|
# - /etc/ntpsec/ntp.conf + "ntpsec" service if found |
|
# - /etc/ntp.conf + "ntp" service if found |
|
# - If none found, default to /etc/ntp.conf with "ntp" service |
|
# ----------------------------------------------------------------------------- |
|
detectNtpConfFile() { |
|
if [[ -f "/etc/ntpsec/ntp.conf" ]]; then |
|
NTP_CONFIG_FILE="/etc/ntpsec/ntp.conf" |
|
NTP_SERVICE="ntpsec" |
|
echo "[INFO] Detected ntpsec config at /etc/ntpsec/ntp.conf. Using 'ntpsec' service." |
|
elif [[ -f "/etc/ntp.conf" ]]; then |
|
NTP_CONFIG_FILE="/etc/ntp.conf" |
|
NTP_SERVICE="ntp" |
|
echo "[INFO] Detected ntp config at /etc/ntp.conf. Using 'ntp' service." |
|
else |
|
NTP_CONFIG_FILE="/etc/ntp.conf" |
|
NTP_SERVICE="ntp" |
|
echo "[INFO] No existing NTP config file found. Defaulting to /etc/ntp.conf with 'ntp' service." |
|
fi |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# installNtpPackage: Installs the ntp package if not already installed. |
|
# (If the system uses ntpsec, user is expected to have it installed.) |
|
# ----------------------------------------------------------------------------- |
|
installNtpPackage() { |
|
echo "[INFO] Checking and installing required packages (ntp)..." |
|
apt-get update -y |
|
DEBIAN_FRONTEND=noninteractive apt-get install -y ntp |
|
echo "[INFO] Package installation completed (or already present)." |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# backupNtpConfig: Make a backup of the existing config file if it exists. |
|
# ----------------------------------------------------------------------------- |
|
backupNtpConfig() { |
|
if [[ -f "${NTP_CONFIG_FILE}" ]]; then |
|
local timestamp |
|
timestamp="$(date +%Y%m%d%H%M%S)" |
|
cp "${NTP_CONFIG_FILE}" "${NTP_CONFIG_FILE}.bak-${timestamp}" |
|
echo "[INFO] Backed up existing NTP config to ${NTP_CONFIG_FILE}.bak-${timestamp}" |
|
fi |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# resolveIpsForHost: Gathers all resolvable IP addresses for a given host |
|
# (server or pool), storing them in SERVER_IPS for IP-based sync determination. |
|
# |
|
# Args: |
|
# $1 (string): Hostname of the NTP server or pool to resolve |
|
# ----------------------------------------------------------------------------- |
|
resolveIpsForHost() { |
|
local hostparam="$1" |
|
local -a ipList=() |
|
|
|
# Attempt to retrieve all addresses via 'getent ahosts'. |
|
while read -r line || [[ -n "$line" ]]; do |
|
local ip |
|
ip="$(awk '{print $1}' <<< "${line}")" |
|
if [[ "${ip}" =~ ^[0-9a-fA-F:\.]+$ ]]; then |
|
ipList+=("${ip}") |
|
fi |
|
done < <(getent ahosts "${hostparam}" 2>/dev/null || true) |
|
|
|
# Remove duplicates |
|
if [[ "${#ipList[@]}" -gt 0 ]]; then |
|
mapfile -t ipList < <(printf '%s\n' "${ipList[@]}" | sort -u) |
|
fi |
|
|
|
SERVER_IPS["${hostparam}"]="${ipList[*]}" |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# gatherAllIpsForHosts: Resolve IPs for each user-provided server and pool so we |
|
# can handle potential aliases or CNAME records in ntpq output. |
|
# ----------------------------------------------------------------------------- |
|
gatherAllIpsForHosts() { |
|
for srv in "${NTP_SERVERS[@]}"; do |
|
resolveIpsForHost "${srv}" |
|
if [[ -n "${SERVER_IPS["${srv}"]:-}" ]]; then |
|
echo "[INFO] '${srv}' resolved to IP(s): ${SERVER_IPS["${srv}"]}" |
|
else |
|
echo "[WARN] Unable to resolve any IP addresses for '${srv}'." |
|
fi |
|
done |
|
|
|
for pl in "${NTP_POOLS[@]}"; do |
|
resolveIpsForHost "${pl}" |
|
if [[ -n "${SERVER_IPS["${pl}"]:-}" ]]; then |
|
echo "[INFO] '${pl}' resolved to IP(s): ${SERVER_IPS["${pl}"]}" |
|
else |
|
echo "[WARN] Unable to resolve any IP addresses for '${pl}'." |
|
fi |
|
done |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# updateExistingNtpConfig: Takes an existing config file and updates it so: |
|
# 1. "server" lines are preserved only if they match user-supplied servers (if any). |
|
# If no servers are provided, all "server" lines are commented out. |
|
# 2. "pool" lines are preserved only if they match user-supplied pools (if any). |
|
# If no pools are provided, all "pool" lines are commented out. |
|
# 3. User-supplied servers/pools missing from the file are added (with iburst). |
|
# ----------------------------------------------------------------------------- |
|
updateExistingNtpConfig() { |
|
echo "[INFO] Updating existing NTP configuration in ${NTP_CONFIG_FILE}..." |
|
|
|
local -a newLines=() |
|
local -a foundServers=() |
|
local -a foundPools=() |
|
local line lineType lineHost |
|
|
|
while IFS='' read -r line || [[ -n "$line" ]]; do |
|
# We look for lines beginning with either "server" or "pool" |
|
# capturing the type in group1 and the host in group2: |
|
# ^[[:space:]]*(server|pool)[[:space:]]+([^[:space:]]+) |
|
if [[ "${line}" =~ ^[[:space:]]*(server|pool)[[:space:]]+([^[:space:]]+)(.*)$ ]]; then |
|
lineType="${BASH_REMATCH[1]}" # "server" or "pool" |
|
lineHost="${BASH_REMATCH[2]}" # The hostname argument |
|
local keep=0 |
|
|
|
if [[ "${lineType}" == "server" ]]; then |
|
if [[ "${#NTP_SERVERS[@]}" -gt 0 ]]; then |
|
# If we have user-supplied servers, keep only if it matches |
|
for s in "${NTP_SERVERS[@]}"; do |
|
if [[ "${s}" == "${lineHost}" ]]; then |
|
keep=1 |
|
foundServers+=("${s}") |
|
break |
|
fi |
|
done |
|
else |
|
# We have no user-supplied servers => comment out all server lines |
|
keep=0 |
|
fi |
|
elif [[ "${lineType}" == "pool" ]]; then |
|
if [[ "${#NTP_POOLS[@]}" -gt 0 ]]; then |
|
# If we have user-supplied pools, keep only if it matches |
|
for p in "${NTP_POOLS[@]}"; do |
|
if [[ "${p}" == "${lineHost}" ]]; then |
|
keep=1 |
|
foundPools+=("${p}") |
|
break |
|
fi |
|
done |
|
else |
|
# We have no user-supplied pools => comment out all pool lines |
|
keep=0 |
|
fi |
|
fi |
|
|
|
if [[ "${keep}" -eq 1 ]]; then |
|
# Keep unmodified |
|
newLines+=("${line}") |
|
else |
|
# Comment out if not already |
|
if [[ "${line}" =~ ^[[:space:]]*# ]]; then |
|
newLines+=("${line}") |
|
else |
|
newLines+=("# ${line}") |
|
fi |
|
fi |
|
else |
|
# Not a server/pool line, keep as-is |
|
newLines+=("${line}") |
|
fi |
|
done < "${NTP_CONFIG_FILE}" |
|
|
|
# Add missing servers if not found |
|
for s in "${NTP_SERVERS[@]}"; do |
|
local wasFound=0 |
|
for f in "${foundServers[@]}"; do |
|
if [[ "${s}" == "${f}" ]]; then |
|
wasFound=1 |
|
break |
|
fi |
|
done |
|
if [[ "${wasFound}" -eq 0 ]]; then |
|
newLines+=("server ${s} iburst") |
|
echo "[INFO] Added missing server '${s}' to configuration." |
|
fi |
|
done |
|
|
|
# Add missing pools if not found |
|
for p in "${NTP_POOLS[@]}"; do |
|
local wasFound=0 |
|
for f in "${foundPools[@]}"; do |
|
if [[ "${p}" == "${f}" ]]; then |
|
wasFound=1 |
|
break |
|
fi |
|
done |
|
if [[ "${wasFound}" -eq 0 ]]; then |
|
newLines+=("pool ${p} iburst") |
|
echo "[INFO] Added missing pool '${p}' to configuration." |
|
fi |
|
done |
|
|
|
# Overwrite the file with the updated lines |
|
printf "%s\n" "${newLines[@]}" > "${NTP_CONFIG_FILE}" |
|
echo "[INFO] Existing NTP configuration has been updated successfully." |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# configureNtp: Main logic to preserve existing items, but adjust "server"/"pool" |
|
# lines if user-supplied servers/pools were specified. If the file |
|
# doesn't exist at all, create minimal with user-supplied lines. |
|
# ----------------------------------------------------------------------------- |
|
configureNtp() { |
|
echo "[INFO] Configuring NTP..." |
|
|
|
local totalCount=$(( ${#NTP_SERVERS[@]} + ${#NTP_POOLS[@]} )) |
|
if [[ "${totalCount}" -eq 0 ]]; then |
|
# No user-supplied servers/pools => we do not modify config |
|
echo "[WARN] No --server or --pool arguments supplied. No config changes performed." |
|
return |
|
fi |
|
|
|
# We have at least one server/pool; back up existing config (if present), |
|
# then update or create. |
|
backupNtpConfig |
|
if [[ -f "${NTP_CONFIG_FILE}" ]]; then |
|
updateExistingNtpConfig |
|
else |
|
# File doesn't exist at all, create minimal with user-supplied lines |
|
echo "[INFO] ${NTP_CONFIG_FILE} not found. Creating a new file with base items and user-supplied lines." |
|
cat <<EOF > "${NTP_CONFIG_FILE}" |
|
driftfile /var/lib/ntp/ntp.drift |
|
|
|
# Restrict default network for security |
|
restrict -4 default kod notrap nomodify nopeer noquery |
|
restrict -6 default kod notrap nomodify nopeer noquery |
|
EOF |
|
|
|
for s in "${NTP_SERVERS[@]}"; do |
|
echo "server ${s} iburst" >> "${NTP_CONFIG_FILE}" |
|
echo "[INFO] Added server '${s}'." |
|
done |
|
|
|
for p in "${NTP_POOLS[@]}"; do |
|
echo "pool ${p} iburst" >> "${NTP_CONFIG_FILE}" |
|
echo "[INFO] Added pool '${p}'." |
|
done |
|
|
|
echo "[INFO] New ${NTP_CONFIG_FILE} created with user-supplied servers/pools." |
|
fi |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# restartNtp: Restart the selected ntp/ntpsec service to apply any changes. |
|
# ----------------------------------------------------------------------------- |
|
restartNtp() { |
|
echo "[INFO] Restarting ${NTP_SERVICE} service..." |
|
systemctl restart "${NTP_SERVICE}" |
|
echo "[INFO] ${NTP_SERVICE} service restarted." |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# checkNtpSync: Check if the system is synchronised with at least one user-provided |
|
# host. We wait several attempts to allow time for sync. If no user hosts exist, |
|
# we skip this entirely. |
|
# ----------------------------------------------------------------------------- |
|
checkNtpSync() { |
|
local totalCount=$(( ${#NTP_SERVERS[@]} + ${#NTP_POOLS[@]} )) |
|
if [[ "${totalCount}" -eq 0 ]]; then |
|
# No user-supplied configuration => skip sync check |
|
return |
|
fi |
|
|
|
local attempt |
|
local synced=0 |
|
|
|
echo "[INFO] Verifying that NTP is able to synchronise..." |
|
for attempt in {1..5}; do |
|
sleep 2 |
|
local syncLines |
|
# We use ntpq -p -n, parse lines that start with an asterisk/star or plus sign |
|
syncLines="$(ntpq -p -n 2>/dev/null | grep -E '^[\*\+]' || true)" |
|
|
|
if [[ -z "${syncLines}" ]]; then |
|
echo "[INFO] Attempt ${attempt}: No sync detected yet, re-checking shortly..." |
|
continue |
|
fi |
|
|
|
# We check IP-based matching for all user-supplied servers/pools |
|
while read -r line; do |
|
local lineIp |
|
lineIp="$(awk '{print $1}' <<< "${line}")" |
|
# Strip leading '*' or '+' |
|
lineIp="${lineIp#\*}" |
|
lineIp="${lineIp#\+}" |
|
|
|
# Compare with resolved IP lists |
|
for s in "${NTP_SERVERS[@]}"; do |
|
local resolvedIps="${SERVER_IPS["${s}"]:-}" |
|
if [[ -n "${resolvedIps}" ]] && grep -qw "${lineIp}" <<< "${resolvedIps}"; then |
|
synced=1 |
|
break 3 |
|
fi |
|
done |
|
for p in "${NTP_POOLS[@]}"; do |
|
local resolvedIps="${SERVER_IPS["${p}"]:-}" |
|
if [[ -n "${resolvedIps}" ]] && grep -qw "${lineIp}" <<< "${resolvedIps}"; then |
|
synced=1 |
|
break 3 |
|
fi |
|
done |
|
done <<< "${syncLines}" |
|
|
|
if [[ "${synced}" -eq 1 ]]; then |
|
break |
|
else |
|
echo "[INFO] Attempt ${attempt}: Some sync lines found, but no match to user-specified servers/pools. Re-checking..." |
|
fi |
|
done |
|
|
|
if [[ "${synced}" -eq 1 ]]; then |
|
echo "[INFO] NTP sync successful; at least one user-specified server/pool is providing sync." |
|
else |
|
echo "[ERROR] Unable to detect synchronisation from any user-specified server/pool after multiple attempts." |
|
echo " Investigate network connectivity or server availability if this is unexpected." |
|
exit 1 |
|
fi |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# main: Main script execution flow. |
|
# ----------------------------------------------------------------------------- |
|
main() { |
|
parseArgs "$@" |
|
checkRoot |
|
checkDebian |
|
installNtpPackage |
|
detectNtpConfFile |
|
|
|
local totalCount=$(( ${#NTP_SERVERS[@]} + ${#NTP_POOLS[@]} )) |
|
if [[ "${totalCount}" -gt 0 ]]; then |
|
gatherAllIpsForHosts |
|
fi |
|
|
|
configureNtp |
|
# Only restart the service if we configured it |
|
if [[ "${totalCount}" -gt 0 ]]; then |
|
restartNtp |
|
fi |
|
|
|
checkNtpSync |
|
echo "[INFO] Final check: NTP configuration complete (if applicable) and service is active." |
|
echo "[INFO] Script completed successfully." |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# Script entry point |
|
# ----------------------------------------------------------------------------- |
|
main "$@" |