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