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