diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 8b99332..b43bc92 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -25,13 +25,16 @@ jobs:
fetch-depth: 0
- name: "Install Nix"
+ if: "${{ steps.release.outputs.release_created }}"
uses: "DeterminateSystems/nix-installer-action@v16"
- name: "Use Nix Cache"
+ if: "${{ steps.release.outputs.release_created }}"
uses: "DeterminateSystems/magic-nix-cache-action@v8"
## TODO: This should not be necessary, but nixpkgs v24.11 requires it.
- name: "Update Haskell Package List"
+ if: "${{ steps.release.outputs.release_created }}"
run: |
nix-shell --pure --run "cabal update --ignore-project"
diff --git a/src/HostPatrol/Remote.hs b/src/HostPatrol/Remote.hs
index 1f1f60e..2a9d32a 100644
--- a/src/HostPatrol/Remote.hs
+++ b/src/HostPatrol/Remote.hs
@@ -85,6 +85,7 @@ compileHostReport ch = do
_hostReportTimezone <- _toParseError _hostName $ _getParse pure "HOSTPATROL_GENERAL_TIMEZONE" kvs
_hostReportCloud <- _mkCloud _hostName kvs
_hostReportHardware <- _mkHardware _hostName kvs
+ _hostReportClock <- _mkClock _hostName =<< _fetchHostClockInfo h
_hostReportKernel <- _mkKernel _hostName kvs
_hostReportDistribution <- _mkDistribution _hostName kvs
_hostReportDockerContainers <- _fetchHostDockerContainers h
@@ -177,6 +178,17 @@ _fetchHostCloudInfo h@Types.Host {..} =
parseKVs <$> _toSshError _hostName (Z.Ssh.runScript (getHostSshConfig h) $(embedStringFile "src/scripts/cloud.sh") ["bash"])
+-- | Attempts to retrieve remote host clock information and return it
+-- as a list of key/value tuples.
+_fetchHostClockInfo
+ :: MonadIO m
+ => MonadError HostPatrolError m
+ => Types.Host
+ -> m [(T.Text, T.Text)]
+_fetchHostClockInfo h@Types.Host {..} =
+ parseKVs <$> _toSshError _hostName (Z.Ssh.runScript (getHostSshConfig h) $(embedStringFile "src/scripts/clock.sh") ["bash"])
+
+
-- | Attempts to retrieve remote host docker containers information and return it.
--
-- Returns 'Nothing' if remote host is not identified as a Docker
@@ -284,6 +296,19 @@ _mkHardware h kvs =
pure Types.Hardware {..}
+-- | Smart constructor for remote host clock information.
+_mkClock
+ :: MonadError HostPatrolError m
+ => Z.Ssh.Destination
+ -> [(T.Text, T.Text)]
+ -> m Types.Clock
+_mkClock h kvs =
+ _toParseError h $ do
+ _clockNtpAvailability <- _getParse pure "HOSTPATROL_CLOCK_NTP" kvs
+ _clockTimeSyncStatus <- _getParse pure "HOSTPATROL_CLOCK_NTP_SYNCHRONIZED" kvs
+ pure Types.Clock {..}
+
+
-- | Smart constructor for remote host kernel information.
_mkKernel
:: MonadError HostPatrolError m
diff --git a/src/HostPatrol/Types.hs b/src/HostPatrol/Types.hs
index f680b0c..1b11016 100644
--- a/src/HostPatrol/Types.hs
+++ b/src/HostPatrol/Types.hs
@@ -136,6 +136,7 @@ data HostReport = HostReport
, _hostReportTimezone :: !T.Text
, _hostReportCloud :: !Cloud
, _hostReportHardware :: !Hardware
+ , _hostReportClock :: !Clock
, _hostReportKernel :: !Kernel
, _hostReportDistribution :: !Distribution
, _hostReportDockerContainers :: !(Maybe [DockerContainer])
@@ -160,6 +161,7 @@ instance ADC.HasCodec HostReport where
<*> ADC.requiredField "timezone" "Timezone of the host." ADC..= _hostReportTimezone
<*> ADC.requiredField "cloud" "Cloud information." ADC..= _hostReportCloud
<*> ADC.requiredField "hardware" "Hardware information." ADC..= _hostReportHardware
+ <*> ADC.requiredField "clock" "Clock information." ADC..= _hostReportClock
<*> ADC.requiredField "kernel" "Kernel information." ADC..= _hostReportKernel
<*> ADC.requiredField "distribution" "Distribution information." ADC..= _hostReportDistribution
<*> ADC.requiredField "dockerContainers" "List of Docker containers if the host is a Docker host." ADC..= _hostReportDockerContainers
@@ -233,6 +235,29 @@ instance ADC.HasCodec Hardware where
<*> ADC.requiredField "diskRoot" "Total disk space of root (`/`) filesystem (in GB)." ADC..= _hardwareDiskRoot
+-- * Clock Information
+
+
+-- | Data definition for host's clock information.
+data Clock = Clock
+ { _clockNtpAvailability :: !T.Text
+ , _clockTimeSyncStatus :: !T.Text
+ }
+ deriving (Eq, Generic, Show)
+ deriving (Aeson.FromJSON, Aeson.ToJSON) via (ADC.Autodocodec Clock)
+
+
+instance ADC.HasCodec Clock where
+ codec =
+ _codec ADC.> "Clock Information"
+ where
+ _codec =
+ ADC.object "Clock" $
+ Clock
+ <$> ADC.requiredField "ntp_availability" "Indicates NTP availability and enablement." ADC..= _clockNtpAvailability
+ <*> ADC.requiredField "time_sync_status" "Indicates time synchronisation status." ADC..= _clockTimeSyncStatus
+
+
-- * Kernel Information
diff --git a/src/scripts/clock.sh b/src/scripts/clock.sh
new file mode 100644
index 0000000..7363202
--- /dev/null
+++ b/src/scripts/clock.sh
@@ -0,0 +1,43 @@
+#!/usr/bin/env sh
+
+###################
+# SHELL BEHAVIOUR #
+###################
+
+# Stop on errors:
+set -e
+
+###############
+# DEFINITIONS #
+###############
+
+# Prints a key/value pair in SHELL variable format. Value is printed
+# within double-quotes, and double-quotes in the variable are escaped.
+_print_var() {
+ printf '%s="%s"\n' "${1}" "$(echo "${2}" | sed 's/"/\\"/g')"
+}
+
+##########
+# CHECKS #
+##########
+
+## Check if timedatectl is available:
+if ! command -v timedatectl >/dev/null; then
+ _print_var "HOSTPATROL_CLOCK_NTP" "timedatectl not available"
+ _print_var "HOSTPATROL_CLOCK_NTP_SYNCHRONIZED" "timedatectl not available"
+ exit 0
+fi
+
+## Check if we have a recent enough version of timedatectl:
+if ! timedatectl show 2>/dev/null; then
+ _print_var "HOSTPATROL_CLOCK_NTP" "timedatectl failed (too old?)"
+ _print_var "HOSTPATROL_CLOCK_NTP_SYNCHRONIZED" "timedatectl failed (too old?)"
+ exit 0
+fi
+
+#############
+# PROCEDURE #
+#############
+
+_print_var "HOSTPATROL_CLOCK_NTP" "$(timedatectl show --property=NTP --value)"
+_print_var "HOSTPATROL_CLOCK_NTP_SYNCHRONIZED" "$(timedatectl show --property=NTPSynchronized --value)"
diff --git a/website/src/components/report/ShowHostDetails.tsx b/website/src/components/report/ShowHostDetails.tsx
index 4a8063a..7d1c920 100644
--- a/website/src/components/report/ShowHostDetails.tsx
+++ b/website/src/components/report/ShowHostDetails.tsx
@@ -56,15 +56,25 @@ export function ShowHostDetails({ host, data }: { host: HostReport; data: HostPa
-
-
+
+
+
+
+
+
Systemd
+ Clock Sync
Tags
@@ -397,6 +398,15 @@ export function TabulateHosts({
{host.systemdServices.length} / {host.systemdTimers.length}
+
+
+
+ {host.clock.time_sync_status.length > 10
+ ? host.clock.time_sync_status.slice(0, 8) + '...'
+ : host.clock.time_sync_status}
+
+
+
{(host.host.tags || []).map((x) => (
diff --git a/website/src/lib/data.ts b/website/src/lib/data.ts
index 6f46ce7..3353ef5 100644
--- a/website/src/lib/data.ts
+++ b/website/src/lib/data.ts
@@ -12,8 +12,14 @@ export const HOSTPATROL_REPORT_SCHEMA = {
items: {
$comment: 'Report Error\nReportError',
properties: {
- host: { $comment: 'Host of the error if applicable.', type: 'string' },
- message: { $comment: 'Error message.', type: 'string' },
+ host: {
+ $comment: 'Host of the error if applicable.',
+ type: 'string',
+ },
+ message: {
+ $comment: 'Error message.',
+ type: 'string',
+ },
},
required: ['message'],
type: 'object',
@@ -30,53 +36,155 @@ export const HOSTPATROL_REPORT_SCHEMA = {
items: {
$comment: 'SSH Public Key Information\nSshPublicKey',
properties: {
- comment: { $comment: 'Comment on the public key.', type: 'string' },
- data: { $comment: 'Original information.', type: 'string' },
- fingerprint: { $comment: 'Fingerprint of the public key.', type: 'string' },
+ comment: {
+ $comment: 'Comment on the public key.',
+ type: 'string',
+ },
+ data: {
+ $comment: 'Original information.',
+ type: 'string',
+ },
+ fingerprint: {
+ $comment: 'Fingerprint of the public key.',
+ type: 'string',
+ },
length: {
$comment: 'Length of the public key.',
maximum: 2147483647,
minimum: -2147483648,
type: 'number',
},
- type: { $comment: 'Type of the public key.', type: 'string' },
+ type: {
+ $comment: 'Type of the public key.',
+ type: 'string',
+ },
},
required: ['fingerprint', 'comment', 'length', 'type', 'data'],
type: 'object',
},
type: 'array',
},
+ clock: {
+ $comment: 'Clock information.\nClock Information\nClock',
+ properties: {
+ ntp_availability: {
+ $comment: 'Indicates NTP availability and enablement.',
+ type: 'string',
+ },
+ time_sync_status: {
+ $comment: 'Indicates time synchronisation status.',
+ type: 'string',
+ },
+ },
+ required: ['time_sync_status', 'ntp_availability'],
+ type: 'object',
+ },
cloud: {
$comment: 'Cloud information.\nCloud Information\nCloud',
properties: {
hostAvailabilityZone: {
$comment: 'Host availability zone.',
- anyOf: [{ type: 'null' }, { type: 'string' }],
+ anyOf: [
+ {
+ type: 'null',
+ },
+ {
+ type: 'string',
+ },
+ ],
},
hostLocalAddress: {
$comment: 'Local address of the host.',
- anyOf: [{ type: 'null' }, { type: 'string' }],
+ anyOf: [
+ {
+ type: 'null',
+ },
+ {
+ type: 'string',
+ },
+ ],
},
hostLocalHostname: {
$comment: 'Local hostname of the host.',
- anyOf: [{ type: 'null' }, { type: 'string' }],
+ anyOf: [
+ {
+ type: 'null',
+ },
+ {
+ type: 'string',
+ },
+ ],
+ },
+ hostRegion: {
+ $comment: 'Host region.',
+ anyOf: [
+ {
+ type: 'null',
+ },
+ {
+ type: 'string',
+ },
+ ],
},
- hostRegion: { $comment: 'Host region.', anyOf: [{ type: 'null' }, { type: 'string' }] },
hostRemoteAddress: {
$comment: 'Remote address of the host.',
- anyOf: [{ type: 'null' }, { type: 'string' }],
+ anyOf: [
+ {
+ type: 'null',
+ },
+ {
+ type: 'string',
+ },
+ ],
},
hostRemoteHostname: {
$comment: 'Remote hostname of the host.',
- anyOf: [{ type: 'null' }, { type: 'string' }],
+ anyOf: [
+ {
+ type: 'null',
+ },
+ {
+ type: 'string',
+ },
+ ],
},
hostReservedAddress: {
$comment: 'Reserved address of the host.',
- anyOf: [{ type: 'null' }, { type: 'string' }],
+ anyOf: [
+ {
+ type: 'null',
+ },
+ {
+ type: 'string',
+ },
+ ],
+ },
+ hostType: {
+ $comment: 'Host type.',
+ anyOf: [
+ {
+ type: 'null',
+ },
+ {
+ type: 'string',
+ },
+ ],
+ },
+ id: {
+ $comment: 'Host identifier.',
+ anyOf: [
+ {
+ type: 'null',
+ },
+ {
+ type: 'string',
+ },
+ ],
+ },
+ name: {
+ $comment: 'Cloud name.',
+ type: 'string',
},
- hostType: { $comment: 'Host type.', anyOf: [{ type: 'null' }, { type: 'string' }] },
- id: { $comment: 'Host identifier.', anyOf: [{ type: 'null' }, { type: 'string' }] },
- name: { $comment: 'Cloud name.', type: 'string' },
},
required: [
'hostReservedAddress',
@@ -97,16 +205,35 @@ export const HOSTPATROL_REPORT_SCHEMA = {
properties: {
codename: {
$comment: "Distribution codename (cat /etc/os-release | grep 'VERSION_CODENAME=').",
- anyOf: [{ type: 'null' }, { type: 'string' }],
+ anyOf: [
+ {
+ type: 'null',
+ },
+ {
+ type: 'string',
+ },
+ ],
},
description: {
$comment: "Distribution description (cat /etc/os-release | grep 'PRETTY_NAME=').",
type: 'string',
},
- id: { $comment: "Distribution ID (cat /etc/os-release | grep 'ID=').", type: 'string' },
- name: { $comment: "Distribution name (cat /etc/os-release | grep 'NAME=')).", type: 'string' },
- release: { $comment: "Distribution release (cat /etc/os-release | grep 'VERSION_ID=').", type: 'string' },
- version: { $comment: "Distribution version (cat /etc/os-release | grep 'VERSION=').", type: 'string' },
+ id: {
+ $comment: "Distribution ID (cat /etc/os-release | grep 'ID=').",
+ type: 'string',
+ },
+ name: {
+ $comment: "Distribution name (cat /etc/os-release | grep 'NAME=')).",
+ type: 'string',
+ },
+ release: {
+ $comment: "Distribution release (cat /etc/os-release | grep 'VERSION_ID=').",
+ type: 'string',
+ },
+ version: {
+ $comment: "Distribution version (cat /etc/os-release | grep 'VERSION=').",
+ type: 'string',
+ },
},
required: ['description', 'codename', 'release', 'version', 'name', 'id'],
type: 'object',
@@ -114,7 +241,9 @@ export const HOSTPATROL_REPORT_SCHEMA = {
dockerContainers: {
$comment: 'List of Docker containers if the host is a Docker host.',
anyOf: [
- { type: 'null' },
+ {
+ type: 'null',
+ },
{
items: {
$comment: 'Docker Container Information\nDockerContainer',
@@ -123,10 +252,22 @@ export const HOSTPATROL_REPORT_SCHEMA = {
$comment: 'Date/time when the container is created at.\nDate/time in ISO8601 format.',
type: 'string',
},
- id: { $comment: 'ID of the container..', type: 'string' },
- image: { $comment: 'Image the container is created from.', type: 'string' },
- name: { $comment: 'Name of the container.', type: 'string' },
- running: { $comment: 'Indicates if the container is running.', type: 'boolean' },
+ id: {
+ $comment: 'ID of the container..',
+ type: 'string',
+ },
+ image: {
+ $comment: 'Image the container is created from.',
+ type: 'string',
+ },
+ name: {
+ $comment: 'Name of the container.',
+ type: 'string',
+ },
+ running: {
+ $comment: 'Indicates if the container is running.',
+ type: 'boolean',
+ },
},
required: ['running', 'created', 'image', 'name', 'id'],
type: 'object',
@@ -144,8 +285,14 @@ export const HOSTPATROL_REPORT_SCHEMA = {
minimum: -2147483648,
type: 'number',
},
- diskRoot: { $comment: 'Total disk space of root (`/`) filesystem (in GB).', type: 'number' },
- ramTotal: { $comment: 'Total RAM (in GB).', type: 'number' },
+ diskRoot: {
+ $comment: 'Total disk space of root (`/`) filesystem (in GB).',
+ type: 'number',
+ },
+ ramTotal: {
+ $comment: 'Total RAM (in GB).',
+ type: 'number',
+ },
},
required: ['diskRoot', 'ramTotal', 'cpuCount'],
type: 'object',
@@ -153,55 +300,114 @@ export const HOSTPATROL_REPORT_SCHEMA = {
host: {
$comment: 'Host descriptor.\nHost Descriptor\nHost',
properties: {
- data: { $comment: 'Arbitrary data for the host.' },
- id: { $comment: 'External identifier of the host.', type: 'string' },
+ data: {
+ $comment: 'Arbitrary data for the host.',
+ },
+ id: {
+ $comment: 'External identifier of the host.',
+ type: 'string',
+ },
knownSshKeys: {
$comment: 'Known SSH public keys for the host.',
items: {
$comment: 'SSH Public Key Information\nSshPublicKey',
properties: {
- comment: { $comment: 'Comment on the public key.', type: 'string' },
- data: { $comment: 'Original information.', type: 'string' },
- fingerprint: { $comment: 'Fingerprint of the public key.', type: 'string' },
+ comment: {
+ $comment: 'Comment on the public key.',
+ type: 'string',
+ },
+ data: {
+ $comment: 'Original information.',
+ type: 'string',
+ },
+ fingerprint: {
+ $comment: 'Fingerprint of the public key.',
+ type: 'string',
+ },
length: {
$comment: 'Length of the public key.',
maximum: 2147483647,
minimum: -2147483648,
type: 'number',
},
- type: { $comment: 'Type of the public key.', type: 'string' },
+ type: {
+ $comment: 'Type of the public key.',
+ type: 'string',
+ },
},
required: ['fingerprint', 'comment', 'length', 'type', 'data'],
type: 'object',
},
type: 'array',
},
- name: { $comment: 'Name of the host.', type: 'string' },
+ name: {
+ $comment: 'Name of the host.',
+ type: 'string',
+ },
ssh: {
$comment: 'SSH configuration.\nSSH Configuration\nSshConfig',
properties: {
- destination: { $comment: 'SSH destination.', type: 'string' },
- options: { $comment: 'SSH options.', items: { type: 'string' }, type: 'array' },
+ destination: {
+ $comment: 'SSH destination.',
+ type: 'string',
+ },
+ options: {
+ $comment: 'SSH options.',
+ items: {
+ type: 'string',
+ },
+ type: 'array',
+ },
},
required: ['destination'],
type: 'object',
},
- tags: { $comment: 'Arbitrary tags for the host.', items: { type: 'string' }, type: 'array' },
- url: { $comment: 'URL to external host information.', type: 'string' },
+ tags: {
+ $comment: 'Arbitrary tags for the host.',
+ items: {
+ type: 'string',
+ },
+ type: 'array',
+ },
+ url: {
+ $comment: 'URL to external host information.',
+ type: 'string',
+ },
},
required: ['name'],
type: 'object',
},
- hostname: { $comment: 'Hostname of the host.', type: 'string' },
+ hostname: {
+ $comment: 'Hostname of the host.',
+ type: 'string',
+ },
kernel: {
$comment: 'Kernel information.\nKernel Information\nKernel',
properties: {
- machine: { $comment: 'Architecture the kernel is running on (uname -m).', type: 'string' },
- name: { $comment: 'Kernel name (uname -s).', type: 'string' },
- node: { $comment: 'Name of the node kernel is running on (uname -n).', type: 'string' },
- os: { $comment: 'Operating system the kernel is driving (uname -o).', type: 'string' },
- release: { $comment: 'Kernel release (uname -r).', type: 'string' },
- version: { $comment: 'Kernel version (uname -v).', type: 'string' },
+ machine: {
+ $comment: 'Architecture the kernel is running on (uname -m).',
+ type: 'string',
+ },
+ name: {
+ $comment: 'Kernel name (uname -s).',
+ type: 'string',
+ },
+ node: {
+ $comment: 'Name of the node kernel is running on (uname -n).',
+ type: 'string',
+ },
+ os: {
+ $comment: 'Operating system the kernel is driving (uname -o).',
+ type: 'string',
+ },
+ release: {
+ $comment: 'Kernel release (uname -r).',
+ type: 'string',
+ },
+ version: {
+ $comment: 'Kernel version (uname -v).',
+ type: 'string',
+ },
},
required: ['os', 'machine', 'version', 'release', 'name', 'node'],
type: 'object',
@@ -211,16 +417,28 @@ export const HOSTPATROL_REPORT_SCHEMA = {
items: {
$comment: 'SSH Public Key Information\nSshPublicKey',
properties: {
- comment: { $comment: 'Comment on the public key.', type: 'string' },
- data: { $comment: 'Original information.', type: 'string' },
- fingerprint: { $comment: 'Fingerprint of the public key.', type: 'string' },
+ comment: {
+ $comment: 'Comment on the public key.',
+ type: 'string',
+ },
+ data: {
+ $comment: 'Original information.',
+ type: 'string',
+ },
+ fingerprint: {
+ $comment: 'Fingerprint of the public key.',
+ type: 'string',
+ },
length: {
$comment: 'Length of the public key.',
maximum: 2147483647,
minimum: -2147483648,
type: 'number',
},
- type: { $comment: 'Type of the public key.', type: 'string' },
+ type: {
+ $comment: 'Type of the public key.',
+ type: 'string',
+ },
},
required: ['fingerprint', 'comment', 'length', 'type', 'data'],
type: 'object',
@@ -229,15 +447,22 @@ export const HOSTPATROL_REPORT_SCHEMA = {
},
systemdServices: {
$comment: 'List of systemd services found on host.',
- items: { type: 'string' },
+ items: {
+ type: 'string',
+ },
type: 'array',
},
systemdTimers: {
$comment: 'List of systemd timers found on host.',
- items: { type: 'string' },
+ items: {
+ type: 'string',
+ },
type: 'array',
},
- timezone: { $comment: 'Timezone of the host.', type: 'string' },
+ timezone: {
+ $comment: 'Timezone of the host.',
+ type: 'string',
+ },
},
required: [
'systemdTimers',
@@ -247,6 +472,7 @@ export const HOSTPATROL_REPORT_SCHEMA = {
'dockerContainers',
'distribution',
'kernel',
+ 'clock',
'hardware',
'cloud',
'timezone',
@@ -262,11 +488,28 @@ export const HOSTPATROL_REPORT_SCHEMA = {
items: {
$comment: 'SSH Public Key Information\nSshPublicKey',
properties: {
- comment: { $comment: 'Comment on the public key.', type: 'string' },
- data: { $comment: 'Original information.', type: 'string' },
- fingerprint: { $comment: 'Fingerprint of the public key.', type: 'string' },
- length: { $comment: 'Length of the public key.', maximum: 2147483647, minimum: -2147483648, type: 'number' },
- type: { $comment: 'Type of the public key.', type: 'string' },
+ comment: {
+ $comment: 'Comment on the public key.',
+ type: 'string',
+ },
+ data: {
+ $comment: 'Original information.',
+ type: 'string',
+ },
+ fingerprint: {
+ $comment: 'Fingerprint of the public key.',
+ type: 'string',
+ },
+ length: {
+ $comment: 'Length of the public key.',
+ maximum: 2147483647,
+ minimum: -2147483648,
+ type: 'number',
+ },
+ type: {
+ $comment: 'Type of the public key.',
+ type: 'string',
+ },
},
required: ['fingerprint', 'comment', 'length', 'type', 'data'],
type: 'object',
@@ -276,10 +519,22 @@ export const HOSTPATROL_REPORT_SCHEMA = {
meta: {
$comment: 'Meta information of the report.\nReport Meta Information\nReportMeta',
properties: {
- buildHash: { $comment: 'Build hash of the application.', type: 'string' },
- buildTag: { $comment: 'Build tag of the application.', type: 'string' },
- timestamp: { $comment: 'Timestamp of the report.\nDate/time in ISO8601 format.', type: 'string' },
- version: { $comment: 'Version of the application.', type: 'string' },
+ buildHash: {
+ $comment: 'Build hash of the application.',
+ type: 'string',
+ },
+ buildTag: {
+ $comment: 'Build tag of the application.',
+ type: 'string',
+ },
+ timestamp: {
+ $comment: 'Timestamp of the report.\nDate/time in ISO8601 format.',
+ type: 'string',
+ },
+ version: {
+ $comment: 'Version of the application.',
+ type: 'string',
+ },
},
required: ['timestamp', 'version'],
type: 'object',