Skip to content

Commit 14eaebc

Browse files
doing some transport refactoring
1 parent 4ec8756 commit 14eaebc

File tree

3 files changed

+112
-35
lines changed

3 files changed

+112
-35
lines changed

SoftLayer/CLI/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,3 @@
1010

1111
from SoftLayer.CLI.helpers import * # NOQA
1212

13-
logger = logging.getLogger()
14-
logger.addHandler(logging.StreamHandler())
15-
logger.setLevel(logging.INFO)

SoftLayer/CLI/core.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,7 @@ def get_command(self, ctx, name):
8888
help="Config file location",
8989
type=click.Path(resolve_path=True))
9090
@click.option('--verbose', '-v',
91-
help="Sets the debug noise level, specify multiple times "
92-
"for more verbosity.",
91+
help="Sets the debug noise level, specify multiple times for more verbosity.",
9392
type=click.IntRange(0, 3, clamp=True),
9493
count=True)
9594
@click.option('--proxy',
@@ -115,10 +114,9 @@ def cli(env,
115114
**kwargs):
116115
"""Main click CLI entry-point."""
117116

118-
if verbose > 0:
119-
logger = logging.getLogger()
120-
logger.addHandler(logging.StreamHandler())
121-
logger.setLevel(DEBUG_LOGGING_MAP.get(verbose, logging.DEBUG))
117+
logger = logging.getLogger()
118+
logger.addHandler(logging.StreamHandler())
119+
logger.setLevel(DEBUG_LOGGING_MAP.get(verbose, logging.DEBUG))
122120

123121
# Populate environement with client and set it as the context object
124122
env.skip_confirmations = really
@@ -127,7 +125,7 @@ def cli(env,
127125
env.ensure_client(config_file=config, is_demo=demo, proxy=proxy)
128126

129127
env.vars['_start'] = time.time()
130-
env.vars['_timings'] = SoftLayer.TimingTransport(env.client.transport)
128+
env.vars['_timings'] = SoftLayer.DebugTransport(env.client.transport)
131129
env.client.transport = env.vars['_timings']
132130

133131

@@ -138,19 +136,16 @@ def output_diagnostics(env, verbose=0, **kwargs):
138136

139137
if verbose > 0:
140138
diagnostic_table = formatting.Table(['name', 'value'])
141-
diagnostic_table.add_row(['execution_time',
142-
'%fs' % (time.time() - START_TIME)])
139+
diagnostic_table.add_row(['execution_time', '%fs' % (time.time() - START_TIME)])
143140

144141
api_call_value = []
145-
for call, _, duration in env.vars['_timings'].get_last_calls():
146-
api_call_value.append(
147-
"%s::%s (%fs)" % (call.service, call.method, duration))
142+
for call in env.client.transport.get_last_calls():
143+
api_call_value.append("%s::%s (%fs)" % (call.service, call.method, call.end_time - call.start_time))
148144

149145
diagnostic_table.add_row(['api_calls', api_call_value])
150146
diagnostic_table.add_row(['version', consts.USER_AGENT])
151147
diagnostic_table.add_row(['python_version', sys.version])
152-
diagnostic_table.add_row(['library_location',
153-
os.path.dirname(SoftLayer.__file__)])
148+
diagnostic_table.add_row(['library_location', os.path.dirname(SoftLayer.__file__)])
154149

155150
env.err(env.fmt(diagnostic_table))
156151

@@ -163,8 +158,7 @@ def main(reraise_exceptions=False, **kwargs):
163158
cli.main(**kwargs)
164159
except SoftLayer.SoftLayerAPIError as ex:
165160
if 'invalid api token' in ex.faultString.lower():
166-
print("Authentication Failed: To update your credentials,"
167-
" use 'slcli config setup'")
161+
print("Authentication Failed: To update your credentials, use 'slcli config setup'")
168162
exit_status = 1
169163
else:
170164
print(str(ex))
@@ -184,6 +178,7 @@ def main(reraise_exceptions=False, **kwargs):
184178
print(str(traceback.format_exc()))
185179
print("Feel free to report this error as it is likely a bug:")
186180
print(" https://github.com/softlayer/softlayer-python/issues")
181+
print("The following snippet should be able to reproduce the error")
187182
exit_status = 1
188183

189184
sys.exit(exit_status)

SoftLayer/transports.py

Lines changed: 101 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import importlib
99
import json
1010
import logging
11+
import re
1112
import time
1213

1314
import requests
@@ -27,6 +28,7 @@
2728
'XmlRpcTransport',
2829
'RestTransport',
2930
'TimingTransport',
31+
'DebugTransport',
3032
'FixtureTransport',
3133
'SoftLayerListResult',
3234
]
@@ -100,6 +102,24 @@ def __init__(self):
100102
#: Integer result offset.
101103
self.offset = None
102104

105+
#: Integer call start time
106+
self.start_time = None
107+
108+
#: Integer call end time
109+
self.end_time = None
110+
111+
#: String full url
112+
self.url = None
113+
114+
#: String result of api call
115+
self.result = None
116+
117+
#: String payload to send in
118+
self.payload = None
119+
120+
#: Exception any exceptions that got caught
121+
self.exception = None
122+
103123

104124
class SoftLayerListResult(list):
105125
"""A SoftLayer API list result."""
@@ -117,8 +137,7 @@ class XmlRpcTransport(object):
117137
"""XML-RPC transport."""
118138
def __init__(self, endpoint_url=None, timeout=None, proxy=None, user_agent=None, verify=True):
119139

120-
self.endpoint_url = (endpoint_url or
121-
consts.API_PUBLIC_ENDPOINT).rstrip('/')
140+
self.endpoint_url = (endpoint_url or consts.API_PUBLIC_ENDPOINT).rstrip('/')
122141
self.timeout = timeout or None
123142
self.proxy = proxy
124143
self.user_agent = user_agent or consts.USER_AGENT
@@ -139,7 +158,6 @@ def __call__(self, request):
139158
:param request request: Request object
140159
"""
141160
largs = list(request.args)
142-
143161
headers = request.headers
144162

145163
if request.identifier is not None:
@@ -163,32 +181,26 @@ def __call__(self, request):
163181
request.transport_headers.setdefault('Content-Type', 'application/xml')
164182
request.transport_headers.setdefault('User-Agent', self.user_agent)
165183

166-
url = '/'.join([self.endpoint_url, request.service])
167-
payload = utils.xmlrpc_client.dumps(tuple(largs),
184+
request.url = '/'.join([self.endpoint_url, request.service])
185+
request.payload = utils.xmlrpc_client.dumps(tuple(largs),
168186
methodname=request.method,
169187
allow_none=True)
170188

171189
# Prefer the request setting, if it's not None
172190
verify = request.verify
173191
if verify is None:
174-
verify = self.verify
192+
request.verify = self.verify
175193

176-
LOGGER.debug("=== REQUEST ===")
177-
LOGGER.debug('POST %s', url)
178-
LOGGER.debug(request.transport_headers)
179-
LOGGER.debug(payload)
180194

181195
try:
182-
resp = self.client.request('POST', url,
183-
data=payload,
196+
resp = self.client.request('POST', request.url,
197+
data=request.payload,
184198
headers=request.transport_headers,
185199
timeout=self.timeout,
186-
verify=verify,
200+
verify=request.verify,
187201
cert=request.cert,
188202
proxies=_proxies_dict(self.proxy))
189-
LOGGER.debug("=== RESPONSE ===")
190-
LOGGER.debug(resp.headers)
191-
LOGGER.debug(resp.content)
203+
192204
resp.raise_for_status()
193205
result = utils.xmlrpc_client.loads(resp.content)[0][0]
194206
if isinstance(result, list):
@@ -218,6 +230,42 @@ def __call__(self, request):
218230
except requests.RequestException as ex:
219231
raise exceptions.TransportError(0, str(ex))
220232

233+
def print_reproduceable(self, request):
234+
"""Prints out the minimal python code to reproduce a specific request
235+
236+
The will also automatically replace the API key so its not accidently exposed.
237+
238+
:param request request: Request object
239+
"""
240+
from string import Template
241+
output = Template('''============= testing.py =============
242+
import requests
243+
from requests.adapters import HTTPAdapter
244+
from urllib3.util.retry import Retry
245+
from xml.etree import ElementTree
246+
client = requests.Session()
247+
client.headers.update({'Content-Type': 'application/json', 'User-Agent': 'softlayer-python/testing',})
248+
retry = Retry(connect=3, backoff_factor=3)
249+
adapter = HTTPAdapter(max_retries=retry)
250+
client.mount('https://', adapter)
251+
url = '$url'
252+
payload = """$payload"""
253+
transport_headers = $transport_headers
254+
timeout = $timeout
255+
verify = $verify
256+
cert = $cert
257+
proxy = $proxy
258+
response = client.request('POST', url, data=payload, headers=transport_headers, timeout=timeout,
259+
verify=verify, cert=cert, proxies=proxy)
260+
xml = ElementTree.fromstring(response.content)
261+
ElementTree.dump(xml)
262+
==========================''')
263+
264+
safe_payload = re.sub(r'<string>[a-z0-9]{64}</string>', r'<string>API_KEY_GOES_HERE</string>', request.payload)
265+
substitutions = dict(url=request.url, payload=safe_payload, transport_headers=request.transport_headers,
266+
timeout=self.timeout, verify=request.verify, cert=request.cert, proxy=_proxies_dict(self.proxy))
267+
return output.substitute(substitutions)
268+
221269

222270
class RestTransport(object):
223271
"""REST transport.
@@ -334,6 +382,43 @@ def __call__(self, request):
334382
raise exceptions.TransportError(0, str(ex))
335383

336384

385+
class DebugTransport(object):
386+
"""Transport that records API call timings."""
387+
388+
def __init__(self, transport):
389+
self.transport = transport
390+
391+
#: List All API calls made during a session
392+
self.requests = []
393+
394+
def __call__(self, call):
395+
call.start_time = time.time()
396+
397+
self.pre_transport_log(call)
398+
try:
399+
call.result = self.transport(call)
400+
except (exceptions.SoftLayerAPIError, exceptions.TransportError) as ex:
401+
call.exception = ex
402+
403+
self.post_transport_log(call)
404+
405+
call.end_time = time.time()
406+
self.requests.append(call)
407+
408+
if call.exception is not None:
409+
raise call.exception
410+
411+
return call.result
412+
413+
def pre_transport_log(self, call):
414+
LOGGER.warning("Calling: {}::{}(id={})".format(call.service, call.method, call.identifier))
415+
416+
def post_transport_log(self, call):
417+
LOGGER.debug(self.transport.print_reproduceable(call))
418+
419+
def get_last_calls(self):
420+
return self.requests
421+
337422
class TimingTransport(object):
338423
"""Transport that records API call timings."""
339424

0 commit comments

Comments
 (0)