Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 42 additions & 23 deletions py-json/LANforge/LFRequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ def __init__(self, url=None,
self.debug = debug_
self.die_on_error = die_on_error_
self.error_list = []
self.last_response_code = None
self.last_diagnostics = None

# please see this discussion on ProxyHandlers:
# https://docs.python.org/3/library/urllib.request.html#urllib.request.ProxyHandler
Expand Down Expand Up @@ -185,6 +187,7 @@ def json_post(self, show_error=True, debug=False, die_on_error_=False, response_

try:
resp = urllib.request.urlopen(myrequest)
self.last_response_code = getattr(resp, 'status', None)
resp_data = resp.read().decode('utf-8')
if debug or die_on_error_:
self.logger.debug("----- LFRequest::json_post:128 debug: --------------------------------------------")
Expand All @@ -208,18 +211,19 @@ def json_post(self, show_error=True, debug=False, die_on_error_=False, response_
return responses[0]

except urllib.error.HTTPError as error:
print_diagnostics(url_=self.requested_url,
request_=myrequest,
responses_=responses,
error_=error,
debug_=debug)
self.last_response_code = error.code
self.last_diagnostics = print_diagnostics(url_=self.requested_url,
request_=myrequest,
responses_=responses,
error_=error,
debug_=debug)

except urllib.error.URLError as uerror:
print_diagnostics(url_=self.requested_url,
request_=myrequest,
responses_=responses,
error_=uerror,
debug_=debug)
self.last_diagnostics = print_diagnostics(url_=self.requested_url,
request_=myrequest,
responses_=responses,
error_=uerror,
debug_=debug)

if die_on_error_:
exit(1)
Expand Down Expand Up @@ -254,20 +258,21 @@ def get(self, method_='GET'):
return myresponses[0]

except urllib.error.HTTPError as error:
print_diagnostics(url_=self.requested_url,
request_=myrequest,
responses_=myresponses,
error_=error,
error_list_=self.error_list,
debug_=self.debug)
self.last_response_code = error.code
self.last_diagnostics = print_diagnostics(url_=self.requested_url,
request_=myrequest,
responses_=myresponses,
error_=error,
error_list_=self.error_list,
debug_=self.debug)

except urllib.error.URLError as uerror:
print_diagnostics(url_=self.requested_url,
request_=myrequest,
responses_=myresponses,
error_=uerror,
error_list_=self.error_list,
debug_=self.debug)
self.last_diagnostics = print_diagnostics(url_=self.requested_url,
request_=myrequest,
responses_=myresponses,
error_=uerror,
error_list_=self.error_list,
debug_=self.debug)

if self.die_on_error:
exit(1)
Expand All @@ -279,6 +284,10 @@ def getAsJson(self):
def get_as_json(self, method_='GET'):
responses = list()
responses.append(self.get(method_=method_))
# get() already stashes last_response_code from error.code on HTTPError;
# only overwrite it here when we actually have a response to read a status from
if responses[0] is not None:
self.last_response_code = getattr(responses[0], 'status', None)
if len(responses) < 1:
if self.debug and self.has_errors():
self.print_errors()
Expand Down Expand Up @@ -350,6 +359,10 @@ def plain_get(url_=None, debug_=False, die_on_error_=False, proxies_=None):


def print_diagnostics(url_=None, request_=None, responses_=None, error_=None, error_list_=None, debug_=False):
"""
:return: a short one-line summary of the error (code/reason/X-Error-* headers), for
callers (like LFRequest._log_api_call plumbing) that want it in a lightweight log.
"""
logger = logging.getLogger(__name__)
# logger.error("LFRequest::print_diagnostics: error_.__class__: %s"%error_.__class__)
# logger.error(pformat(error_))
Expand Down Expand Up @@ -395,6 +408,10 @@ def print_diagnostics(url_=None, request_=None, responses_=None, error_=None, er
errors_list.append(" = = = = = = = = = = = = = = = =")
logger.error("\n".join(errors_list))

summary = "%s <%s> HTTP %s: %s" % (method, err_full_url, err_code, err_reason)
if xerrors and err_code != 404:
summary += " | " + "; ".join(xerrors)

if error_.__class__ is urllib.error.HTTPError:
debug_list = []
debug_list.append("\n----- LFRequest: HTTPError: --------------------------------------------")
Expand Down Expand Up @@ -425,11 +442,13 @@ def print_diagnostics(url_=None, request_=None, responses_=None, error_=None, er

debug_list.append("------------------------------------------------------------------------")
logger.debug("\n".join(debug_list))
return
return summary

if error_.__class__ is urllib.error.URLError:
errors_list.append("\n----- LFRequest: URLError: ---------------------------------------------")
errors_list.append("%s <%s> HTTP %s: %s" % (method, err_full_url, err_code, err_reason))
errors_list.append("------------------------------------------------------------------------")
logger.error("\n".join(errors_list))

return summary
# ~LFRequest
89 changes: 88 additions & 1 deletion py-json/LANforge/lfcli_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import re
import logging
import math
import json

if sys.version_info[0] != 3:
print("This script requires Python 3")
Expand Down Expand Up @@ -50,14 +51,28 @@ def __init__(self, _lfjson_host, _lfjson_port,
_exit_on_fail=False,
_local_realm=None,
_proxy_str=None,
_capture_signal_list=None):
_capture_signal_list=None,
_save_api=False,
_api_log_file_name=None):
if _capture_signal_list is None:
_capture_signal_list = []
self.fail_pref = "FAILED: "
self.pass_pref = "PASSED: "
self.lfclient_host = _lfjson_host
self.lfclient_port = _lfjson_port
self.debug = _debug
# when True, json_get/json_post/json_put/json_delete append a lightweight
# record of each call (url, payload, response_code/error) to api_log_filename
self.save_api = _save_api
self.api_log_filename = _api_log_file_name or os.path.join(os.path.expanduser('~'), 'lf_api_calls.log')
if self.save_api:
# truncate so each run starts with a clean log -- otherwise this file grows
# forever across runs and generate_report() would copy the entire history
# (including stale pre-fix entries) into every report folder
try:
open(self.api_log_filename, 'w').close()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop truncating API logs per band

When --save_api is enabled, every LFCliBase/Realm construction truncates the log. lf_webpage.py constructs a new HttpDownload/Realm inside its for bands in args.bands loop but copies http.local_realm.api_log_filename into the report only once after the loop, so the default multi-band run erases earlier band entries when later bands initialize and the report only contains the last band's calls. Reset the file once per test run or append across band objects.

Useful? React with 👍 / 👎.

except Exception as x:
logger.debug("LFCliBase: unable to reset %s: %s" % (self.api_log_filename, x))
# if (_debug):
# logger.debug("LFCliBase._proxy_str: %s" % _proxy_str)
self.proxy = {}
Expand Down Expand Up @@ -216,6 +231,42 @@ def log_set_filename(filename=None):

# - END LOGGING -

def _log_api_call(self, method, url, data=None, response_code=None, error=None, diagnostics=None):
"""
Append a lightweight record of a json_get/json_post/json_put/json_delete call
to api_log_filename. Only writes when self.save_api is True.
Logs the HTTP response code rather than the response body/object, so the log
stays small regardless of how large a given LANforge response is.
:param method: "GET" | "POST" | "PUT" | "DELETE"
:param url: requested url
:param data: payload sent (POST/PUT only)
:param response_code: HTTP status code returned by the call, if any
:param error: exception raised by the call, if any -- marks the entry as ERROR
:param diagnostics: one-line summary from LFRequest.print_diagnostics(), if the
call went through a caught HTTPError/URLError (reason, X-Error-* headers, etc.)
"""
if not self.save_api:
return
if error is not None:
status = "ERROR"
elif response_code is not None:
status = "OK" if 200 <= response_code < 300 else "ERROR"
else:
status = "UNKNOWN"
try:
with open(self.api_log_filename, 'a') as api_log:
api_log.write("%s %s %s [%s]\n" % (datetime.datetime.now().isoformat(), method, url, status))
if data is not None:
api_log.write(" payload: %s\n" % json.dumps(data, default=str))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Redact secrets before writing payloads

When save_api is enabled for code paths that send credentials, this writes raw request payloads to a log file that lf_webpage.py can copy into report artifacts. Existing LANforge payloads use fields such as passwd, password, and key for Wi-Fi/DUT credentials, so the report can leak secrets to anyone who receives it; redact common secret fields or make payload logging explicitly opt-in.

Useful? React with 👍 / 👎.

if error is not None:
api_log.write(" error: %s\n" % error)
if response_code is not None:
api_log.write(" response_code: %s\n" % response_code)
if diagnostics is not None:
api_log.write(" diagnostics: %s\n" % diagnostics)
except Exception as x:
logger.debug("_log_api_call: unable to write %s: %s" % (self.api_log_filename, x))

def json_post(self, _req_url, _data, debug_=False, suppress_related_commands_=None, response_json_list_=None):
"""
send json to the LANforge client
Expand All @@ -227,6 +278,8 @@ def json_post(self, _req_url, _data, debug_=False, suppress_related_commands_=No
:return: http response object
"""
json_response = None
_api_error = None
lf_r = None
debug_ |= self.debug
try:
lf_r = LFRequest.LFRequest(url=self.lfclient_url,
Expand Down Expand Up @@ -264,13 +317,20 @@ def json_post(self, _req_url, _data, debug_=False, suppress_related_commands_=No
if debug_ and (response_json_list_ is not None):
logger.debug(pprint.pformat(response_json_list_))
except Exception as x:
_api_error = x
if debug_ or self.exit_on_error:
logger.debug("json_post posted to %s" % _req_url)
logger.debug(pprint.pformat(_data))
logger.debug("Exception %s:" % x)
logger.debug(traceback.format_exception(Exception, x, x.__traceback__, chain=True))
if self.exit_on_error:
self._log_api_call("POST", _req_url, data=_data,
response_code=getattr(json_response, 'status', None) or getattr(lf_r, 'last_response_code', None),
error=_api_error, diagnostics=getattr(lf_r, 'last_diagnostics', None))
exit(1)
self._log_api_call("POST", _req_url, data=_data,
response_code=getattr(json_response, 'status', None) or getattr(lf_r, 'last_response_code', None),
error=_api_error, diagnostics=getattr(lf_r, 'last_diagnostics', None))
return json_response

def json_put(self, _req_url, _data, debug_=False, response_json_list_=None):
Expand All @@ -286,6 +346,8 @@ def json_put(self, _req_url, _data, debug_=False, response_json_list_=None):
"""
debug_ |= self.debug
json_response = None
_api_error = None
lf_r = None
try:
lf_r = LFRequest.LFRequest(url=self.lfclient_url,
uri=_req_url,
Expand All @@ -302,13 +364,20 @@ def json_put(self, _req_url, _data, debug_=False, response_json_list_=None):
if debug_ and (response_json_list_ is not None):
pprint.pprint(response_json_list_)
except Exception as x:
_api_error = x
if debug_ or self.exit_on_error:
logger.debug("json_put submitted to %s" % _req_url)
logger.debug(pprint.pformat(_data))
logger.debug("Exception %s:" % x)
logger.debug(traceback.format_exception(Exception, x, x.__traceback__, chain=True))
if self.exit_on_error:
self._log_api_call("PUT", _req_url, data=_data,
response_code=getattr(json_response, 'status', None) or getattr(lf_r, 'last_response_code', None),
error=_api_error, diagnostics=getattr(lf_r, 'last_diagnostics', None))
exit(1)
self._log_api_call("PUT", _req_url, data=_data,
response_code=getattr(json_response, 'status', None) or getattr(lf_r, 'last_response_code', None),
error=_api_error, diagnostics=getattr(lf_r, 'last_diagnostics', None))
return json_response

def json_get(self, _req_url, debug_=None):
Expand All @@ -319,6 +388,8 @@ def json_get(self, _req_url, debug_=None):
if debug_ is None:
debug_ = self.debug
json_response = None
_api_error = None
lf_r = None
try:
lf_r = LFRequest.LFRequest(url=self.lfclient_url,
uri=_req_url,
Expand All @@ -334,22 +405,31 @@ def json_get(self, _req_url, debug_=None):
else:
logger.debug("LFCliBase.json_get: no entity/response, check other errors")
time.sleep(10)
self._log_api_call("GET", _req_url, response_code=getattr(lf_r, 'last_response_code', None),
diagnostics=getattr(lf_r, 'last_diagnostics', None))
return None
except ValueError as ve:
_api_error = ve
if debug_ or self.exit_on_error:
logger.debug("jsonGet asked for {_req_url} ".format(_req_url=_req_url))
logger.debug("Exception %s:" % ve)
logger.debug(traceback.format_exception(ValueError, ve, ve.__traceback__, chain=True))
if self.exit_on_error:
self._log_api_call("GET", _req_url, response_code=getattr(lf_r, 'last_response_code', None), error=_api_error,
diagnostics=getattr(lf_r, 'last_diagnostics', None))
sys.exit(1)

self._log_api_call("GET", _req_url, response_code=getattr(lf_r, 'last_response_code', None), error=_api_error,
diagnostics=getattr(lf_r, 'last_diagnostics', None))
return json_response

def json_delete(self, _req_url, debug_=False):
debug_ |= self.debug
if debug_:
logger.debug("DELETE: {_req_url}".format(_req_url=_req_url))
json_response = None
_api_error = None
lf_r = None
try:
# logger.info("----- DELETE ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ")
lf_r = LFRequest.LFRequest(url=self.lfclient_url,
Expand All @@ -362,15 +442,22 @@ def json_delete(self, _req_url, debug_=False):
# logger.debug(debug_printer.pformat(json_response))
if (json_response is None) and debug_:
logger.debug("LFCliBase.json_delete: no entity/response, probabily status 404")
self._log_api_call("DELETE", _req_url, response_code=getattr(lf_r, 'last_response_code', None),
diagnostics=getattr(lf_r, 'last_diagnostics', None))
return None
except ValueError as ve:
_api_error = ve
if debug_ or self.exit_on_error:
logger.debug("json_delete asked for {_req_url}".format(_req_url=_req_url))
logger.debug("Exception %s:" % ve)
logger.debug(traceback.format_exception(ValueError, ve, ve.__traceback__, chain=True))
if self.exit_on_error:
self._log_api_call("DELETE", _req_url, response_code=getattr(lf_r, 'last_response_code', None), error=_api_error,
diagnostics=getattr(lf_r, 'last_diagnostics', None))
sys.exit(1)
# print("----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ")
self._log_api_call("DELETE", _req_url, response_code=getattr(lf_r, 'last_response_code', None), error=_api_error,
diagnostics=getattr(lf_r, 'last_diagnostics', None))
return json_response

@staticmethod
Expand Down
8 changes: 6 additions & 2 deletions py-json/realm.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,18 @@ def __init__(self,
_exit_on_error=False,
_exit_on_fail=False,
_proxy_str=None,
_capture_signal_list=None):
_capture_signal_list=None,
_save_api=False,
_api_log_file_name=None):
super().__init__(_lfjson_host=lfclient_host,
_lfjson_port=lfclient_port,
_debug=debug_,
_exit_on_error=_exit_on_error,
_exit_on_fail=_exit_on_fail,
_proxy_str=_proxy_str,
_capture_signal_list=_capture_signal_list)
_capture_signal_list=_capture_signal_list,
_save_api=_save_api,
_api_log_file_name=_api_log_file_name)

if _capture_signal_list is None:
_capture_signal_list = []
Expand Down
Loading
Loading