Skip to content

Commit a57561e

Browse files
authored
New REST API client for python + tests (#52)
* New REST API client for python + tests * Switch to plain urllib3 to lessen external dependencies * Import urlencode based on python version * Add SSL cert validation; update tests * Before tests, install reqs from both files. * Add safeties from uninit'd API tokens or URL * Updated dependencies to support SSL connections * Remove merge artifact. * Move client initializtion into Setup * Add urllib3[secure] to test set * Fix client call in parameter.
1 parent 001bf6c commit a57561e

File tree

3 files changed

+534
-0
lines changed

3 files changed

+534
-0
lines changed

instana/api.py

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
"""
2+
This module provides a client for the Instana REST API.
3+
4+
Use of this client requires the URL of your Instana account dashboard
5+
and an API token. The API token can be generated in your dashboard under
6+
Settings > Access Control > API Tokens.
7+
8+
See the associated REST API documentation here:
9+
https://documenter.getpostman.com/view/1527374/instana-api/2TqWQh#intro
10+
11+
The API currently uses the requests package to make the REST calls to the API.
12+
As such, requests response objects are returned from API calls.
13+
"""
14+
import os
15+
import sys
16+
import json
17+
import time
18+
import certifi
19+
import urllib3
20+
from .log import logger as log
21+
22+
23+
PY2 = sys.version_info[0] == 2
24+
PY3 = sys.version_info[0] == 3
25+
26+
if PY2:
27+
from urllib import urlencode
28+
import urllib3.contrib.pyopenssl
29+
urllib3.contrib.pyopenssl.inject_into_urllib3()
30+
else:
31+
import urllib3
32+
from urllib.parse import urlencode
33+
34+
35+
# For use with the Token related API calls
36+
token_config = {
37+
"id": "",
38+
"name": "",
39+
"canConfigureServiceMapping": False,
40+
"canConfigureEumApplications": False,
41+
"canConfigureUsers": False,
42+
"canInstallNewAgents": False,
43+
"canSeeUsageInformation": False,
44+
"canConfigureIntegrations": False,
45+
"canSeeOnPremLicenseInformation": False,
46+
"canConfigureRoles": False,
47+
"canConfigureCustomAlerts": False,
48+
"canConfigureApiTokens": False,
49+
"canConfigureAgentRunMode": False,
50+
"canViewAuditLog": False,
51+
"canConfigureObjectives": False
52+
}
53+
54+
# For use with the Bindings related API calls
55+
binding_config = {
56+
"id": "1",
57+
"enabled": True,
58+
"triggering": False,
59+
"severity": 5,
60+
"text": "text",
61+
"description": "desc",
62+
"expirationTime": 60000,
63+
"query": "",
64+
"ruleIds": [
65+
"2"
66+
]
67+
}
68+
69+
# For use with the Rule related API calls
70+
rule_config = {
71+
"id": "1",
72+
"name": "test rule",
73+
"entityType": "mariaDbDatabase",
74+
"metricName": "status.MAX_USED_CONNECTIONS",
75+
"rollup": 1000,
76+
"window": 60000,
77+
"aggregation": "avg",
78+
"conditionOperator": ">=",
79+
"conditionValue": 10
80+
}
81+
82+
# For use with the Role related API calls
83+
role_config = {
84+
"id": "1",
85+
"name": "Developer",
86+
"implicitViewFilter": "",
87+
"canConfigureServiceMapping": True,
88+
"canConfigureEumApplications": True,
89+
"canConfigureUsers": False,
90+
"canInstallNewAgents": False,
91+
"canSeeUsageInformation": False,
92+
"canConfigureIntegrations": False,
93+
"canSeeOnPremLicenseInformation": False,
94+
"canConfigureRoles": False,
95+
"canConfigureCustomAlerts": False,
96+
"canConfigureApiTokens": False,
97+
"canConfigureAgentRunMode": False,
98+
"canViewAuditLog": False,
99+
"canConfigureObjectives": False
100+
}
101+
102+
103+
class APIClient(object):
104+
"""
105+
The Python client to the Instana REST API.
106+
107+
This client supports the use of environment variables. These environment variables
108+
will override any passed in options:
109+
110+
INSTANA_API_TOKEN=asdffdsa
111+
INSTANA_BASE_URL=https://test-test.instana.io
112+
113+
Example usage:
114+
from instana.api import APIClient
115+
c = APIClient(base_url="https://test-test.instana.io", api_token='asdffdsa')
116+
117+
# Retrieve the current application view
118+
x = c.application_view()
119+
x.json()
120+
121+
# Retrieve snapshots results from a query
122+
y = c.snapshots("entity.selfType:webService entity.service.name:\"pwpush.com\"")
123+
"""
124+
base_url = None
125+
api_token = None
126+
127+
def __init__(self, **kwds):
128+
for key in kwds:
129+
self.__dict__[key] = kwds[key]
130+
131+
if "INSTANA_API_TOKEN" in os.environ:
132+
self.api_token = os.environ["INSTANA_API_TOKEN"]
133+
134+
if "INSTANA_BASE_URL" in os.environ:
135+
self.base_url = os.environ["INSTANA_BASE_URL"]
136+
137+
if self.base_url is None or self.api_token is None:
138+
log.warn("APIClient: API token or Base URL not set. No-op mode")
139+
else:
140+
self.api_key = "apiToken %s" % self.api_token
141+
self.headers = {'Authorization': self.api_key}
142+
self.http = urllib3.PoolManager(cert_reqs='CERT_REQUIRED',
143+
ca_certs=certifi.where())
144+
145+
def ts_now(self):
146+
return int(round(time.time() * 1000))
147+
148+
def build_url(self, path, query_args):
149+
if self.base_url and self.api_token:
150+
url = self.base_url + path
151+
else:
152+
url = ""
153+
154+
if query_args:
155+
encoded_args = urlencode(query_args)
156+
url = url + '?' + encoded_args
157+
return url
158+
159+
def get(self, path, query_args=None):
160+
if self.base_url and self.api_token:
161+
url = self.build_url(path, query_args)
162+
return self.http.request('GET', url, headers=self.headers)
163+
164+
def put(self, path, query_args=None, payload=''):
165+
if self.base_url and self.api_token:
166+
url = self.build_url(path, query_args)
167+
encoded_data = json.dumps(payload).encode('utf-8')
168+
post_headers = self.headers
169+
post_headers['Content-Type'] = 'application/json'
170+
return self.http.request('PUT', url, body=encoded_data, headers=post_headers)
171+
172+
def post(self, path, query_args=None, payload=''):
173+
if self.base_url and self.api_token:
174+
url = self.build_url(path, query_args)
175+
encoded_data = json.dumps(payload).encode('utf-8')
176+
post_headers = self.headers
177+
post_headers['Content-Type'] = 'application/json'
178+
return self.http.request('POST', url, body=encoded_data, headers=post_headers)
179+
180+
def delete(self, path, query_args):
181+
if self.base_url and self.api_token:
182+
url = self.build_url(path, query_args)
183+
return self.http.request('DELETE', url, headers=self.headers)
184+
185+
def tokens(self):
186+
return self.get('/api/apiTokens')
187+
188+
def token(self, token):
189+
return self.get('/api/apiTokens/%s' % token)
190+
191+
def delete_token(self, token):
192+
return self.delete('/api/apiTokens/%s' % token)
193+
194+
def upsert_token(self, token_config):
195+
return self.put('/api/apiTokens/%s' % token_config["id"], payload=token_config)
196+
197+
def audit_log(self):
198+
return self.get('/api/auditlog')
199+
200+
def eum_apps(self):
201+
return self.get('/api/eumApps')
202+
203+
def create_eum_app(self, name):
204+
return self.post('/api/eumApps', payload={'name': name})
205+
206+
def rename_eum_app(self, eum_app_id, new_name):
207+
return self.put('/api/eumApps/%s' % (eum_app_id), payload={'name': new_name})
208+
209+
def delete_eum_app(self, eum_app_id):
210+
return self.delete('/api/eumApps/%s' % eum_app_id)
211+
212+
def events(self, window_size=300000, to=None):
213+
if to is None:
214+
to = self.ts_now()
215+
return self.get('/api/events/', query_args={'windowsize': window_size, 'to': to})
216+
217+
def event(self, event_id):
218+
return self.get('/api/events/%s' % event_id)
219+
220+
def metrics(self, metric_name, ts_from, ts_to, aggregation, snapshot_id, rollup):
221+
params = {'metric': metric_name,
222+
'from': ts_from,
223+
'to': ts_to,
224+
'aggregation': aggregation,
225+
'snapshotId': snapshot_id,
226+
'rollup': rollup}
227+
return self.get('/api/metrics', query_args=params)
228+
229+
def metric(self, metric_name, timestamp, aggregation, snapshot_id, rollup):
230+
params = {'metric': metric_name,
231+
'time': timestamp,
232+
'aggregation': aggregation,
233+
'snapshotId': snapshot_id,
234+
'rollup': rollup}
235+
return self.get('/api/metric', query_args=params)
236+
237+
def rule_bindings(self):
238+
return self.get('/api/ruleBindings')
239+
240+
def rule_binding(self, rule_binding_id):
241+
return self.get('/api/ruleBindings/%s' % rule_binding_id)
242+
243+
def upsert_rule_binding(self, rule_binding_config):
244+
path = '/api/ruleBindings/%s' % rule_binding_config["id"]
245+
return self.put(path, rule_binding_config)
246+
247+
def delete_rule_binding(self, rule_binding_id):
248+
return self.detel('/api/ruleBindings/%s' % rule_binding_id)
249+
250+
def rules(self):
251+
return self.get('/api/rules')
252+
253+
def rule(self, rule_id):
254+
return self.get('/api/rules/%s' % rule_id)
255+
256+
def upsert_rule(self, rule_config):
257+
path = '/api/rules/%s' % rule_config["id"]
258+
return self.put(path, rule_config)
259+
260+
def delete_rule(self, rule_id):
261+
return self.delete('/api/rules/%s' % rule_id)
262+
263+
def search_fields(self):
264+
return self.get('/api/searchFields')
265+
266+
def service_extraction_configs(self):
267+
return self.get('/api/serviceExtractionConfigs')
268+
269+
def upsert_service_extraction_configs(self, service_extraction_config):
270+
path = '/api/serviceExtractionConfigs/%s' % service_extraction_config["id"]
271+
return self.put(path, service_extraction_config)
272+
273+
def snapshot(self, id, timestamp=None):
274+
if timestamp is None:
275+
timestamp = self.ts_now()
276+
277+
params = {'time': timestamp}
278+
path = "/api/snapshots/%s" % id
279+
return self.get(path, query_args=params)
280+
281+
def snapshots(self, query, timestamp=None, size=5):
282+
if timestamp is None:
283+
timestamp = self.ts_now()
284+
285+
params = {'time': timestamp, 'q': query, 'size': size}
286+
path = "/api/snapshots"
287+
return self.get(path, query_args=params)
288+
289+
def trace(self, trace_id):
290+
return self.get('/api/traces/%d' % trace_id)
291+
292+
def traces_by_timeframe(self, query, window_size, ts_to, sort_by='ts', sort_mode='asc'):
293+
params = {'windowsize': window_size,
294+
'to': ts_to,
295+
'sortBy': sort_by,
296+
'sortMode': sort_mode,
297+
'query': query}
298+
return self.get('/api/traces', query_args=params)
299+
300+
def roles(self):
301+
return self.get('/api/roles')
302+
303+
def role(self, role_id):
304+
return self.get('/api/roles/%s' % role_id)
305+
306+
def upsert_role(self, role_config):
307+
path = '/api/roles/%s' % role_config["id"]
308+
return self.put(path, payload=role_config)
309+
310+
def delete_role(self, role_id):
311+
return self.delete('/api/roles/%s' % role_id)
312+
313+
def users(self):
314+
return self.get('/api/tenant/users/overview')
315+
316+
def set_user_role(self, user_id, role_id):
317+
return self.put('/api/tenant/users/%s/role' % user_id,
318+
query_args={'roleId': role_id})
319+
320+
def remove_user_from_tenant(self, user_id):
321+
return self.delete('/api/tenant/users/%s' % user_id)
322+
323+
def invite_user(self, email, role_id):
324+
return self.post('/api/tenant/users/invitations',
325+
query_args={'email', email, 'roleId', role_id})
326+
327+
def revoke_pending_invitation(self, email):
328+
return self.delete('/api/tenant/users/invitations',
329+
query_args={'email': email})
330+
331+
def application_view(self):
332+
return self.get('/api/graph/views/application')
333+
334+
def infrastructure_view(self):
335+
return self.get('/api/graph/views/infrastructure')
336+
337+
def usage(self):
338+
return self.get('/api/usage/')
339+
340+
def usage_for_month(self, year, month):
341+
return self.get('/api/usage/%d/%d' % (month, year))
342+
343+
def usage_for_day(self, year, month, day):
344+
return self.get('/api/usage/%d/%d/%d' % (day, month, year))
345+
346+
def average_number_of_hosts_for_month(self, year, month):
347+
return self.get('/api/usage/hosts/%d/%d' % (month, year))
348+
349+
def average_number_of_hosts_for_day(self, year, month, day):
350+
return self.get('/api/usage/hosts/%d/%d/%d' % (month, year, day))

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
'nose>=1.0',
2828
'flask>=0.12.2',
2929
'requests>=2.17.1',
30+
'urllib3[secure]>=1.15',
3031
'spyne>=2.9',
3132
'lxml>=3.4',
3233
'suds-jurko>=0.6'

0 commit comments

Comments
 (0)