Home | History | Annotate | Download | only in chaos_lib
      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