| 
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()
 |