Back in 2021, I added a check for the latest NextCloud version to Icinga based on a forum post from 2019 that suggested scraping the version from a file their GitHub website repository. Today I was looking at something on Icinga and it occurred to me I have not installed any NextCloud updates for a while, a quick check and I determined that the latest version in that file stalled at 23.0.3 (released 21 March 2022, now unsupported and not the last of the 23.x line - 23.0.12 is the last) and I am now 3 major versions behind - the current release is 26.0.0 on 21 March 2023.

I, obviously, upgraded but need to revisit my check to make it work again. I will reiterate, as I did in my last post, it is very frustrating NextCloud don’t publish any “latest version number” endpoint for self-hosters to check.

This time, I looked at the source for the Updater Server and found that there is a file, config.php that contains all of the release numbers and the version to which they should upgrade next (generally speaking, the last version of a release line is a special case that points to the next major release). As my monitoring server, which is where the check command runs (as it can remotely get the version from my instance), has PHP for other monitoring tools (including Icinga Web) I can actually use PHP to parse this file properly and output the latest version.

This is the code to fetch, parse and output the latest version:

workdir=$(mktemp -d)
cd $workdir

curl -O -L https://github.com/nextcloud/updater_server/raw/master/config/config.php

if [[ $? -ne 0 ]]
then
  echo "Unable to fetch config.php from upstream git repository"
  exit 3  # Internal error
fi

# Quoting the HEREDOC marker disables variable interpolation
php <<'EOF'
  <?php
    $nc_versions = require 'config.php';
    $stable_versions = array_keys($nc_versions['stable']);
  
    // Sort versions according to semantic versioning
    usort($stable_versions, 'version_compare');
    $latest_release = end($stable_versions);
  
    // There may be multiple next versions, keyed by percentage chance of
    // the updater server giving that version to a client. Search the list
    // for the largest 'latest' version.
    $latest_version = null;
    foreach ($nc_versions['stable'][$latest_release] as $percent => $info) {
      if (is_null($latest_version) or version_compare($latest_version, $info['latest'], '<')) {
          $latest_version = $info['latest'];
      }
    }
  
    // Output the latest version number
    echo($latest_version);
  ?>
EOF
# Tidy up
cd -
rm -rf $workdir

This is the revised nagios plugin script, in full:

#!/bin/bash

if which curl &>/dev/null
then
  CURL_CMD="curl -Ls"
elif which wget &>/dev/null
then
  CURL_CMD="wget -q -O-"
else
  echo "Unable to locate a webscraper (curl or wget) - cannot continue" >&1
  exit 3  # Usage/internal error
fi

if ! which php &>/dev/null
then
  echo "Unable to locate php (to parse upstream version) - cannot continue" >&1
  exit 3  # Usage/internal error 
fi

if [[ $1 == "--help" ]]
then
  echo "Usage: $0 [-v [-v [-v]]] target_server_to_check"
  echo "This plugin requires either curl or wget to be installed, internet"
  echo "access to fetch, and php to parse, the upstream version file."
  echo "-v: increase the verbosity level one level for each '-v' (up to 3)"
  exit 0
fi

VERBOSITY_LEVEL=0
if [[ $1 == "-v" ]]
then
  while [[ $1 == "-v" ]]
  do
    VERBOSITY_LEVEL=$(( VERBOSITY_LEVEL + 1 ))
    shift
  done
  if [[ $VERBOSITY_LEVEL -gt 3 ]]
  then
    echo "Cannot increase verbosity beyond 3!"
    # Could just set it down to 3 but this is a usage error...
    exit 3  # Usage/internal error
  fi
fi

TARGET_SERVER=$1

# BEGIN fetch and parse upstream updater's configuration file.
# Create temporary directory 
workdir=$(mktemp -d)
# Go to that directory
cd $workdir
# Fetch the updater's configuration file (which has all the versions)
$CURL_CMD https://github.com/nextcloud/updater_server/raw/master/config/config.php > config.php

if [[ $? -ne 0 ]]
then
  echo "Unable to fetch config.php from upstream git repository"
  exit 3  # Internal error
fi

# Parse the file
CURRENT_RELEASE=$(
  # Quoting the HEREDOC marker disables variable interpolation and allows
  # us to indent it (by including spaces inside the quotes).
  # I used tr to delete spaces, rather than HEREDOC's '-' and tab indentation
  # due to problems getting editors to retain tabs and not replace them with
  # spaces.
  php <<'  EOF' | tr -d ' '
    <?php
      $nc_versions = require 'config.php';
      $stable_versions = array_keys($nc_versions['stable']);
    
      // Sort versions according to semantic versioning
      usort($stable_versions, 'version_compare');
      $latest_release = end($stable_versions);
    
      // There may be multiple next versions, keyed by percentage chance of
      // the updater server giving that version to a client. Search the list
      // for the largest 'latest' version.
      $latest_version = null;
      foreach ($nc_versions['stable'][$latest_release] as $percent => $info) {
        if (is_null($latest_version) or version_compare($latest_version, $info['latest'], '<')) {
            $latest_version = $info['latest'];
        }
      }
    
      // Output the latest version number
      echo($latest_version);
    ?>
  EOF
)
# Tidy up
cd - >/dev/null
rm -rf $workdir
# END fetch and parse upstream updater's configuration file.

if [[ $VERBOSITY_LEVEL -ge 2 ]]
then
  echo "Got current release $CURRENT_RELEASE from NextCloud's updater github source."
fi

# We could use jq to parse the json properly (jq -r '.versionstring')
# however that introduces a dependency that we do not need if we use
# sed.  Just a note for the future, if the sed method breaks down.
INSTALLED_RELEASE=$(
  $CURL_CMD https://$TARGET_SERVER/status.php |
  sed -n 's/^.*,"versionstring":"\([^"]\+\)",.*$/\1/p'
)

if [[ $VERBOSITY_LEVEL -ge 2 ]]
then
  echo "Got release $INSTALLED_RELEASE from server's status.php response."
fi

if [[ $CURRENT_RELEASE == $INSTALLED_RELEASE ]]
then
  echo "OK: $INSTALLED_RELEASE = $CURRENT_RELEASE"
  exit 0  # Ok
else
  # Version mismatch - try to find if it's the major version or minor
  if [[ ${CURRENT_RELEASE%%.*} == ${INSTALLED_RELEASE%%.*} ]]
  then
    # Major versions match
    echo "WARNING: Minor version mismatch" \
      "($INSTALLED_RELEASE != $CURRENT_RELEASE)"
    # Only do multi-line output with verbosity 1 or higher
    # see: https://nagios-plugins.org/doc/guidelines.html#PLUGOUTPUT
    [[ $VERBOSITY_LEVEL -ge 1 ]] && echo "Major version of both is ${CURRENT_RELEASE%%.*}"
    exit 1  # Warning
  else
    # Major version mismatch
    echo "CRITICAL: Major version mismatch" \
      "($INSTALLED_RELEASE != $CURRENT_RELEASE)"
    exit 2  # Critical
  fi
fi