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
6import argparse
7import os
8import re
9
10import chaos_capture_analyzer
11import chaos_log_analyzer
12
13class 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
167class 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
196class 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
269def 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
289if __name__ == "__main__":
290    main()
291
292