• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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