Blob Blame History Raw
#!/bin/bash
#
# This is a shell script which prints the ntpd or chronyd synchronisation
# status, using the ntpq or chronyc program. It emulates the original
# ntpstat program written in C by G. Richard Keech, which implemented a
# subset of the mode6 protocol supported by ntpd.
#
# Copyright (C) 2016  Miroslav Lichvar <mlichvar@redhat.com>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

CHRONYC=("chronyc" "-n")
NTPQ=("ntpq" "-c" "timeout 100" "-c" "raw")

export LC_ALL=C

parse_tracking_field() {
    local tracking=$1 name=$2 field
    field=$(echo "$tracking" | grep "^$name")
    echo "${field#* : }"
}

get_chronyd_state() {
    local output line disp delay
    local leap source address stratum distance poll

    output=$("${CHRONYC[@]}" tracking 2> /dev/null) || return 2

    leap=$(parse_tracking_field "$output" "Leap status")
    case "$leap" in
        "Normal") leap="0";;
        "Insert second") leap="1";;
        "Delete second") leap="2";;
        "Not synchronised") leap="3";;
    esac

    address=$(parse_tracking_field "$output" "Reference ID")
    address=${address%)*}
    address=${address#*(}

    stratum=$(parse_tracking_field "$output" "Stratum")
    delay=$(parse_tracking_field "$output" "Root delay")
    delay=${delay% seconds}
    disp=$(parse_tracking_field "$output" "Root dispersion")
    disp=${disp% seconds}
    offset=$(parse_tracking_field "$output" "System time")
    offset=${offset% seconds*}

    distance=$(echo "$delay $disp $offset" | \
               awk '{ printf "%.3f", ($1 / 2.0 + $2 + $3) * 1e3 }')

    if [ -n "$address" ]; then
        line=$("${CHRONYC[@]}" sources 2> /dev/null | \
            grep " $address ") || return 3
        poll=$(echo "$line" | awk '{ print $4 }')

        case "${line:0:1}" in
            "*"|"=") source="NTP server";;
            "#")     source="reference clock";;
            *)       source="unknown source";;
        esac
    fi

    echo "$leap,NTP server,$address,$stratum,$distance,$poll"
}

parse_rv_field() {
    local rv=$1 name=$2 field
    field=$(echo "$rv" | grep -o "$name=[^,]*")
    echo "${field#*=}"
}

get_ntpd_state() {
    local output syspeer_id disp delay
    local leap source address stratum distance poll

    output=$("${NTPQ[@]}" -c "rv 0" 2> /dev/null) || return 2
    [[ $output == *"associd"*"status"* ]] || return 3

    leap=$(parse_rv_field "$output" "leap")
    source=$(parse_rv_field "$output" "status" | \
        awk '{ print and(rshift(strtonum($1), 8), 0x3f) }')
    case "$source" in
        0) source="unspecified";;
        1) source="atomic clock";;
        2) source="VLF radio";;
        3) source="HF radio";;
        4) source="UHF radio";;
        5) source="local net";;
        6) source="NTP server";;
        7) source="UDP/TIME";;
        8) source="wristwatch";;
        9) source="modem";;
        *) source="unknown source";;
    esac

    stratum=$(parse_rv_field "$output" "stratum")
    delay=$(parse_rv_field "$output" "rootdelay")
    disp=$(parse_rv_field "$output" "rootdisp")
    distance=$(echo "$delay $disp" | awk '{ printf "%.3f", $1 / 2.0 + $2 }')

    syspeer_id=$("${NTPQ[@]}" -c associations 2> /dev/null |\
        grep 'sys\.peer' | awk '{ print $2 }') || return 4
    output=$("${NTPQ[@]}" -c "rv $syspeer_id" 2> /dev/null) || return 5

    if [ "$source" = "NTP server" ]; then
        address=$(parse_rv_field "$output" "srcadr")
    fi
    poll=$(parse_rv_field "$output" "hpoll")

    echo "$leap,$source,$address,$stratum,$distance,$poll"
}


max_distance=""
while getopts "m:h" opt; do
    case $opt in
        m)
            max_distance=$OPTARG
            ;;
        *)
            echo "Usage: $0 [-m MAXERROR]"
            exit 3
            ;;
    esac
done

if ! state=$(get_chronyd_state) && ! state=$(get_ntpd_state); then
    echo "Unable to talk to NTP daemon. Is it running?" >&2
    exit 2
fi

IFS=, read -r leap source address stratum distance poll <<< "$state"

if [ "$leap" -ge 0 -a "$leap" -le 2 ]; then
    printf "synchronised to %s" "$source"
    if [ -n "$address" ]; then
        printf " (%s)" "$address"
    fi
    if [ -n "$stratum" ]; then
        printf " at stratum %d\n" "$stratum"
    else
        printf ", stratum unknown\n"
    fi

    if [ -n "$distance" ]; then
        printf "   time correct to within %.0f ms" "$distance"
        if [ -n "$max_distance" ] &&
                echo "$distance $max_distance" | awk '{ exit $1 <= $2 }'; then
            printf " (exceeded maximum of %s ms)\n" "$max_distance"
            status=1
        else
            printf "\n"
            status=0
        fi
    else
        printf "accuracy unknown\n"
        [ -n "$max_distance" ] && status=1 || status=0
    fi
else
    printf "unsynchronised\n"
    status=1
fi

if [ -n "$poll" ]; then
    printf "   polling server every %d s\n" "$[2**$poll]"
else
    printf "poll interval unknown\n"
fi

exit $status