1#!/usr/bin/python2 2# Copyright 2015 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6from __future__ import print_function 7 8import argparse 9import os 10import re 11 12import chaos_capture_analyzer 13import chaos_log_analyzer 14 15class ChaosTestInfo(object): 16 """ Class to gather the relevant test information from a folder. """ 17 18 MESSAGES_FILE_NAME = "messages" 19 NET_LOG_FILE_NAME = "net.log" 20 TEST_DEBUG_LOG_FILE_END = "DEBUG" 21 SYSINFO_FOLDER_NAME_END = "sysinfo" 22 TEST_DEBUG_FOLDER_NAME_END = "debug" 23 24 def __init__(self, dir_name, file_names, failures_only): 25 """ 26 Gathers all the relevant Chaos test results from a given folder. 27 28 @param dir: Folder to check for test results. 29 @param files: Files present in the folder found during os.walk. 30 @param failures_only: Flag to indicate whether to analyze only 31 failure test attempts. 32 33 """ 34 self._meta_info = None 35 self._traces = [] 36 self._message_log = None 37 self._net_log = None 38 self._test_debug_log = None 39 for file_name in file_names: 40 if file_name.endswith('.trc'): 41 basename = os.path.basename(file_name) 42 if 'success' in basename and failures_only: 43 continue 44 self._traces.append(os.path.join(dir_name, file_name)) 45 if self._traces: 46 for root, dir_name, file_names in os.walk(dir_name): 47 # Now get the log files from the sysinfo, debug folder 48 if root.endswith(self.SYSINFO_FOLDER_NAME_END): 49 # There are multiple copies of |messages| file under 50 # sysinfo tree. We only want the one directly in sysinfo. 51 for file_name in file_names: 52 if file_name == self.MESSAGES_FILE_NAME: 53 self._message_log = os.path.join(root, file_name) 54 for root, dir_name, file_names in os.walk(root): 55 for file_name in file_names: 56 if file_name == self.NET_LOG_FILE_NAME: 57 self._net_log = os.path.join(root, file_name) 58 if root.endswith(self.TEST_DEBUG_FOLDER_NAME_END): 59 for root, dir_name, file_names in os.walk(root): 60 for file_name in file_names: 61 if file_name.endswith(self.TEST_DEBUG_LOG_FILE_END): 62 self._test_debug_log = ( 63 os.path.join(root, file_name)) 64 self._parse_meta_info( 65 os.path.join(root, file_name)) 66 67 def _parse_meta_info(self, file): 68 dut_mac_prefix ='\'DUT\': ' 69 ap_bssid_prefix ='\'AP Info\': ' 70 ap_ssid_prefix ='\'SSID\': ' 71 self._meta_info = {} 72 with open(file) as infile: 73 for line in infile.readlines(): 74 line = line.strip() 75 if line.startswith(dut_mac_prefix): 76 dut_mac = line[len(dut_mac_prefix):].rstrip() 77 self._meta_info['dut_mac'] = ( 78 dut_mac.replace('\'', '').replace(',', '')) 79 if line.startswith(ap_ssid_prefix): 80 ap_ssid = line[len(ap_ssid_prefix):].rstrip() 81 self._meta_info['ap_ssid'] = ( 82 ap_ssid.replace('\'', '').replace(',', '')) 83 if line.startswith(ap_bssid_prefix): 84 debug_info = self._parse_debug_info(line) 85 if debug_info: 86 self._meta_info.update(debug_info) 87 88 def _parse_debug_info(self, line): 89 # Example output: 90 #'AP Info': "{'2.4 GHz MAC Address': '84:1b:5e:e9:74:ee', \n 91 #'5 GHz MAC Address': '84:1b:5e:e9:74:ed', \n 92 #'Controller class': 'Netgear3400APConfigurator', \n 93 #'Hostname': 'chromeos3-row2-rack2-host12', \n 94 #'Router name': 'wndr 3700 v3'}", 95 debug_info = line.replace('\'', '') 96 address_label = 'Address: ' 97 bssids = [] 98 for part in debug_info.split(','): 99 address_index = part.find(address_label) 100 if address_index >= 0: 101 address = part[(address_index+len(address_label)):] 102 if address != 'N/A': 103 bssids.append(address) 104 if not bssids: 105 return None 106 return { 'ap_bssids': bssids } 107 108 def _is_meta_info_valid(self): 109 return ((self._meta_info is not None) and 110 ('dut_mac' in self._meta_info) and 111 ('ap_ssid' in self._meta_info) and 112 ('ap_bssids' in self._meta_info)) 113 114 @property 115 def traces(self): 116 """Returns the trace files path in test info.""" 117 return self._traces 118 119 @property 120 def message_log(self): 121 """Returns the message log path in test info.""" 122 return self._message_log 123 124 @property 125 def net_log(self): 126 """Returns the net log path in test info.""" 127 return self._net_log 128 129 @property 130 def test_debug_log(self): 131 """Returns the test debug log path in test info.""" 132 return self._test_debug_log 133 134 @property 135 def bssids(self): 136 """Returns the BSSID of the AP in test info.""" 137 return self._meta_info['ap_bssids'] 138 139 @property 140 def ssid(self): 141 """Returns the SSID of the AP in test info.""" 142 return self._meta_info['ap_ssid'] 143 144 @property 145 def dut_mac(self): 146 """Returns the MAC of the DUT in test info.""" 147 return self._meta_info['dut_mac'] 148 149 def is_valid(self, packet_capture_only): 150 """ 151 Checks if the given folder contains a valid Chaos test results. 152 153 @param packet_capture_only: Flag to indicate whether to analyze only 154 packet captures. 155 156 @return True if valid chaos results are found; False otherwise. 157 158 """ 159 if packet_capture_only: 160 return ((self._is_meta_info_valid()) and 161 (bool(self._traces))) 162 else: 163 return ((self._is_meta_info_valid()) and 164 (bool(self._traces)) and 165 (bool(self._message_log)) and 166 (bool(self._net_log))) 167 168 169class ChaosLogger(object): 170 """ Class to log the analysis to the given output file. """ 171 172 LOG_SECTION_DEMARKER = "--------------------------------------" 173 174 def __init__(self, output): 175 self._output = output 176 177 def log_to_output_file(self, log_msg): 178 """ 179 Logs the provided string to the output file. 180 181 @param log_msg: String to print to the output file. 182 183 """ 184 self._output.write(log_msg + "\n") 185 186 def log_start_section(self, section_description): 187 """ 188 Starts a new section in the output file with demarkers. 189 190 @param log_msg: String to print in section description. 191 192 """ 193 self.log_to_output_file(self.LOG_SECTION_DEMARKER) 194 self.log_to_output_file(section_description) 195 self.log_to_output_file(self.LOG_SECTION_DEMARKER) 196 197 198class ChaosAnalyzer(object): 199 """ Main Class to analyze the chaos test output from a given folder. """ 200 201 LOG_OUTPUT_FILE_NAME_FORMAT = "chaos_analyzer_try_%s.log" 202 TRACE_FILE_ATTEMPT_NUM_RE = r'\d+' 203 204 def _get_attempt_number_from_trace(self, trace): 205 file_name = os.path.basename(trace) 206 return re.search(self.TRACE_FILE_ATTEMPT_NUM_RE, file_name).group(0) 207 208 def _get_all_test_infos(self, dir_name, failures_only, packet_capture_only): 209 test_infos = [] 210 for root, dir, files in os.walk(dir_name): 211 test_info = ChaosTestInfo(root, files, failures_only) 212 if test_info.is_valid(packet_capture_only): 213 test_infos.append(test_info) 214 if not test_infos: 215 print("Did not find any valid test info!") 216 return test_infos 217 218 def analyze(self, input_dir_name=None, output_dir_name=None, 219 failures_only=False, packet_capture_only=False): 220 """ 221 Starts the analysis of the Chaos test logs and packet capture. 222 223 @param input_dir_name: Directory which contains the chaos test results. 224 @param output_dir_name: Directory to which the chaos analysis is output. 225 @param failures_only: Flag to indicate whether to analyze only 226 failure test attempts. 227 @param packet_capture_only: Flag to indicate whether to analyze only 228 packet captures. 229 230 """ 231 for test_info in self._get_all_test_infos(input_dir_name, failures_only, 232 packet_capture_only): 233 for trace in test_info.traces: 234 attempt_num = self._get_attempt_number_from_trace(trace) 235 trace_dir_name = os.path.dirname(trace) 236 print("Analyzing attempt number: " + attempt_num + \ 237 " from folder: " + os.path.abspath(trace_dir_name)) 238 # Store the analysis output in the respective log folder 239 # itself unless there is an explicit output directory 240 # specified in which case we prepend the |testname_| to the 241 # output analysis file name. 242 output_file_name = ( 243 self.LOG_OUTPUT_FILE_NAME_FORMAT % (attempt_num)) 244 if not output_dir_name: 245 output_dir = trace_dir_name 246 else: 247 output_dir = output_dir_name 248 output_file_name = "_".join([trace_dir_name, 249 output_file_name]) 250 output_file_path = ( 251 os.path.join(output_dir, output_file_name)) 252 try: 253 with open(output_file_path, "w") as output_file: 254 logger = ChaosLogger(output_file) 255 protocol_analyzer = ( 256 chaos_capture_analyzer.ChaosCaptureAnalyzer( 257 test_info.bssids, test_info.ssid, 258 test_info.dut_mac, logger)) 259 protocol_analyzer.analyze(trace) 260 if not packet_capture_only: 261 with open(test_info.message_log, "r") as message_log, \ 262 open(test_info.net_log, "r") as net_log: 263 log_analyzer = ( 264 chaos_log_analyzer.ChaosLogAnalyzer( 265 message_log, net_log, logger)) 266 log_analyzer.analyze(attempt_num) 267 except IOError as e: 268 print('Operation failed: %s!' % e.strerror) 269 270 271def main(): 272 # By default the script parses all the logs places under the current 273 # directory and places the analyzed output for each set of logs in their own 274 # respective directories. 275 parser = argparse.ArgumentParser(description='Analyze Chaos logs.') 276 parser.add_argument('-f', '--failures-only', action='store_true', 277 help='analyze only failure logs.') 278 parser.add_argument('-p', '--packet-capture-only', action='store_true', 279 help='analyze only packet captures.') 280 parser.add_argument('-i', '--input-dir', action='store', default='.', 281 help='process the logs from directory.') 282 parser.add_argument('-o', '--output-dir', action='store', 283 help='output the analysis to directory.') 284 args = parser.parse_args() 285 chaos_analyzer = ChaosAnalyzer() 286 chaos_analyzer.analyze(input_dir_name=args.input_dir, 287 output_dir_name=args.output_dir, 288 failures_only=args.failures_only, 289 packet_capture_only=args.packet_capture_only) 290 291if __name__ == "__main__": 292 main() 293 294