From b57eddcd748858014c247e69b76cd7d0488658cb Mon Sep 17 00:00:00 2001 From: Simon Lauger Date: Sat, 10 Jul 2021 23:48:40 +0200 Subject: [PATCH] feat: fetch installed firmware version via NITRO API --- README.md | 33 ++++------ check_nsupdates.py | 159 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 137 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 750d5f4..8150e51 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,12 @@ # check_nsupdates -## Build 13.0 71.40/71.44 and above - -Since 13.0 Build 71.40/71.44 it is no longer possible to obtain the installed firmware version via the `pluginlist.xml`. It seems that the firmware does not appear in any other public file. So there is no longer an option for an external update monitoring. - -If you still need an external monitoring you could switch to a responder policy with a [HTTP callout](https://docs.citrix.com/en-us/citrix-adc/current-release/appexpert/http-callout/how-http-callouts-work.html) to the [nsversion](https://developer-docs.citrix.com/projects/citrix-adc-nitro-api-reference/en/latest/configuration/ns/nsversion/) API endpoint. Be aware that you need to connect to a SNIP with management access. An internal HTTP callout to to the NSIP will be dropped. - -The plugin will support this method soon. - ## Documentation Nagios Plugin which checks if an update is available for a Citrix NetScaler. The plugin connects to citrix.com and parses the [RSS feed](https://www.citrix.com/content/citrix/en_us/downloads/netscaler-adc.rss) to get a dict of all available NetScaler relases and the latest available build per release. -To get the installed version of the NetScaler the plugin makes use of the fact that the versioning for the NetScaler and the EPA clienttools is the same schema. The version of the hosted EPA client is exposed to the outside world in `/vpn/pluginlist.xml` on each NetScaler Gateway vServer. - -Example: -``` --bash$ curl -q https://gateway.example.com/vpn/pluginlist.xml 2> /dev/null | egrep '.*version="(1[012])\.([0-9])\.([0-9]{2})\.([0-9]{1,2})".*' - version="12.1.48.13" path="/epa/scripts/win/nsepa_setup.exe" - version="12.1.48.13" path="/epa/scripts/win/nsepa_setup64.exe" - version="12.1.48.13" path="/vpns/scripts/vista/AGEE_setup.exe" -``` +To get the installed version of the target NetScaler the plugin uses the NITRO API. ## Dependencies @@ -33,9 +17,18 @@ Example: ## Usage ``` --bash$ ./check_nsupdates.py gateway1.example.com gateway2.example.com -WARNING: gateway1.example.com: update available (installed: 11.1 56.19, available: 11.1 57.11) -WARNING: gateway2.example.com: update available (installed: 12.0 56.20, available: 12.0 57.19) +# check with default credentials +./check_nsupdates.py -U http://10.0.0.100 +WARNING: http://10.0.0.240: update available (installed: 13.0 71.44, available: 13.0 82.42) + +# check with credentials given via cli +./check_nsupdates.py -U http://10.0.0.100 -u admin -p admin + +# check with credentials given by ENV +export NETSCALER_USERNAME=admin +export NETSCALER_PASSWORD=admin +./check_nsupdates.py -U http://10.0.0.100 +WARNING: http://10.0.0.240: update available (installed: 13.0 71.44, available: 13.0 82.42) ``` ## Author diff --git a/check_nsupdates.py b/check_nsupdates.py index 91a013c..03dc000 100755 --- a/check_nsupdates.py +++ b/check_nsupdates.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # check_nsupdates.py # @@ -14,11 +14,13 @@ # @date: 2018-06-10 # @version v1.1.0 +import os import re import sys import feedparser import requests import urllib3 +import argparse from packaging import version urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -33,8 +35,8 @@ class check_nsversion: # New - Citrix ADC Release (Maintenance Phase) 12.1 Build 53.12 ctx_pattern = 'New \- (NetScaler|Citrix ADC) Release( \(Feature Phase\)| \(Maintenance Phase\))? (1[0123]\.[0-9]) Build ([0-9]{2}\.[0-9]{1,2})' - # var nsversion="12,0,57,19"; - ns_pattern = '.*version="(1[0123])\.([0-9])\.([0-9]{2})\.([0-9]{1,2})".*' + # NetScaler NS13.0: Build 71.44.nc, Date: Dec 26 2020, 11:31:14 (64-bit) + ns_pattern = 'NetScaler NS(1[0123])\.([0-9]): Build ([0-9]{2})\.([0-9]{1,2}).*' # All major releases and latest available build per major version releases = {} @@ -53,12 +55,27 @@ class check_nsversion: # plugin exitcode exitcode = 0 + # debug mode + debug = False + def __init__(self): self.feed = feedparser.parse(self.ctx_url) self.ctx_regex = re.compile(self.ctx_pattern) self.ns_regex = re.compile(self.ns_pattern) self.parse() + def set_baseurl(self, baseurl): + self.baseurl = baseurl + + def set_username(self, username): + self.username = username + + def set_password(self, password): + self.password = password + + def set_debug(self, value): + self.debug = bool(value) + def parse(self): for item in self.feed['items']: matches = self.ctx_regex.match(item['title']) @@ -81,41 +98,113 @@ def nagios_exit(self): for message in self.messages: print(message) sys.exit(self.exitcode) - - def check(self, fqdn): - url = "https://" + fqdn + "/vpn/pluginlist.xml" + + def check(self): + url = self.baseurl + "/nitro/v1/config/nsversion" try: - response = requests.get(url, verify=False) - except: - self.add_message(self.return_codes['CRITICAL'], "CRITICAL: " + fqdn + " is unreachable") + response = requests.get(url, verify=False, headers={ + 'X-NITRO-USER': self.username, + 'X-NITRO-PASS': self.password, + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }) + except Exception as e: + if self.debug: + print(e) + self.add_message(self.return_codes['CRITICAL'], "CRITICAL: http request to " + self.baseurl + " failed") return self.return_codes['CRITICAL'] - for line in response.iter_lines(): - matches = self.ns_regex.match(line) - if matches: - major = str(matches.group(1) + '.' + matches.group(2)) - build = str(matches.group(3) + '.' + matches.group(4)) - if self.releases[major] == build or version.parse(self.releases[major]) < version.parse(build): - self.add_message(self.return_codes['OK'], "OK: " + fqdn + ": up to date (installed: " + major + " " + build + ", available: " + major + " " + self.releases[major] + ")") - return self.return_codes['OK'] - else: - self.add_message(self.return_codes['WARNING'], "WARNING: " + fqdn + ": update available (installed: " + major + " " + build + ", available: " + major + " " + self.releases[major] + ")") - return self.return_codes['WARNING'] - self.add_message(self.return_codes['UNKOWN'], "UNKOWN: " + fqdn + ": could not find a nsversion string in response") - return self.return_codes['UNKOWN'] -# check if a arugment is given to the script -if len(sys.argv) < 2: - print("usage: " + sys.argv[0] + " [] [...]") - sys.exit(check_nsversion.return_codes['UNKOWN']) + if response.status_code != 200: + if self.debug: + print(response.text) + self.add_message(self.return_codes['CRITICAL'], "CRITICAL: http request to " + self.baseurl + " returned status code " + str(response.status_code)) + return self.return_codes['CRITICAL'] + + version_str = response.json()['nsversion']['version'] + + matches = self.ns_regex.match(version_str) -# start plugin and parse rss feed -plugin = check_nsversion() + major = str(matches.group(1) + '.' + matches.group(2)) + build = str(matches.group(3) + '.' + matches.group(4)) -# check all hosts -for fqdn in sys.argv: - if fqdn == sys.argv[0]: - continue - plugin.check(fqdn) + if self.releases[major] == build or version.parse(self.releases[major]) < version.parse(build): + self.add_message(self.return_codes['OK'], "OK: " + self.baseurl + ": up to date (installed: " + major + " " + build + ", available: " + major + " " + self.releases[major] + ")") + return self.return_codes['OK'] + else: + self.add_message(self.return_codes['WARNING'], "WARNING: " + self.baseurl + ": update available (installed: " + major + " " + build + ", available: " + major + " " + self.releases[major] + ")") + return self.return_codes['WARNING'] + + self.add_message(self.return_codes['UNKOWN'], "UNKOWN: " + self.baseurl + ": could not find a nsversion string in response") + return self.return_codes['UNKOWN'] -# exit and print results -plugin.nagios_exit() +if __name__ == '__main__': + add_args = ( + { + '--url': { + 'alias': '-U', + 'help': 'Base URL of the Citrix ADC. If not set the value from the ENV NETSCALER_URL is used.', + 'type': str, + 'default': None, + 'required': True, + } + }, + { + '--username': { + 'alias': '-u', + 'help': 'Username for Citrix ADC. If not set the value from the ENV NETSCALER_USERNAME is used. Defaults to nsroot.', + 'type': str, + 'default': os.environ.get('NETSCALER_USERNAME', 'nsroot'), + 'required': False, + } + }, + { + '--password': { + 'alias': '-p', + 'help': 'Password for Citrix ADC. If not set the value from the ENV NETSCALER_PASSWORD is used. Defaults to nsroot', + 'type': str, + 'default': os.environ.get('NETSCALER_PASSWORD', 'nsroot'), + 'required': False, + } + } + ) + + parser = argparse.ArgumentParser(description='Nagios Plugin which checks if an update is available for a Citrix ADC (formerly Citrix NetScaler)') + + for item in add_args: + arg = list(item.keys())[0] + params = list(item.values())[0] + parser.add_argument( + params['alias'], arg, + type=params['type'], + help=params['help'], + default=params['default'], + required=params['required'], + ) + + parser.add_argument('--verbose', '-v', action='store_true', help='Enable verbose mode and print the requests python response') + + # parse args + args = parser.parse_args() + + # start plugin and parse rss feed + plugin = check_nsversion() + + # check for url + if not args.url: + plugin.add_message(3, 'netscaler url is not defined or empty') + plugin.nagios_exit() + + # add args to class + plugin.set_baseurl(args.url) + plugin.set_username(args.username) + plugin.set_password(args.password) + + # debug mode + if (args.verbose): + plugin.set_debug(True) + + # run check + plugin.check() + + # exit and print results + plugin.nagios_exit()