Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit cc1c1da

Browse files
author
Michael Bianchi
committed
Init working copy
0 parents commit cc1c1da

File tree

3 files changed

+353
-0
lines changed

3 files changed

+353
-0
lines changed

‎LICENSE‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2019 Michael Bianchi
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

‎pingdomWrapper.py‎

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
#!/usr/bin/env python
2+
3+
import requests
4+
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
5+
from requests_oauthlib import OAuth1
6+
from math import floor
7+
from datetime import datetime, timedelta
8+
import yaml
9+
import logging
10+
import os
11+
import sys
12+
from enum import Enum
13+
14+
# TODO get LOG_LEVEL or DEBUG from env vars
15+
# logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
16+
logger = logging.getLogger('pingdom.pingdomWrapper')
17+
handler = logging.StreamHandler(sys.stdout)
18+
logger.setLevel(logging.DEBUG)
19+
logger.addHandler(handler)
20+
21+
22+
class AuthenticationError(Exception):
23+
pass
24+
25+
26+
AuthType = Enum('AuthType', 'HTTPBASICAUTH HTTPDIGESTAUTH OAUTH1 OAUTH2 NONE')
27+
28+
29+
class Pingdom(object):
30+
def __init__(
31+
self,
32+
user=None,
33+
password=None,
34+
client_app_key=None,
35+
client_app_secret=None,
36+
user_oauth_token=None,
37+
user_oauth_token_secret=None,
38+
api_app_key=None
39+
):
40+
'''Set up client for API communications
41+
This is where you'll need to specify all the authentication and
42+
required headers
43+
44+
Preference will be given towards passed in variables, otherwise
45+
environment variables will be used
46+
47+
Config file is supported but discouraged since it's a common
48+
source of credential leaks
49+
'''
50+
# Setup Host here
51+
self.url = 'https://api.pingdom.com'
52+
# Setup Session object for all future API calls
53+
self.session = requests.Session()
54+
55+
# Setup authentication
56+
# If interested in using a config file instead of env vars, load with
57+
# self._load_key(config_key, path)
58+
# Feel free to clear out auth methods not implemented by the API
59+
auth_type = AuthType[os.getenv('AUTHTYPE', default='NONE')]
60+
if (auth_type == AuthType.HTTPBASICAUTH or
61+
auth_type == AuthType.HTTPDIGESTAUTH):
62+
if not user:
63+
user = os.getenv('CLIENT_USER')
64+
if not password:
65+
password = os.getenv('CLIENT_PASSWORD')
66+
if auth_type == AuthType.HTTPBASICAUTH:
67+
self.session.auth = HTTPBasicAuth(user, password)
68+
else:
69+
self.session.auth = HTTPDigestAuth(user, password)
70+
if auth_type == AuthType.OAUTH1:
71+
if not client_app_key:
72+
client_app_key = os.getenv('CLIENT_APP_KEY')
73+
if not client_app_secret:
74+
client_app_secret = os.getenv('CLIENT_APP_SECRET')
75+
if not user_oauth_token:
76+
user_oauth_token = os.getenv('USER_OAUTH_TOKEN')
77+
if not user_oauth_token_secret:
78+
user_oauth_token_secret = os.getenv('USER_OAUTH_TOKEN_SECRET')
79+
self.session.auth = OAuth1(
80+
client_app_key,
81+
client_app_secret,
82+
user_oauth_token,
83+
user_oauth_token_secret
84+
)
85+
if auth_type == AuthType.OAUTH2:
86+
# Feel free to create a PR if you want to contribute
87+
raise NotImplementedError("OAuth2 currently not supported")
88+
89+
# Some APIs require an API key in a header in addition to or instead
90+
# of standard authentication methods
91+
if not api_app_key:
92+
api_app_key = os.getenv('API_APP_KEY')
93+
self.session.headers.update({'App-Key': api_app_key})
94+
95+
# Setup any additional headers required by the API
96+
# This sometimes includes additional account info
97+
account_owner = os.getenv('PINGDOM_ACCOUNT_OWNER')
98+
if account_owner:
99+
self.session.headers.update({'account-email': account_owner})
100+
101+
logger.info('Authenticating...')
102+
if self._authenticate():
103+
logger.info('Authentication Successful!')
104+
else:
105+
logger.info('Authentication Failed!')
106+
raise AuthenticationError('Authentication Failed!')
107+
108+
def _load_key(self, config_key, path):
109+
'''Example function for loading config values from a yml file
110+
'''
111+
with open(path) as stream:
112+
yaml_data = yaml.safe_load(stream)
113+
return yaml_data[config_key]
114+
115+
def _authenticate(self):
116+
'''Authenticate by making simple request
117+
Some APIs will offer a simple auth validation endpoint, some
118+
won't.
119+
I like to make the simplest authenticated request when
120+
instantiating the client just to make sure the auth works
121+
'''
122+
resp_json = self._make_request('/api/2.1/servertime', 'GET')
123+
try:
124+
pass
125+
except AuthenticationError as e:
126+
raise e
127+
print(resp_json)
128+
if resp_json:
129+
return True
130+
else:
131+
return False
132+
133+
def _make_request(self, endpoint, method, query_params=None, body=None):
134+
'''Handles all requests to Pingdom API
135+
'''
136+
url = self.url + endpoint
137+
req = requests.Request(method, url, params=query_params, json=body)
138+
prepped = self.session.prepare_request(req)
139+
140+
self._pprint_request(prepped)
141+
142+
r = self.session.send(prepped)
143+
144+
self._pprint_response(r)
145+
146+
# Handle all response codes as elegantly as needed in a single spot
147+
if r.status_code == requests.codes.ok:
148+
try:
149+
resp_json = r.json()
150+
logger.debug('Response: {}'.format(resp_json))
151+
return resp_json
152+
except ValueError:
153+
return r.text
154+
155+
elif r.status_code == 401:
156+
logger.info("Authentication Unsuccessful!")
157+
try:
158+
resp_json = r.json()
159+
logger.debug('Details: ' + str(resp_json))
160+
raise AuthenticationError(resp_json)
161+
except ValueError:
162+
raise
163+
164+
# Raises HTTP error if status_code is 4XX or 5XX
165+
elif r.status_code >= 400:
166+
logger.error('Received a ' + str(r.status_code) + ' error!')
167+
try:
168+
logger.debug('Details: ' + str(r.json()))
169+
except ValueError:
170+
pass
171+
r.raise_for_status()
172+
173+
def _pprint_request(self, prepped):
174+
'''
175+
method endpoint HTTP/version
176+
Host: host
177+
header_key: header_value
178+
179+
body
180+
'''
181+
method = prepped.method
182+
url = prepped.path_url
183+
headers = '\n'.join('{}: {}'.format(k, v) for k, v in
184+
prepped.headers.items())
185+
# Print body if present or empty string if not
186+
body = prepped.body or ""
187+
logger.debug(
188+
'{}\n{} {} HTTP/1.1\n{}\n\n{}'.format(
189+
'-----------REQUEST-----------',
190+
method,
191+
url,
192+
headers,
193+
body
194+
)
195+
)
196+
197+
def _pprint_response(self, r):
198+
'''
199+
HTTP/version status_code status_text
200+
header_key: header_value
201+
202+
body
203+
'''
204+
# Not using requests_toolbelt.dump because I want to be able to
205+
# print the request before submitting and response after
206+
# ref: https://stackoverflow.com/a/35392830/8418673
207+
208+
httpv0, httpv1 = list(str(r.raw.version))
209+
httpv = 'HTTP/{}.{}'.format(httpv0, httpv1)
210+
status_code = r.status_code
211+
status_text = r.reason
212+
headers = '\n'.join('{}: {}'.format(k, v) for k, v in
213+
r.headers.items())
214+
body = r.text or ""
215+
216+
logger.debug(
217+
'{}\n{} {} {}\n{}\n\n{}'.format(
218+
'-----------RESPONSE-----------',
219+
httpv,
220+
status_code,
221+
status_text,
222+
headers,
223+
body
224+
)
225+
)
226+
227+
def list_checks(
228+
self,
229+
tags=[],
230+
offset=0,
231+
limit=20
232+
):
233+
'''Get list of checks in Pingdom
234+
235+
:tags: list of tags to filter checks on - checks will match ANY tag
236+
'''
237+
endpoint = '/api/2.1/checks'
238+
params = {}
239+
if tags:
240+
params['tags'] = ','.join(tags)
241+
else:
242+
tags = None
243+
params['offset'] = offset
244+
params['limit'] = limit
245+
params['include_tags'] = True
246+
247+
return self._make_request(endpoint, 'GET', query_params=params)
248+
249+
def get_check_details(self, check_id):
250+
"""TODO: Docstring for get_check_details.
251+
"""
252+
endpoint = '/api/2.1/checks/{}'.format(check_id)
253+
return self._make_request(endpoint, 'GET')
254+
255+
def pause_unpause_mult_checks(self, check_list, pause=True):
256+
"""Pauses or Unpauses multiple checks
257+
258+
:check_list: list of check ids to update
259+
:pause: True to pause checks, False to unpause checks
260+
:returns: TODO
261+
262+
"""
263+
endpoint = '/api/2.1/checks'
264+
check_ids = ','.join(check_list)
265+
params = {}
266+
params['checkids'] = check_ids
267+
params['paused'] = pause
268+
return self._make_request(endpoint, 'PUT', query_params=params)
269+
270+
def create_new_check(
271+
self,
272+
name,
273+
host,
274+
**kwargs
275+
):
276+
'''Create new check in Pingdom
277+
'''
278+
endpoint = '/api/2.1/checks'
279+
params = {}
280+
params['name'] = name
281+
params['host'] = host
282+
# Allows an arbitrary number of keyword arguments to this method
283+
# to be converted into query_params
284+
for key, value in kwargs.items():
285+
params[key] = value
286+
return self._make_request(endpoint, 'POST', query_params=params)
287+
288+
def create_new_maintenance_window(
289+
self,
290+
description,
291+
from_datetime,
292+
to_datetime,
293+
uptime_ids=None,
294+
tms_ids=None
295+
):
296+
'''Create new maintenance window in Pingdom
297+
'''
298+
endpoint = '/api/2.1/maintenance'
299+
params = {}
300+
params['description'] = description
301+
302+
if from_datetime is type(datetime):
303+
# floor needed because datetime.timestamp() returns
304+
# >>> datetime.datetime.now().timestamp()
305+
# 1550686321.920955 - <epoch seconds>.<epoch microseconds>
306+
params['from'] = floor(from_datetime.timestamp())
307+
else:
308+
params['from'] = from_datetime
309+
params['to'] = floor(to_datetime.timestamp())
310+
if uptime_ids:
311+
# Convert easy to use list to comma-delimited for the API
312+
params['uptimeids'] = ','.join(map(str, uptime_ids))
313+
if tms_ids:
314+
params['tmsids'] = ','.join(map(str, tms_ids))
315+
return self._make_request(endpoint, 'POST', query_params=params)
316+
317+
318+
if __name__ == "__main__":
319+
account = Pingdom('test@example.com', 'password')
320+
from_datetime = datetime.now()
321+
to_datetime = datetime.now() + timedelta(1)
322+
account.list_checks()
323+
account.create_new_maintenance_window('name', from_datetime, to_datetime,
324+
uptime_ids='123,345')

‎requirements.txt‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
certifi==2018年11月29日
2+
chardet==3.0.4
3+
idna==2.8
4+
oauthlib==3.0.1
5+
PyYAML==4.2b1
6+
requests==2.21.0
7+
requests-oauthlib==1.2.0
8+
urllib3==1.24.1

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /