1# Copyright (c) 2014 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 time
7
8from autotest_lib.client.common_lib import error
9from autotest_lib.client.common_lib.cros.network import iw_runner
10from autotest_lib.client.common_lib.cros.network import ping_runner
11from autotest_lib.client.common_lib.cros.network import xmlrpc_datatypes
12from autotest_lib.server import hosts
13from autotest_lib.server.cros.network import wifi_client
14from autotest_lib.server.cros.network import netperf_runner
15
16WORK_CLIENT_CONNECTION_RETRIES = 3
17WAIT_FOR_CONNECTION = 10
18
19class ConnectionWorker(object):
20    """ ConnectionWorker is a thin layer of interfaces for worker classes """
21
22    @property
23    def name(self):
24        """@return a string: representing name of the worker class"""
25        raise NotImplementedError('Missing subclass implementation')
26
27
28    @classmethod
29    def create_from_parent(cls, parent_obj, **init_args):
30        """Creates a derived ConnectionWorker object from the provided parent
31        object.
32
33        @param cls: derived class object which we're trying to create.
34        @param obj: existing parent class object.
35        @param init_args: Args to be passed to the derived class constructor.
36
37        @returns Instance of cls with the required fields copied from parent.
38        """
39        obj = cls(**init_args)
40        obj.work_client = parent_obj.work_client
41        obj.host = parent_obj.host
42        return obj
43
44
45    def prepare_work_client(self, work_client_machine):
46        """Prepare the SSHHost object into WiFiClient object
47
48        @param work_client_machine: a SSHHost object to be wrapped
49
50        """
51        work_client_host = hosts.create_host(work_client_machine.hostname)
52        # All packet captures in chaos lab have dual NICs. Let us use phy1 to
53        # be a radio dedicated for work client
54        iw = iw_runner.IwRunner(remote_host=work_client_host)
55        phys = iw.list_phys()
56        devs = iw.list_interfaces(desired_if_type='managed')
57        if len(devs) > 0:
58            logging.debug('Removing interfaces in work host machine %s', devs)
59            for i in range(len(devs)):
60                iw.remove_interface(devs[i].if_name)
61        if len(phys) > 1:
62            logging.debug('Adding interfaces in work host machine')
63            iw.add_interface('phy1', 'work0', 'managed')
64            logging.debug('Interfaces in work client %s', iw.list_interfaces())
65        elif len(phys) == 1:
66            raise error.TestError('Not enough phys available to create a'
67                                  'work client interface %s.' %
68                                   work_client_host.hostname)
69        self.work_client = wifi_client.WiFiClient(
70                work_client_host, './debug', False)
71        # Make the host object easily accessible
72        self.host = self.work_client.host
73
74
75    def connect_work_client(self, assoc_params):
76        """
77        Connect client to the AP.
78
79        Tries to connect the work client to AP in WORK_CLIENT_CONNECTION_RETRIES
80        tries. If we fail to connect in all tries then we would return False
81        otherwise returns True on successful connection to the AP.
82
83        @param assoc_params: an AssociationParameters object.
84        @return a boolean: True if work client is successfully connected to AP
85                or False on failing to connect to the AP
86
87        """
88        if not self.work_client.shill.init_test_network_state():
89            logging.error('Failed to set up isolated test context profile for '
90                          'work client.')
91            return False
92
93        success = False
94        for i in range(WORK_CLIENT_CONNECTION_RETRIES):
95            logging.info('Connecting work client to AP')
96            assoc_result = xmlrpc_datatypes.deserialize(
97                           self.work_client.shill.connect_wifi(assoc_params))
98            success = assoc_result.success
99            if not success:
100                logging.error('Connection attempt of work client failed, try %d'
101                              ' reason: %s', (i+1), assoc_result.failure_reason)
102            else:
103                logging.info('Work client connected to the AP')
104                self.ssid = assoc_params.ssid
105                break
106        return success
107
108
109    def cleanup(self):
110        """Teardown work_client"""
111        self.work_client.shill.disconnect(self.ssid)
112        self.work_client.shill.clean_profiles()
113
114
115    def run(self, client):
116        """Executes the connection worker
117
118        @param client: WiFiClient object representing the DUT
119
120        """
121        raise NotImplementedError('Missing subclass implementation')
122
123
124class ConnectionDuration(ConnectionWorker):
125    """This test is to check the liveliness of the connection to the AP. """
126
127    def __init__(self, duration_sec=30):
128        """
129        Holds WiFi connection open with periodic pings
130
131        @param duration_sec: amount of time to hold connection in seconds
132
133        """
134
135        self.duration_sec = duration_sec
136
137
138    @property
139    def name(self):
140        """@return a string: representing name of this class"""
141        return 'duration'
142
143
144    def run(self, client):
145        """Periodically pings work client to check liveliness of the connection
146
147        @param client: WiFiClient object representing the DUT
148
149        """
150        ping_config = ping_runner.PingConfig(self.work_client.wifi_ip, count=10)
151        logging.info('Pinging work client ip: %s', self.work_client.wifi_ip)
152        start_time = time.time()
153        while time.time() - start_time < self.duration_sec:
154            time.sleep(10)
155            ping_result = client.ping(ping_config)
156            logging.info('Connection liveness %r', ping_result)
157
158
159class ConnectionSuspend(ConnectionWorker):
160    """
161    This test is to check the liveliness of the connection to the AP with
162    suspend resume cycle involved.
163
164    """
165
166    def __init__(self, suspend_sec=30):
167        """
168        Construct a ConnectionSuspend.
169
170        @param suspend_sec: amount of time to suspend in seconds
171
172        """
173
174        self._suspend_sec = suspend_sec
175
176
177    @property
178    def name(self):
179        """@return a string: representing name of this class"""
180        return 'suspend'
181
182
183    def run(self, client):
184        """
185        Check the liveliness of the connection to the AP by pinging the work
186        client before and after a suspend resume.
187
188        @param client: WiFiClient object representing the DUT
189
190        """
191        ping_config = ping_runner.PingConfig(self.work_client.wifi_ip, count=10)
192        # pinging work client to ensure we have a connection
193        logging.info('work client ip: %s', self.work_client.wifi_ip)
194        ping_result = client.ping(ping_config)
195        logging.info('before suspend:%r', ping_result)
196        client.do_suspend(self._suspend_sec)
197        # When going to suspend, DUTs using ath9k devices do not disassociate
198        # from the AP. On resume, DUTs would re-use the association from prior
199        # to suspend. However, this leads to some confused state for some APs
200        # (see crbug.com/346417) where the AP responds to actions frames like
201        # NullFunc but not to any data frames like DHCP/ARP packets from the
202        # DUT.  Let us sleep for:
203        #       + 2 seconds for linkmonitor to detect failure if any
204        #       + 10 seconds for ReconnectTimer timeout
205        #       + 5 seconds to reconnect to the AP
206        #       + 3 seconds let us not have a very strict timeline.
207        # 20 seconds before we start to query shill about the connection state.
208        # TODO (krisr): add board detection code in wifi_client and adjust the
209        # sleep time here based on the wireless chipset
210        time.sleep(20)
211
212        # Wait for WAIT_FOR_CONNECTION time before trying to ping.
213        success, state, elapsed_time = client.wait_for_service_states(
214                self.ssid, ('ready', 'portal', 'online'), WAIT_FOR_CONNECTION)
215        if not success:
216            raise error.TestFail('DUT failed to connect to AP (%s state) after'
217                                 'resume in %d seconds' %
218                                 (state, WAIT_FOR_CONNECTION))
219        else:
220            logging.info('DUT entered %s state after %s seconds',
221                         state, elapsed_time)
222            # ping work client to ensure we have connection after resume.
223            ping_result = client.ping(ping_config)
224            logging.info('after resume:%r', ping_result)
225
226
227class ConnectionNetperf(ConnectionWorker):
228    """
229    This ConnectionWorker is used to run a sustained data transfer between the
230    DUT and the work_client through an AP.
231
232    """
233
234    # Minimum expected throughput for netperf streaming tests
235    NETPERF_MIN_THROUGHPUT = 2.0 # Mbps
236
237    def __init__(self, netperf_config):
238        """
239        Construct a ConnectionNetperf object.
240
241        @param netperf_config: NetperfConfig object to define transfer test.
242
243        """
244        self._config = netperf_config
245
246
247    @property
248    def name(self):
249        """@return a string: representing name of this class"""
250        return 'netperf_%s' % self._config.human_readable_tag
251
252
253    def run(self, client):
254        """
255        Create a NetperfRunner, run netperf between DUT and work_client.
256
257        @param client: WiFiClient object representing the DUT
258
259        """
260        with netperf_runner.NetperfRunner(
261                client, self.work_client, self._config) as netperf:
262            ping_config = ping_runner.PingConfig(
263                    self.work_client.wifi_ip, count=10)
264            # pinging work client to ensure we have a connection
265            logging.info('work client ip: %s', self.work_client.wifi_ip)
266            ping_result = client.ping(ping_config)
267
268            result = netperf.run(self._config)
269            logging.info('Netperf Result: %s', result)
270
271        if result is None:
272            raise error.TestError('Failed to create NetperfResult')
273
274        if result.duration_seconds < self._config.test_time:
275            raise error.TestFail(
276                    'Netperf duration too short: %0.2f < %0.2f' %
277                    (result.duration_seconds, self._config.test_time))
278
279        # TODO: Convert this limit to a perf metric crbug.com/348780
280        if result.throughput <self.NETPERF_MIN_THROUGHPUT:
281            raise error.TestFail(
282                    'Netperf throughput too low: %0.2f < %0.2f' %
283                    (result.throughput, self.NETPERF_MIN_THROUGHPUT))
284