1# Copyright 2016 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import logging
6import os
7import time
8import re
9import shutil
10
11import common
12from autotest_lib.client.common_lib import error
13from autotest_lib.client.common_lib.cros.network import ap_constants
14from autotest_lib.client.common_lib.cros.network import iw_runner
15from autotest_lib.server import hosts
16from autotest_lib.server import frontend
17from autotest_lib.server import site_utils
18from autotest_lib.server.cros.ap_configurators import ap_configurator
19from autotest_lib.server.cros.ap_configurators import ap_cartridge
20from autotest_lib.server.cros.ap_configurators import ap_spec as ap_spec_module
21
22
23def allocate_packet_capturer(lock_manager, hostname, prefix):
24    """Allocates a machine to capture packets.
25
26    Locks the allocated machine if the machine was discovered via AFE
27    to prevent tests stomping on each other.
28
29    @param lock_manager HostLockManager object.
30    @param hostname string optional hostname of a packet capture machine.
31    @param prefix string chamber location (ex. chromeos3, chromeos5, chromeos7)
32
33    @return: An SSHHost object representing a locked packet_capture machine.
34    """
35    if hostname is not None:
36        return hosts.SSHHost(hostname)
37
38    afe = frontend.AFE(debug=True,
39                       server=site_utils.get_global_afe_hostname())
40    available_pcaps = afe.get_hosts(label='packet_capture')
41    for pcap in available_pcaps:
42        pcap_prefix = pcap.hostname.split('-')[0]
43        # Ensure the pcap and dut are in the same subnet
44        if pcap_prefix == prefix:
45            if lock_manager.lock([pcap.hostname]):
46                return hosts.SSHHost(pcap.hostname + '.cros')
47            else:
48                logging.info('Unable to lock %s', pcap.hostname)
49                continue
50    raise error.TestError('Unable to lock any pcaps - check in cautotest if '
51                          'pcaps in %s are locked.', prefix)
52
53def allocate_webdriver_instance(lock_manager):
54    """Allocates a machine to capture webdriver instance.
55
56    Locks the allocated machine if the machine was discovered via AFE
57    to prevent tests stomping on each other.
58
59    @param lock_manager HostLockManager object.
60
61    @return An SSHHost object representing a locked webdriver instance.
62    """
63    afe = frontend.AFE(debug=True,
64                       server=site_utils.get_global_afe_hostname())
65    hostname = '%s.cros' % site_utils.lock_host_with_labels(
66        afe, lock_manager, labels=['webdriver'])
67    webdriver_host = hosts.SSHHost(hostname)
68    if webdriver_host is not None:
69        return webdriver_host
70    logging.error("Unable to allocate VM instance")
71    return None
72
73
74def is_VM_running(master, instance):
75    """Check if locked VM is running.
76
77    @param master: chaosvmmaster SSHHost
78    @param instance: locked webdriver instance
79
80    @return True if locked VM is running; False otherwise
81    """
82    hostname = instance.hostname.split('.')[0]
83    logging.debug('Check %s VM status', hostname)
84    list_running_vms_cmd = 'VBoxManage list runningvms'
85    running_vms = master.run(list_running_vms_cmd).stdout
86    return hostname in running_vms
87
88
89def power_on_VM(master, instance):
90    """Power on VM
91
92    @param master: chaosvmmaster SSHHost
93    @param instance: locked webdriver instance
94
95    """
96    hostname = instance.hostname.split('.')[0]
97    logging.debug('Powering on %s VM without GUI', hostname)
98    power_on_cmd = 'VBoxManage startvm %s --type headless' % hostname
99    master.run(power_on_cmd)
100
101
102def power_off_VM(master, instance):
103    """Power off VM
104
105    @param master: chaosvmmaster SSHHost
106    @param instance: locked webdriver instance
107
108    """
109    hostname = instance.hostname.split('.')[0]
110    logging.debug('Powering off %s VM', hostname)
111    power_off_cmd = 'VBoxManage controlvm %s poweroff' % hostname
112    master.run(power_off_cmd)
113
114
115def power_down_aps(aps, broken_pdus=[]):
116     """Powers down a list of aps.
117
118     @param aps: a list of APConfigurator objects.
119     @param broken_pdus: a list of broken PDUs identified.
120     """
121     cartridge = ap_cartridge.APCartridge()
122     for ap in aps:
123         ap.power_down_router()
124         cartridge.push_configurator(ap)
125     cartridge.run_configurators(broken_pdus)
126
127
128def configure_aps(aps, ap_spec, broken_pdus=[]):
129    """Configures a given list of APs.
130
131    @param aps: a list of APConfigurator objects.
132    @param ap_spec: APSpec object corresponding to the AP configuration.
133    @param broken_pdus: a list of broken PDUs identified.
134    """
135    cartridge = ap_cartridge.APCartridge()
136    for ap in aps:
137        ap.set_using_ap_spec(ap_spec)
138        cartridge.push_configurator(ap)
139    cartridge.run_configurators(broken_pdus)
140
141
142def is_dut_healthy(client, ap):
143    """Returns if iw scan is working properly.
144
145    Sometimes iw scan will die, especially on the Atheros chips.
146    This works around that bug.  See crbug.com/358716.
147
148    @param client: a wifi_client for the DUT
149    @param ap: ap_configurator object
150
151    @returns True if the DUT is healthy (iw scan works); False otherwise.
152    """
153    # The SSID doesn't matter, all that needs to be verified is that iw
154    # works.
155    networks = client.iw_runner.wait_for_scan_result(
156            client.wifi_if, ssids=[ap.ssid])
157    if networks == None:
158        return False
159    return True
160
161
162def is_conn_worker_healthy(conn_worker, ap, assoc_params, job):
163    """Returns if the connection worker is working properly.
164
165    From time to time the connection worker will fail to establish a
166    connection to the APs.
167
168    @param conn_worker: conn_worker object
169    @param ap: an ap_configurator object
170    @param assoc_params: the connection association parameters
171    @param job: the Autotest job object
172
173    @returns True if the worker is healthy; False otherwise
174    """
175    if conn_worker is None:
176        return True
177    conn_status = conn_worker.connect_work_client(assoc_params)
178    if not conn_status:
179        job.run_test('network_WiFi_ChaosConfigFailure', ap=ap,
180                     error_string=ap_constants.WORK_CLI_CONNECT_FAIL,
181                     tag=ap.ssid)
182        # Obtain the logs from the worker
183        log_dir_name = str('worker_client_logs_%s' % ap.ssid)
184        log_dir = os.path.join(job.resultdir, log_dir_name)
185        conn_worker.host.collect_logs(
186                '/var/log', log_dir, ignore_errors=True)
187        return False
188    return True
189
190
191def release_ap(ap, batch_locker, broken_pdus=[]):
192    """Powers down and unlocks the given AP.
193
194    @param ap: the APConfigurator under test.
195    @param batch_locker: the batch locker object.
196    @param broken_pdus: a list of broken PDUs identified.
197    """
198    ap.power_down_router()
199    try:
200        ap.apply_settings()
201    except ap_configurator.PduNotResponding as e:
202        if ap.pdu not in broken_pdus:
203            broken_pdus.append(ap.pdu)
204    batch_locker.unlock_one_ap(ap.host_name)
205
206
207def filter_quarantined_and_config_failed_aps(aps, batch_locker, job,
208                                             broken_pdus=[]):
209    """Filter out all PDU quarantined and config failed APs.
210
211    @param aps: the list of ap_configurator objects to filter
212    @param batch_locker: the batch_locker object
213    @param job: an Autotest job object
214    @param broken_pdus: a list of broken PDUs identified.
215
216    @returns a list of ap_configuration objects.
217    """
218    aps_to_remove = list()
219    for ap in aps:
220        failed_ap = False
221        if ap.pdu in broken_pdus:
222            ap.configuration_success = ap_constants.PDU_FAIL
223        if (ap.configuration_success == ap_constants.PDU_FAIL):
224            failed_ap = True
225            error_string = ap_constants.AP_PDU_DOWN
226            tag = ap.host_name + '_PDU'
227        elif (ap.configuration_success == ap_constants.CONFIG_FAIL):
228            failed_ap = True
229            error_string = ap_constants.AP_CONFIG_FAIL
230            tag = ap.host_name
231        if failed_ap:
232            tag += '_' + str(int(round(time.time())))
233            job.run_test('network_WiFi_ChaosConfigFailure',
234                         ap=ap,
235                         error_string=error_string,
236                         tag=tag)
237            aps_to_remove.append(ap)
238            if error_string == ap_constants.AP_CONFIG_FAIL:
239                release_ap(ap, batch_locker, broken_pdus)
240            else:
241                # Cannot use _release_ap, since power_down will fail
242                batch_locker.unlock_one_ap(ap.host_name)
243    return list(set(aps) - set(aps_to_remove))
244
245
246def get_security_from_scan(ap, networks, job):
247    """Returns a list of securities determined from the scan result.
248
249    @param ap: the APConfigurator being testing against.
250    @param networks: List of matching networks returned from scan.
251    @param job: an Autotest job object
252
253    @returns a list of possible securities for the given network.
254    """
255    securities = list()
256    # Sanitize MIXED security setting for both Static and Dynamic
257    # configurators before doing the comparison.
258    security = networks[0].security
259    if (security == iw_runner.SECURITY_MIXED and
260        ap.configurator_type == ap_spec_module.CONFIGURATOR_STATIC):
261        securities = [iw_runner.SECURITY_WPA, iw_runner.SECURITY_WPA2]
262        # We have only seen WPA2 be backwards compatible, and we want
263        # to verify the configurator did the right thing. So we
264        # promote this to WPA2 only.
265    elif (security == iw_runner.SECURITY_MIXED and
266          ap.configurator_type == ap_spec_module.CONFIGURATOR_DYNAMIC):
267        securities = [iw_runner.SECURITY_WPA2]
268    else:
269        securities = [security]
270    return securities
271
272
273def scan_for_networks(ssid, capturer, ap_spec):
274    """Returns a list of matching networks after running iw scan.
275
276    @param ssid: the SSID string to look for in scan.
277    @param capturer: a packet capture device.
278    @param ap_spec: APSpec object corresponding to the AP configuration.
279
280    @returns a list of the matching networks; if no networks are found at
281             all, returns None.
282    """
283    # Setup a managed interface to perform scanning on the
284    # packet capture device.
285    freq = ap_spec_module.FREQUENCY_TABLE[ap_spec.channel]
286    wifi_if = capturer.get_wlanif(freq, 'managed')
287    capturer.host.run('%s link set %s up' % (capturer.cmd_ip, wifi_if))
288    # We have some APs that need a while to come on-line
289    networks = capturer.iw_runner.wait_for_scan_result(
290            wifi_if, ssids=[ssid], timeout_seconds=300)
291    capturer.remove_interface(wifi_if)
292    return networks
293
294
295def return_available_networks(ap, capturer, job, ap_spec):
296    """Returns a list of networks configured as described by an APSpec.
297
298    @param ap: the APConfigurator being testing against.
299    @param capturer: a packet capture device
300    @param job: an Autotest job object.
301    @param ap_spec: APSpec object corresponding to the AP configuration.
302
303    @returns a list of networks returned from _scan_for_networks().
304    """
305    for i in range(2):
306        networks = scan_for_networks(ap.ssid, capturer, ap_spec)
307        if networks is None:
308            return None
309        if len(networks) == 0:
310            # The SSID wasn't even found, abort
311            logging.error('The ssid %s was not found in the scan', ap.ssid)
312            job.run_test('network_WiFi_ChaosConfigFailure', ap=ap,
313                         error_string=ap_constants.AP_SSID_NOTFOUND,
314                         tag=ap.ssid)
315            return list()
316        security = get_security_from_scan(ap, networks, job)
317        if ap_spec.security in security:
318            return networks
319        if i == 0:
320            # The SSID exists but the security is wrong, give the AP time
321            # to possible update it.
322            time.sleep(60)
323    if ap_spec.security not in security:
324        logging.error('%s was the expected security but got %s: %s',
325                      ap_spec.security,
326                      str(security).strip('[]'),
327                      networks)
328        job.run_test('network_WiFi_ChaosConfigFailure',
329                     ap=ap,
330                     error_string=ap_constants.AP_SECURITY_MISMATCH,
331                     tag=ap.ssid)
332        networks = list()
333    return networks
334
335
336def sanitize_client(host):
337    """Clean up logs and reboot the DUT.
338
339    @param host: the cros host object to use for RPC calls.
340    """
341    host.run('rm -rf /var/log')
342    host.reboot()
343
344
345def get_firmware_ver(host):
346    """Get firmware version of DUT from /var/log/messages.
347
348    WiFi firmware version is matched against list of known firmware versions
349    from ToT.
350
351    @param host: the cros host object to use for RPC calls.
352
353    @returns the WiFi firmware version as a string, None if the version
354             cannot be found.
355    """
356    # TODO(rpius): Need to find someway to get this info for Android/Brillo.
357    if host.get_os_type() != 'cros':
358        return None
359
360    # Firmware versions manually aggregated by installing ToT on each device
361    known_firmware_ver = ['Atheros', 'mwifiex', 'loaded firmware version',
362                          'brcmf_c_preinit_dcmds']
363    # Find and return firmware version in logs
364    for firmware_ver in known_firmware_ver:
365        result_str = host.run(
366            'awk "/%s/ {print}" /var/log/messages' % firmware_ver).stdout
367        if not result_str:
368            continue
369        else:
370            if 'Atheros' in result_str:
371                pattern = '%s \w+ Rev:\d' % firmware_ver
372            elif 'mwifiex' in result_str:
373                pattern = '%s [\d.]+ \([\w.]+\)' % firmware_ver
374            elif 'loaded firmware version' in result_str:
375                pattern = '(\d+\.\d+\.\d+)'
376            elif 'Firmware version' in result_str:
377                pattern = '\d+\.\d+\.\d+ \([\w.]+\)'
378            else:
379                logging.info('%s does not match known firmware versions.',
380                             result_str)
381                return None
382            result = re.search(pattern, result_str)
383            if result:
384                return result.group(0)
385    return None
386
387
388def collect_pcap_info(tracedir, pcap_filename, try_count):
389        """Gather .trc and .trc.log files into android debug directory.
390
391        @param tracedir: string name of the directory that has the trace files.
392        @param pcap_filename: string name of the pcap file.
393        @param try_count: int Connection attempt number.
394
395        """
396        pcap_file = os.path.join(tracedir, pcap_filename)
397        pcap_log_file = os.path.join(tracedir, '%s.log' % pcap_filename)
398        debug_dir = 'android_debug_try_%d' % try_count
399        debug_dir_path = os.path.join(tracedir, 'debug/%s' % debug_dir)
400        if os.path.exists(debug_dir_path):
401            pcap_dir_path = os.path.join(debug_dir_path, 'pcap')
402            if not os.path.exists(pcap_dir_path):
403                os.makedirs(pcap_dir_path)
404                shutil.copy(pcap_file, pcap_dir_path)
405                shutil.copy(pcap_log_file, pcap_dir_path)
406        logging.debug('Copied failed packet capture data to directory')
407