├── MANIFEST.in ├── .gitignore ├── setup.py ├── LICENSE ├── README.rst └── xymon └── __init__.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst LICENSE 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS* 2 | *.pyc 3 | *.bak 4 | *.swp 5 | build/* 6 | dist/* 7 | *.egg-info 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from xymon import __version__ as package_version 3 | 4 | 5 | with open('README.rst') as doc: 6 | long_description = doc.read() 7 | 8 | setup( 9 | name = "Xymon", 10 | author = 'Rob McBroom', 11 | author_email = 'pypi@skurfer.com', 12 | url = 'https://github.com/skurfer/python-xymon', 13 | license = "Don’t Be a Dick", 14 | packages = ['xymon'], 15 | version = package_version, 16 | description = 'Update and query statuses on a Xymon server', 17 | long_description = long_description, 18 | install_requires = [], 19 | ) 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DON'T BE A DICK PUBLIC LICENSE 2 | ============================== 3 | 4 | > Version 1, December 2009 5 | 6 | > Copyright (C) 2009 Philip Sturgeon 7 | 8 | Everyone is permitted to copy and distribute verbatim or modified copies of 9 | this license document, and changing it is allowed as long as the name is 10 | changed. 11 | 12 | > DON'T BE A DICK PUBLIC LICENSE 13 | > TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 14 | 15 | 1. Do whatever you like with the original work, just don't be a dick. 16 | 17 | Being a dick includes - but is not limited to - the following instances: 18 | 19 | 1a. Outright copyright infringement - Don't just copy this and change 20 | the name. 21 | 1b. Selling the unmodified original with no work done what-so-ever, 22 | that's REALLY being a dick. 23 | 1c. Modifying the original work to contain hidden harmful content. That 24 | would make you a PROPER dick. 25 | 26 | 2. If you become rich through modifications, related works/services, or 27 | supporting the original work, share the love. Only a dick would make loads 28 | off this work and not buy the original work's creator(s) a pint. 29 | 30 | 3. Code is provided with no warranty. Using somebody else's code and bitching 31 | when it goes wrong makes you a DONKEY dick. Fix the problem yourself. A 32 | non-dick would submit the fix back. 33 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | Xymon Python Library 3 | ==================== 4 | 5 | This library allows basic communication with a Xymon server directly from a Python script. It works with Python 2.6, 2.7, and 3. It can be used on a system with no Xymon client installed. 6 | 7 | Some basic examples are shown here. See ``help(Xymon)`` for details. 8 | 9 | Installation 10 | ------------ 11 | 12 | .. code-block:: bash 13 | 14 | $ pip install Xymon 15 | 16 | Getting Started 17 | --------------- 18 | 19 | Get an instance of the ``Xymon`` class to talk to the server. 20 | 21 | .. code-block:: pycon 22 | 23 | >>> from xymon import Xymon 24 | >>> server = Xymon('xymon.domain.tld', 1984) 25 | 26 | The server name and port number are optional. The server name will default to the ``$XYMSRV`` environment variable if your script is being run by a Xymon client, or ``localhost`` if the variable isn't set. The port defaults to ``1984``. 27 | 28 | Reporting 29 | --------- 30 | 31 | Report a status to the server: 32 | 33 | .. code-block:: pycon 34 | 35 | >>> server.report('webserver01', 'https', 'yellow', 'slow HTTP response') 36 | 37 | Querying 38 | -------- 39 | 40 | Getting status data: 41 | 42 | .. code-block:: pycon 43 | 44 | >>> server.appfeed(host='ldap.*', test='ldaps') 45 | {'ldap01': 46 | {'ldaps': 47 | {'status': 'green', 48 | 'changed': 1396294952, 49 | 'time': 1396462829, 50 | 'url': 'https://xymon.domain.tld/xymon-cgi/svcstatus.sh?HOST=ldap01&SERVICE=ldaps', 51 | 'summary': 'green Wed Apr 2 14:19:56 2014 ldaps ok '} 52 | }, 53 | 'ldap02': 54 | {'ldaps': 55 | {'status': 'green', 56 | 'changed': 1396294952, 57 | 'time': 1396462829, 58 | 'url': 'https://xymon.domain.tld/xymon-cgi/svcstatus.sh?HOST=ldap02&SERVICE=ldaps', 59 | 'summary': 'green Wed Apr 2 14:19:56 2014 ldaps ok '} 60 | } 61 | } 62 | 63 | This communicates with the server using its ``appfeed.cgi`` interface. If called with no arguments, ``appfeed()`` will return data for all tests on all hosts. Results can be limited by host, test, page, and color. 64 | 65 | Note that ``host`` can be a pattern as described in `Xymon's documentation`_. 66 | 67 | To just get the status of a single service on a single host as a string, use ``status()``: 68 | 69 | .. code-block:: pycon 70 | 71 | >>> server.status('ldap01', 'ldaps') 72 | 'green' 73 | 74 | If you want data for more than one host/test, it's probably more efficient to get all the data using ``appfeed()`` and pull out what you want. 75 | 76 | .. _Xymon's Documentation: http://www.xymon.com/xymon/help/manpages/man1/appfeed.cgi.1.html 77 | -------------------------------------------------------------------------------- /xymon/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | import socket 6 | from time import ctime 7 | from collections import defaultdict 8 | from xml.etree import ElementTree 9 | if sys.version_info[0] == 2: 10 | # Python 2.x 11 | from urllib import urlopen, urlencode 12 | else: 13 | # Python 3.x 14 | from urllib.request import urlopen 15 | from urllib.parse import urlencode 16 | 17 | 18 | __version__ = '2.0.0' 19 | 20 | 21 | class Xymon(object): 22 | """Communicate with a Xymon server 23 | 24 | server: Hostname or IP address of a Xymon server. Defaults to $XYMSRV 25 | if set, or 'localhost' if not. 26 | port: The port number the server listens on. Defaults to 1984. 27 | """ 28 | def __init__(self, server=None, port=1984): 29 | if server is None: 30 | server = os.environ.get('XYMSRV', 'localhost') 31 | self.server = server 32 | self.port = port 33 | 34 | def report(self, host, test, color, message, interval='30m'): 35 | """Report status to a Xymon server 36 | 37 | host: The hostname to associate the report with. 38 | test: The name of the test or service. 39 | color: The color to set. Can be 'green', 'yellow', 'red', or 'clear' 40 | message: Details about the current state. 41 | interval: An optional interval between tests. The status will change 42 | to purple if no further reports are sent in this time. 43 | """ 44 | args = { 45 | 'host': host, 46 | 'test': test, 47 | 'color': color, 48 | 'message': message, 49 | 'interval': interval, 50 | 'date': ctime(), 51 | } 52 | report = '''status+{interval} {host}.{test} {color} {date} 53 | {message}'''.format(**args) 54 | self.send_message(report) 55 | 56 | def data(self, host, test, raw_data): 57 | """Report data to a Xymon server 58 | 59 | host: The hostname to associate the report with. 60 | test: The name of the test or service. 61 | data: The RRD data. 62 | """ 63 | args = { 64 | 'host': host, 65 | 'test': test, 66 | 'data': raw_data, 67 | } 68 | report = '''data {host}.{test}\n{data}'''.format(**args) 69 | self.send_message(report) 70 | 71 | def send_message(self, message): 72 | """Report arbitrary information to the server 73 | 74 | See the xymon(1) man page for message syntax. 75 | """ 76 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 77 | try: 78 | server_ip = socket.gethostbyname(self.server) 79 | message = message + '\n' 80 | s.connect((server_ip, self.port)) 81 | s.sendall(message.encode()) 82 | except: 83 | # Re-raising the exceptions as this should not pass silently. 84 | raise 85 | finally: 86 | s.close() 87 | 88 | def appfeed( 89 | self, host=None, test=None, page=None, 90 | color=None, cgi=None, ssl=True, 91 | ): 92 | """Query a Xymon server for the current status of tests 93 | 94 | Returns a dictionary of status information by host, then by test. 95 | Suitable for conversion to JSON, YAML, etc. 96 | 97 | host: Limit the results to a specific host or pattern 98 | test: Limit the results to a specific test 99 | color: Limit the results to specific colors (comma separated). 100 | Includes all colors by default. 101 | cgi: The directory prefix for appfeed.cgi. Defaults to 102 | $XYMONSERVERCGIURL if set, or '/xymon-cgi' if not. 103 | ssl: Uses HTTPS if True (default), HTTP if False 104 | 105 | If called without arguments, all data for all tests is returned. 106 | 107 | See http://www.xymon.com/xymon/help/manpages/man1/appfeed.cgi.1.html 108 | """ 109 | # default value 110 | err_host = host if host else 'nohost' 111 | err_test = test if test else 'notest' 112 | statuses = { 113 | err_host: { 114 | err_test: { 115 | 'status': 'unknown', 116 | 'summary': 'data never retrieved', 117 | } 118 | } 119 | } 120 | proto = ('http', 'https')[bool(ssl)] 121 | if cgi is None: 122 | cgi = os.environ.get('XYMONSERVERCGIURL', '/xymon-cgi') 123 | if color is None: 124 | color = 'blue,purple,clear,yellow,green,red' 125 | test_filter = 'color={0}'.format(color) 126 | if test is not None: 127 | test_filter = 'test={0} {1}'.format(test, test_filter) 128 | if host is not None: 129 | test_filter = 'host={0} {1}'.format(host, test_filter) 130 | if page is not None: 131 | test_filter = 'page={0} {1}'.format(page, test_filter) 132 | url_params = urlencode({'filter': test_filter}) 133 | base = '{0}://{1}'.format(proto, self.server) 134 | appfeed = '{0}{1}/appfeed.sh'.format(base, cgi) 135 | url = appfeed + '?' + url_params 136 | try: 137 | remote = urlopen(url) 138 | status_xml = remote.read() 139 | remote.close() 140 | try: 141 | root = ElementTree.fromstring(status_xml) 142 | statuses = defaultdict(dict) 143 | for status_element in root.getiterator('ServerStatus'): 144 | hostname = status_element.find('Servername').text 145 | service = status_element.find('Type').text 146 | status = { 147 | 'status': status_element.find('Status').text, 148 | 'summary': status_element.find('MessageSummary').text, 149 | 'url': base + status_element.find('DetailURL').text, 150 | 'time': int(status_element.find('LogTime').text), 151 | 'changed': int(status_element.find('LastChange').text), 152 | } 153 | if status['status'] == 'blue': 154 | status['disabled'] = \ 155 | status_element.find('DisableText').text.strip() 156 | status['by'] = \ 157 | status_element.find('DisabledBy').text 158 | statuses[hostname][service] = status 159 | if len(statuses) == 0: 160 | statuses = { 161 | err_host: { 162 | err_test: { 163 | 'status': 'unmonitored', 164 | 'summary': 'no data for {0}.{1}'.format( 165 | err_host, err_test 166 | ), 167 | } 168 | } 169 | } 170 | else: 171 | # convert defaultdict to a normal dictionary 172 | statuses = dict(statuses) 173 | except Exception: 174 | statuses = { 175 | err_host: { 176 | err_test: { 177 | 'status': 'unknown', 178 | 'summary': 'Error parsing XML from Xymon', 179 | } 180 | } 181 | } 182 | except Exception: 183 | statuses = { 184 | err_host: { 185 | err_test: { 186 | 'status': 'unknown', 187 | 'summary': 'Error getting data from ' + appfeed, 188 | } 189 | } 190 | } 191 | return statuses 192 | 193 | def status(self, host, test): 194 | """Return only the status of a single host/service as a string""" 195 | result = self.appfeed(host=host, test=test) 196 | if host in result and test in result[host]: 197 | return result[host][test]['status'] 198 | else: 199 | return 'unknown' 200 | --------------------------------------------------------------------------------