Skip to content

Commit

Permalink
feat: fetch installed firmware version via NITRO API
Browse files Browse the repository at this point in the history
  • Loading branch information
slauger committed Jul 10, 2021
1 parent 25354b4 commit b57eddc
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 55 deletions.
33 changes: 13 additions & 20 deletions 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

Expand All @@ -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
Expand Down
159 changes: 124 additions & 35 deletions check_nsupdates.py
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
#
# check_nsupdates.py
#
Expand All @@ -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)
Expand All @@ -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 = {}
Expand All @@ -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'])
Expand All @@ -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] + " <hostname> [<hostname>] [...]")
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()

0 comments on commit b57eddc

Please sign in to comment.