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