diff --git a/py-json/LANforge/LFRequest.py b/py-json/LANforge/LFRequest.py index 981453541..f2f88b88b 100644 --- a/py-json/LANforge/LFRequest.py +++ b/py-json/LANforge/LFRequest.py @@ -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 @@ -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: --------------------------------------------") @@ -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) @@ -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) @@ -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() @@ -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_)) @@ -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: --------------------------------------------") @@ -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 diff --git a/py-json/LANforge/lfcli_base.py b/py-json/LANforge/lfcli_base.py index 34d4085a8..da1b3565a 100644 --- a/py-json/LANforge/lfcli_base.py +++ b/py-json/LANforge/lfcli_base.py @@ -14,6 +14,7 @@ import re import logging import math +import json if sys.version_info[0] != 3: print("This script requires Python 3") @@ -50,7 +51,9 @@ 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: " @@ -58,6 +61,18 @@ def __init__(self, _lfjson_host, _lfjson_port, 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() + 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 = {} @@ -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)) + 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 @@ -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, @@ -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): @@ -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, @@ -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): @@ -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, @@ -334,15 +405,22 @@ 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): @@ -350,6 +428,8 @@ def json_delete(self, _req_url, debug_=False): 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, @@ -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 diff --git a/py-json/realm.py b/py-json/realm.py index c52f79f8f..ed8abd60d 100755 --- a/py-json/realm.py +++ b/py-json/realm.py @@ -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 = [] diff --git a/py-scripts/lf_webpage.py b/py-scripts/lf_webpage.py index f6acb2c86..105b00f6a 100755 --- a/py-scripts/lf_webpage.py +++ b/py-scripts/lf_webpage.py @@ -154,7 +154,8 @@ def __init__(self, lfclient_host, lfclient_port, upstream, num_sta, security, ss device_list=None, get_url_from_file=None, file_path=None, device_csv_name='', expected_passfail_value=None, file_name=None, group_name=None, profile_name=None, eap_method=None, eap_identity=None, ieee80211=None, ieee80211u=None, ieee80211w=None, enable_pkc=None, bss_transition=None, power_save=None, disable_ofdma=None, roam_ft_ds=None, key_management=None, pairwise=None, private_key=None, ca_cert=None, client_cert=None, pk_passwd=None, pac_file=None, config=False, wait_time=60, get_live_view=False, total_floors=0, robot_test=False, - robot_ip=None, coordinate=None, rotation=None, duration=None, do_bandsteering=False, cycles=None, bssids=None, duration_to_skip=None): + robot_ip=None, coordinate=None, rotation=None, duration=None, do_bandsteering=False, cycles=None, bssids=None, duration_to_skip=None, + _save_api=False, _api_log_file_name=None): # super().__init__(lfclient_host=lfclient_host, # lfclient_port=lfclient_port) self.ssid_list = [] @@ -190,7 +191,8 @@ def __init__(self, lfclient_host, lfclient_port, upstream, num_sta, security, ss self.ap_name = ap_name self.windows_ports = [] self.windows_eids = [] - self.local_realm = realm.Realm(lfclient_host=self.host, lfclient_port=self.port) + self.local_realm = realm.Realm(lfclient_host=self.host, lfclient_port=self.port, + _save_api=_save_api, _api_log_file_name=_api_log_file_name) self.station_profile = self.local_realm.new_station_profile() self.http_profile = self.local_realm.new_http_profile() self.http_profile.requests_per_ten = self.target_per_ten @@ -1515,6 +1517,12 @@ def generate_report(self, date, num_stations, duration, test_setup_info, dataset # To store http_datavalues.csv in report folder report_path_date_time = report.get_path_date_time() + # Copy the api log file (json_get/post/put/delete calls) into the report folder, if enabled + if getattr(self.local_realm, 'save_api', False): + try: + shutil.copy(self.local_realm.api_log_filename, report_path_date_time) + except Exception: + logging.info("failed to copy api log file %s to report dir" % self.local_realm.api_log_filename) # It ensures no blocker for virtual clients if self.client_type == 'Real': shutil.move('http_datavalues.csv', report_path_date_time) @@ -2696,6 +2704,10 @@ def main(): optional.add_argument("--test_priority", default="", help="dut model for kpi.csv, test-priority is arbitrary number") optional.add_argument("--test_id", default="lf_webpage", help="test-id for kpi.csv, script or test name") optional.add_argument('--csv_outfile', help="--csv_outfile ", default="") + optional.add_argument('--save_api', help="save json_get/json_post/json_put/json_delete calls to a lightweight log file", + action="store_true", default=False) + optional.add_argument('--api_log_file_name', help="path to the api log file used when --save_api is set (default: ~/lf_api_calls.log)", + default=None) # ARGS for webGUI required.add_argument('--dowebgui', help="If true will execute script for webgui", default=False) # FOR WEBGUI optional.add_argument('--result_dir', @@ -2882,6 +2894,7 @@ def main(): ssid = [args.twog_ssid, args.fiveg_ssid] passwd = [args.twog_passwd, args.fiveg_passwd] http = HttpDownload(lfclient_host=args.mgr, lfclient_port=args.mgr_port, + _save_api=args.save_api, _api_log_file_name=args.api_log_file_name, upstream=args.upstream_port, num_sta=args.num_stations, security=security, ap_name=args.ap_name, ssid=ssid, password=passwd,