autotest: Migrates network_WiFiRoaming/003SSIDMultiSwitchBack to new method.

Replaces a portion of network_WiFiRoaming/00SSIDMultiSwitchBack test
with script and control file in network_WiFi_SSIDMultiSwitchBack
directory.  The 'low signal' tests will be submitted in a different CL.

New code conforms to test infrastructure changes provided to address
crbug.com/224443.

This roaming test configures 2 radios (different frequencies and BSSIDs) on
the same SSID, connects to one, and cuts its power to simulate an
out-of-range event. It then verifies that the DUT discovers this and
connects to the other radio.  Wrote the test to use the
BeagleBone-connected RF attenuators (currently requires a specific
setup: chromeos3-grover-host1) to change the AP's transmit power rather
than the previous 'iw' command that didn't work, anyway.

This CL required various plumbing efforts in the test infrastructure:
  - mulit_interface.  Augmented RvRTestContextManager.configure to take, and
    pass-through, multi_interface (did is_ibss while I was there) to allow
    configuration of an AP to use 2 radios.  Modified LinuxRouter to respond
    to its multi_interface parameter.
  - ap_num.  Added ap_num to WifiTestContextManager.wait_for_connection and
    .assert_ping_from_dut so that I could ping the second radio on an AP.
  - port and MAX_ATTENUATION.  Added port-specific attenuation calls to
    Attenuator and made set_variable_attenuation public so that I could set
    max attenuation (which is relative to the fixed_loss of the attenuator --
    added MAX_ATTENUATION, as well).

BUG=chromium:263887
TEST=autotest (this one).

Change-Id: I00d3b445a22b0a94579f3b033b8f6039764d8c2a
Reviewed-on: https://chromium-review.googlesource.com/168457
Reviewed-by: Wade Guthrie <wdg@chromium.org>
Commit-Queue: Wade Guthrie <wdg@chromium.org>
Tested-by: Wade Guthrie <wdg@chromium.org>
diff --git a/server/cros/network/rvr_test_context_manager.py b/server/cros/network/rvr_test_context_manager.py
index df5f1aa..6fa4cc3 100644
--- a/server/cros/network/rvr_test_context_manager.py
+++ b/server/cros/network/rvr_test_context_manager.py
@@ -70,7 +70,16 @@
         self._attenuator.cleanup()
 
 
-    def configure(self, ap_config):
-        """Configures AP and variable attenuators for a WiFi RvR test."""
-        super(RvRTestContextManager, self).configure(ap_config)
+    def configure(self, ap_config, multi_interface=None, is_ibss=None):
+        """Configures AP and variable attenuators for a WiFi RvR test.
+
+        @param ap_config: parameters for access-point
+        @param multi_interface: bool indicates whether AP's second radio
+               is being configured by this command
+        @is_ibss: bool indicates whether this is an IBSS endpoint
+
+        """
+        super(RvRTestContextManager, self).configure(
+                configuration_parameters=ap_config,
+                multi_interface=multi_interface, is_ibss=is_ibss)
         self._attenuator.config(self.client.host.hostname, ap_config.frequency)
diff --git a/server/cros/network/wifi_test_context_manager.py b/server/cros/network/wifi_test_context_manager.py
index 281c3b0..a10ee72 100644
--- a/server/cros/network/wifi_test_context_manager.py
+++ b/server/cros/network/wifi_test_context_manager.py
@@ -295,11 +295,12 @@
         self.server.ping(ping_config)
 
 
-    def wait_for_connection(self, ssid, freq=None):
+    def wait_for_connection(self, ssid, freq=None, ap_num=None):
         """Verifies a connection to network ssid on frequency freq.
 
         @param ssid string ssid of the network to check.
         @param freq int frequency of network to check.
+        @param ap_num int AP to which to connect
 
         """
         success, state, elapsed_seconds = self.client.wait_for_service_states(
@@ -309,7 +310,7 @@
                     'Failed to connect to "%s" in %f seconds (state=%s)' %
                     (ssid, elapsed_seconds, state))
 
-        self.assert_ping_from_dut()
+        self.assert_ping_from_dut(ap_num=ap_num)
         if freq:
             self.client.check_iw_link_value(
                     wifi_client.WiFiClient.IW_LINK_KEY_FREQUENCY, freq)
diff --git a/server/site_attenuator.py b/server/site_attenuator.py
index a7d9a33..442cf35 100644
--- a/server/site_attenuator.py
+++ b/server/site_attenuator.py
@@ -43,6 +43,10 @@
     # We only use 2 ports out of the 4 available.
     PORTS = [0, 1]
 
+    # The scripts that run on the attenuator limit the attenuation to this
+    # plus the fixed attenuation for the specific port.
+    MAX_VARIABLE_ATTENUATION = 95
+
 
     # TODO(tgao): refactor & merge this w/ site_wifitest.install_script()
     def _copy_script(self, script_name, *support_scripts):
@@ -53,6 +57,7 @@
         @return a string, script_name if it's copied successfully.
 
         @raises ScriptNotFound if any source script is not found.
+
         """
         if script_name in self._installed_scripts:
             return self._installed_scripts[script_name]
@@ -88,6 +93,7 @@
 
         @param port: an integer, Beaglebone I/O port number (0 or 1).
         @param cleanup: a boolean, True == unexport GPIO pins/reset port.
+
         """
         # TODO(tgao): bundle these scripts as part of a test image?
         flag = '-c' if cleanup else ''
@@ -105,6 +111,7 @@
         """Initialize.
 
         @param host: an Autotest host object, representing the attenuator.
+
         """
         self._host = host
         self._installed_scripts = dict()
@@ -130,33 +137,54 @@
         """Reads current attenuation level in dB.
 
         @param port: an integer, Beaglebone I/O port number (0 or 1).
+
         """
         self._host.run('python "%s" -p %d 2>&1' % (self._config_script, port))
 
 
-    def _set_variable_attenuation(self, variable_db):
+    def set_variable_attenuation_on_port(self, port, variable_db):
+        """Sets desired variable attenuation in dB.
+
+        @param port: port to attenuate.
+        @param variable_db: an integer, variable attenuation in dB.
+
+        """
+        fixed_db = self.fixed_loss[port]
+        total_db = fixed_db + variable_db
+        self._host.run('python "%s" -p %d -f %d -t %d 2>&1' %
+                       (self._config_script, port, fixed_db, total_db))
+
+
+    def set_variable_attenuation(self, variable_db):
         """Sets desired variable attenuation in dB.
 
         @param variable_db: an integer, variable attenuation in dB.
-        @returns an integer, total attenuation in dB.
+
         """
         for port in self.PORTS:
-            fixed_db = self.fixed_loss[port]
-            total_db = fixed_db + variable_db
-            self._host.run('python "%s" -p %d -f %d -t %d 2>&1' %
-                           (self._config_script, port, fixed_db, total_db))
-        return total_db
+            self.set_variable_attenuation_on_port(port, variable_db)
+
+
+    def set_total_attenuation_on_port(self, port, total_db):
+        """Sets desired total attenuation in dB.
+
+        @param port: port to attenuate.
+        @param total_db: an integer, total attenuation in dB.
+
+        """
+        self._host.run('python "%s" -p %d -f %d -t %d 2>&1' %
+                       (self._config_script, port, self.fixed_loss[port],
+                        total_db))
 
 
     def set_total_attenuation(self, total_db):
         """Sets desired total attenuation in dB.
 
         @param total_db: an integer, total attenuation in dB.
+
         """
         for port in self.PORTS:
-            self._host.run('python "%s" -p %d -f %d -t %d 2>&1' %
-                           (self._config_script, port, self.fixed_loss[port],
-                            total_db))
+            self.set_total_attenuation_on_port(port, total_db)
 
 
     @staticmethod
@@ -168,6 +196,7 @@
 
         @param freq an integer, frequency in MHz.
         @returns an integer, approximate frequency from FREQ_LOSS_MAP.
+
         """
         old_offset = None
         approx_freq = None
@@ -203,6 +232,6 @@
         logging.info('Looking up fixed path loss on freq %d', freq_used)
 
         self.fixed_loss = self.FREQ_LOSS_MAP[freq_used][hostname]
-        self._set_variable_attenuation(0)
+        self.set_variable_attenuation(0)
 
 
diff --git a/server/site_linux_router.py b/server/site_linux_router.py
index 19b0fa0..09a8718 100644
--- a/server/site_linux_router.py
+++ b/server/site_linux_router.py
@@ -357,14 +357,17 @@
             conf['obss_interval'] = configuration.obss_interval
         conf.update(configuration.get_security_hostapd_conf())
 
-        self.start_hostapd(conf, {})
+        # TODO(wiley): Remove this multi_interface flag when the bridge router
+        # class is gone.
+        params = {'multi_interface': 1} if multi_interface else {}
+        self.start_hostapd(conf, params)
         # Configure transmit power
         tx_power_params = {'interface': conf['interface']}
         # TODO(wiley) support for setting transmit power
         self.set_txpower(tx_power_params)
         if self.force_local_server:
             self.start_local_server(conf['interface'])
-        self._post_start_hook({})
+        self._post_start_hook(params)
         logging.info('AP configured.')
         self.hostapd['configured'] = True
 
diff --git a/server/site_tests/network_WiFi_SSIDMultiSwitchBack/control b/server/site_tests/network_WiFi_SSIDMultiSwitchBack/control
new file mode 100644
index 0000000..5bae492
--- /dev/null
+++ b/server/site_tests/network_WiFi_SSIDMultiSwitchBack/control
@@ -0,0 +1,23 @@
+# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+AUTHOR = 'Wade Guthrie <wdg@google.com>'
+TIME = 'SHORT'
+NAME = 'network_WiFi_SSIDMultiSwitchBack'
+TEST_CATEGORY = 'Functional'
+TEST_CLASS = 'network'
+TEST_TYPE = 'Server'
+DOC = """
+The SSIDSwitchBack test verifies that the connection manager is able
+to switch APs when one disappears.
+"""
+
+def run(machine):
+    host = hosts.create_host(machine)
+    job.run_test('network_WiFi_SSIDMultiSwitchBack',
+                 host=host,
+                 raw_cmdline_args=args)
+
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/network_WiFi_SSIDMultiSwitchBack/network_WiFi_SSIDMultiSwitchBack.py b/server/site_tests/network_WiFi_SSIDMultiSwitchBack/network_WiFi_SSIDMultiSwitchBack.py
new file mode 100644
index 0000000..025819d
--- /dev/null
+++ b/server/site_tests/network_WiFi_SSIDMultiSwitchBack/network_WiFi_SSIDMultiSwitchBack.py
@@ -0,0 +1,105 @@
+# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import logging
+import time
+
+from autotest_lib.client.common_lib.cros.network import xmlrpc_datatypes
+from autotest_lib.server import site_attenuator
+from autotest_lib.server.cros.network import hostap_config
+from autotest_lib.server.cros.network import rvr_test_base
+
+class network_WiFi_SSIDMultiSwitchBack(rvr_test_base.RvRTestBase):
+    """Tests roaming to an AP when the old one's signal is too weak.
+
+    This test uses a dual-radio Stumpy as the AP and configures the radios to
+    broadcast two BSS's with different frequencies on the same SSID.  The DUT
+    connects to the first radio, the test attenuates that radio, and the DUT
+    is supposed to roam to the second radio.
+
+    This test requires a particular configuration of test equipment:
+
+                                   +--------- StumpyCell/AP ----------+
+                                   | chromeX.grover.hostY.router.cros |
+                                   |                                  |
+                                   |       [Radio 0]  [Radio 1]       |
+                                   +--------A-----B----C-----D--------+
+        +------ BeagleBone ------+          |     |    |     |
+        | chromeX.grover.hostY.  |          |     X    |     X
+        | attenuator.cros      [Port0]-[attenuator]    |
+        |                      [Port1]----- | ----[attenuator]
+        |                      [Port2]-X    |          |
+        |                      [Port3]-X    +-----+    |
+        |                        |                |    |
+        +------------------------+                |    |
+                                   +--------------E----F--------------+
+                                   |             [Radio 0]            |
+                                   |                                  |
+                                   |    chromeX.grover.hostY.cros     |
+                                   +-------------- DUT ---------------+
+
+    Where antennas A, C, and E are the primary antennas for AP/radio0,
+    AP/radio1, and DUT/radio0, respectively; and antennas B, D, and F are the
+    auxilliary antennas for AP/radio0, AP/radio1, and DUT/radio0,
+    respectively.  The BeagleBone controls 2 attenuators that are connected
+    to the primary antennas of AP/radio0 and 1 which are fed into the primary
+    and auxilliary antenna ports of DUT/radio 0.  Ports 2 and 3 of the
+    BeagleBone as well as the auxillary antennae of AP/radio0 and 1 are
+    terminated.
+
+    This arrangement ensures that the attenuator port numbers are assigned to
+    the primary radio, first, and the secondary radio, second.  If this happens,
+    the ports will be numbered in the order in which the AP's channels are
+    configured (port 0 is first, port 1 is second, etc.).
+
+    This test is a de facto test that the ports are configured in that
+    arrangement since swapping Port0 and Port1 would cause us to attenuate the
+    secondary radio, providing no impetus for the DUT to switch radios and
+    causing the test to fail to connect at radio 1's frequency.
+
+    """
+
+    version = 1
+
+    FREQUENCY_0 = 2412
+    FREQUENCY_1 = 2462
+    PORT_0 = 0  # Port created first (FREQUENCY_0)
+    PORT_1 = 1  # Port created second (FREQUENCY_1)
+
+
+    def run_once(self):
+        """Test body."""
+
+        logging.info("- Configure first AP & connect")
+        self.context.configure(hostap_config.HostapConfig(
+                frequency=network_WiFi_SSIDMultiSwitchBack.FREQUENCY_0,
+                mode=hostap_config.HostapConfig.MODE_11G))
+        router_ssid = self.context.router.get_ssid()
+        self.context.assert_connect_wifi(xmlrpc_datatypes.AssociationParameters(
+                ssid=router_ssid))
+        self.context.assert_ping_from_dut()
+
+        logging.info('- Configure second AP')
+        self.context.configure(hostap_config.HostapConfig(
+                ssid=router_ssid,
+                frequency=network_WiFi_SSIDMultiSwitchBack.FREQUENCY_1,
+                mode=hostap_config.HostapConfig.MODE_11G),
+                               multi_interface=True)
+
+        logging.info('- Drop the power on the first AP')
+        self.context.attenuator.set_variable_attenuation_on_port(
+            network_WiFi_SSIDMultiSwitchBack.PORT_0,
+            site_attenuator.Attenuator.MAX_VARIABLE_ATTENUATION)
+        time.sleep(50)  # Give DUT time to scan and roam.
+
+        logging.info("- Wait for a connection on the second AP")
+        # Instead of explicitly connecting, just wait to see if the DUT
+        # connects to the second AP by itself
+        self.context.wait_for_connection(
+                ssid=router_ssid,
+                freq=network_WiFi_SSIDMultiSwitchBack.FREQUENCY_1,
+                ap_num=1)
+
+        # Clean up.
+        self.context.router.deconfig()