| 778 | 1 #!/usr/bin/env python | 
|  | 2 # -*- coding: utf-8 -*- | 
|  | 3 | 
|  | 4 """ | 
|  | 5 monitor custom endpoints form a JSON file | 
|  | 6 """ | 
|  | 7 | 
|  | 8 # imports | 
|  | 9 import argparse | 
|  | 10 import json | 
|  | 11 import os | 
|  | 12 import requests | 
|  | 13 import statuspage | 
|  | 14 import sys | 
|  | 15 import time | 
|  | 16 | 
|  | 17 # What we want but I'm not sure if it works in python 2.6 | 
|  | 18 # http://stackoverflow.com/questions/6921699/can-i-get-json-to-load-into-an-ordereddict-in-python | 
|  | 19 | 
|  | 20 | 
|  | 21 def main(args=sys.argv[1:]): | 
|  | 22     """CLI""" | 
|  | 23 | 
|  | 24     # parse command line | 
|  | 25     parser = argparse.ArgumentParser(description=__doc__) | 
|  | 26     parser.add_argument('input', | 
|  | 27                         type=argparse.FileType('r'), | 
|  | 28                         help="input JSON file") | 
|  | 29     parser.add_argument('--customer', dest='customer', nargs='+', | 
|  | 30                         help="customer to act on") | 
|  | 31     parser.add_argument('--list-customers', dest='list_customers', | 
|  | 32                         action='store_true', default=False, | 
|  | 33                         help="list customers and return") | 
|  | 34     parser.add_argument('--list-endpoints', dest='list_endpoints', | 
|  | 35                         action='store_true', default=False, | 
|  | 36                         help="list endpoints and exit") | 
|  | 37     parser.add_argument('--failures', dest='output_failures', | 
|  | 38                         action='store_true', default=False, | 
|  | 39                         help="output failures only") | 
|  | 40     parser.add_argument('--successes', dest='output_successes', | 
|  | 41                         action='store_true', default=False, | 
|  | 42                         help="output successes only") | 
|  | 43     parser.add_argument('--timeout', dest='timeout', | 
|  | 44                         type=float, default=60, | 
|  | 45                         help="request timeout in seconds [DEFAULT: %(default)s]") | 
|  | 46     parser.add_argument('--verify', dest='verify', | 
|  | 47                         action='store_true', default=False, | 
|  | 48                         help="verify HTTPS requests") | 
|  | 49     parser.add_argument('--statuspage', '--statuspage-io', dest='statuspage_io', | 
|  | 50                         nargs=2, metavar=('PAGE_ID', 'API_KEY'), | 
|  | 51                         help="post status to statuspage.io") | 
|  | 52     options = parser.parse_args(args) | 
|  | 53 | 
|  | 54     # sanity | 
|  | 55     if options.output_failures and options.output_successes: | 
|  | 56         parser.error("Cannot use both --failures and --successes") | 
|  | 57 | 
|  | 58     if not options.verify: | 
|  | 59         # disable insecure warnings: | 
|  | 60         # http://stackoverflow.com/questions/27981545/suppress-insecurerequestwarning-unverified-https-request-is-being-made-in-pytho | 
|  | 61         requests.packages.urllib3.disable_warnings() | 
|  | 62 | 
|  | 63     # load input file JSON | 
|  | 64     data = json.load(options.input) | 
|  | 65 | 
|  | 66     # check for conformity | 
|  | 67     message = "Expected a dict with a list of URLs" | 
|  | 68     if not isinstance(data, dict): | 
|  | 69         parser.error(message) | 
|  | 70     if not all([isinstance(value, list) | 
|  | 71                 for value in data.values()]): | 
|  | 72         parser.error(message) | 
|  | 73 | 
|  | 74     if options.list_customers: | 
|  | 75         # list customers and exit | 
|  | 76         print (json.dumps(sorted(data.keys()), | 
|  | 77                           indent=2)) | 
|  | 78         return | 
|  | 79 | 
|  | 80     if options.customer: | 
|  | 81         # choose data only for subset of customers | 
|  | 82         missing = [customer for customer in options.customer | 
|  | 83                    if customer not in data] | 
|  | 84         if missing: | 
|  | 85             parser.error("Customers not found: {0}".format(', '.join(missing))) | 
|  | 86         data = dict([(customer, data[customer]) | 
|  | 87                      for customer in options.customer]) | 
|  | 88 | 
|  | 89     if options.list_endpoints: | 
|  | 90         # list all endpoints and exit | 
|  | 91         print (json.dumps(sorted(sum(data.values(), [])), indent=2)) | 
|  | 92         return | 
|  | 93 | 
|  | 94     # hit endpoints | 
|  | 95     results = {} | 
|  | 96     for customer, endpoints in data.items(): | 
|  | 97         results[customer] = [] | 
|  | 98         for endpoint in endpoints: | 
|  | 99             data = {'url': endpoint, 'success': False} | 
|  | 100             try: | 
|  | 101                 start = time.time() | 
|  | 102                 response = requests.get(endpoint, | 
|  | 103                                         verify=options.verify, | 
|  | 104                                         timeout=options.timeout) | 
|  | 105                 end = time.time() | 
|  | 106                 data['status_code'] = response.status_code | 
|  | 107                 data['reason'] = response.reason | 
|  | 108                 response.raise_for_status() | 
|  | 109                 data['success'] = True | 
|  | 110             except requests.HTTPError as e: | 
|  | 111                 end = time.time() | 
|  | 112                 pass  # we already have the data we need | 
|  | 113             except requests.exceptions.SSLError as e: | 
|  | 114                 end = time.time() | 
|  | 115                 data['reason'] = "SSL Error: {0}".format(e) | 
|  | 116             except (requests.ConnectionError, requests.Timeout) as e: | 
|  | 117                 # despite what it says at | 
|  | 118                 # http://stackoverflow.com/questions/16511337/correct-way-to-try-except-using-python-requests-module | 
|  | 119                 # both `requests.Timeout` and `requests.ConnectionError` seem to fall to this point | 
|  | 120                 # so we descriminate for either | 
|  | 121                 end = time.time() | 
|  | 122 | 
|  | 123                 try: | 
|  | 124                     args = e.args[0].reason.args | 
|  | 125                     if len(args) == 2: | 
|  | 126                         message = args[-1] | 
|  | 127                     elif len(args) == 1: | 
|  | 128                         message = args[0].split(':', 1)[-1] | 
|  | 129                 except AttributeError: | 
|  | 130                     # Protocol error | 
|  | 131                     message = "{0} {1}".format(e.args[0].args[0], | 
|  | 132                                                e.args[0].args[-1].args[-1]) | 
|  | 133                 data['reason'] = message | 
|  | 134             data['duration'] = end - start | 
|  | 135             results[customer].append(data) | 
|  | 136 | 
|  | 137     # obtain successes + failures | 
|  | 138     failures = {} | 
|  | 139     successes = {} | 
|  | 140     for customer, result in results.items(): | 
|  | 141         for item in result: | 
|  | 142             append_to = successes if item['success'] else failures | 
|  | 143             append_to.setdefault(customer, []).append(item) | 
|  | 144 | 
|  | 145     # serialize results | 
|  | 146     if options.statuspage_io: | 
|  | 147         api = statuspage.StatuspageIO(*options.statuspage_io) | 
|  | 148 | 
|  | 149         # get existing components | 
|  | 150         components = api.components() | 
|  | 151 | 
|  | 152         # figure out what components we need to make | 
|  | 153         # we use the customer name as a key so we will go from there | 
|  | 154         name_map = dict([(component['name'], component) | 
|  | 155                        for component in components]) | 
|  | 156         id_map = {} | 
|  | 157         for customer, data in results.items(): | 
|  | 158 | 
|  | 159             for item in data: | 
|  | 160                 name = item['url'] | 
|  | 161                 if 'code' in item: | 
|  | 162                     description = "{code} {reason}".format(**item) | 
|  | 163                 else: | 
|  | 164                     description = item['reason'] | 
|  | 165                 endpoint_component = name_map.get(name) | 
|  | 166                 if endpoint_component is None: | 
|  | 167                     try: | 
|  | 168                         endpoint_component = api.create_component(name, description) | 
|  | 169                     except Exception as e: | 
|  | 170                         import pdb; pdb.set_trace() | 
|  | 171 | 
|  | 172                 # determine status | 
|  | 173                 if item['success']: | 
|  | 174                     status = "operational" | 
|  | 175                 else: | 
|  | 176                     status = "operational" # TODO: you could be better | 
|  | 177                 api.update_component(endpoint_component['id'], | 
|  | 178                                      description=description, | 
|  | 179                                      status=status) | 
|  | 180     else: | 
|  | 181         if options.output_failures: | 
|  | 182             output_data = failures | 
|  | 183         elif options.output_successes: | 
|  | 184             output_data = successes | 
|  | 185         else: | 
|  | 186             output_data = results | 
|  | 187         print (json.dumps(output_data, sort_keys=True, indent=2)) | 
|  | 188 | 
|  | 189     # exit with appropriate code | 
|  | 190     sys.exit(1 if failures else 0) | 
|  | 191 | 
|  | 192 if __name__ == '__main__': | 
|  | 193     main() |