Add regulatory test for channel move

Inject an 802.11h Spectrum Management Channel Switch Action Frame
into an actively running 802.11 connection and ensure that the
client vacates the channel.

CQ-DEPEND=CL:*39532
BUG=chrome-os-partner:19953
TEST=This is a test; Passes with Lumpy.  Fails with daisy ToT, and
passes with daisy with kernel changes to support 802.11h.

Change-Id: I25a5d35e5a7f0c429ea52eb38ab9a8fce6c27211
Reviewed-on: https://gerrit.chromium.org/gerrit/58431
Reviewed-by: Christopher Wiley <wiley@chromium.org>
Tested-by: Paul Stewart <pstew@chromium.org>
Commit-Queue: Paul Stewart <pstew@chromium.org>
diff --git a/server/cros/wlan/wifi_client.py b/server/cros/wlan/wifi_client.py
index c2f65a1..acca65c 100644
--- a/server/cros/wlan/wifi_client.py
+++ b/server/cros/wlan/wifi_client.py
@@ -213,13 +213,15 @@
         self._host.close()
 
 
-    def ping(self, ping_ip, ping_args, count=None):
+    def ping(self, ping_ip, ping_args, count=None, ignore_status=False):
         """Ping an address from the client and return the command output.
 
         @param ping_ip string IPv4 address for the client to ping.
         @param ping_args dict of parameters understood by
                 wifi_test_utils.ping_args().
         @param count int number of times to ping the address.
+        @param ignore_status bool whether to consider an error exit status
+                from the ping command to be a fatal error.
         @return string raw output of the ping command
 
         """
@@ -233,7 +235,7 @@
                 '%s %s %s' % (self.COMMAND_PING,
                               wifi_test_utils.ping_args(ping_args),
                               ping_ip),
-                timeout=timeout)
+                timeout=timeout, ignore_status=ignore_status)
         return result.stdout
 
 
diff --git a/server/site_linux_router.py b/server/site_linux_router.py
index 3af1d05..c1e8466 100644
--- a/server/site_linux_router.py
+++ b/server/site_linux_router.py
@@ -33,6 +33,18 @@
     """
 
 
+    def get_capabilities(self):
+        """@return iterable object of AP capabilities for this system."""
+        caps = set()
+        try:
+            self.cmd_send_management_frame = wifi_test_utils.must_be_installed(
+                    self.router, '/usr/bin/send_management_frame')
+            caps.add(self.CAPABILITY_SEND_MANAGEMENT_FRAME)
+        except error.TestFail:
+            pass
+        return super(LinuxRouter, self).get_capabilities().union(caps)
+
+
     def __init__(self, host, params, defssid):
         """Build a LinuxRouter.
 
@@ -737,6 +749,21 @@
                          params['client']))
 
 
+    def send_management_frame(self, frame_type, instance=0):
+        """Injects a management frame into an active hostapd session.
+
+        @param frame_type string the type of frame to send.
+        @param instance int indicating which hostapd instance to inject into.
+
+        """
+        hostap_interface = self.hostapd_instances[instance]['interface']
+        interface = self._get_wlanif(0, 'monitor', same_phy_as=hostap_interface)
+        self.router.run("%s link set %s up" % (self.cmd_ip, interface))
+        self.router.run('%s %s %s' %
+                        (self.cmd_send_management_frame, interface, frame_type))
+        self._release_wlanif(interface)
+
+
     def _pre_config_hook(self, config):
         """Hook for subclasses.
 
diff --git a/server/site_linux_system.py b/server/site_linux_system.py
index 7b3b2f4..d4c8dff 100644
--- a/server/site_linux_system.py
+++ b/server/site_linux_system.py
@@ -23,6 +23,7 @@
     CAPABILITY_MULTI_AP = 'multi_ap'
     CAPABILITY_MULTI_AP_SAME_BAND = 'multi_ap_same_band'
     CAPABILITY_IBSS = 'ibss_supported'
+    CAPABILITY_SEND_MANAGEMENT_FRAME = 'send_management_frame'
 
 
     @property
@@ -250,7 +251,7 @@
         return phys[0]
 
 
-    def _get_wlanif(self, frequency, phytype, mode = None):
+    def _get_wlanif(self, frequency, phytype, mode = None, same_phy_as = None):
         """Get a WiFi device that supports the given frequency, mode, and type.
 
         This function is used by inherited classes, so we use the single '_'
@@ -263,10 +264,18 @@
         @param frequency int WiFi frequency to support.
         @param phytype string type of phy (e.g. 'monitor').
         @param mode string 'a' 'b' or 'g'.
+        @param same_phy_as string create the interface on the same phy as this.
         @return string WiFi device.
 
         """
-        if mode in ('b', 'g') and self.phydev2 is not None:
+        if same_phy_as:
+            for phy, wlanif_i, phytype_i in self.wlanifs_in_use:
+                if wlanif_i == same_phy_as:
+                     break
+            else:
+                raise error.TestFail('Unable to find phy for interface %s' %
+                                     same_phy_as)
+        elif mode in ('b', 'g') and self.phydev2 is not None:
             phy = self.phydev2
         elif mode == 'a' and self.phydev5 is not None:
             phy = self.phydev5
diff --git a/server/site_tests/network_WiFi_Regulatory/control b/server/site_tests/network_WiFi_Regulatory/control
new file mode 100644
index 0000000..4e4687e
--- /dev/null
+++ b/server/site_tests/network_WiFi_Regulatory/control
@@ -0,0 +1,33 @@
+# 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 = 'pstew@chromium.com'
+NAME = 'network_WiFi_Regulatory'
+TIME = 'SHORT'
+TEST_TYPE = 'Server'
+SUITE = 'wifi_matfunc'
+DEPENDENCIES = 'wificell'
+
+DOC = """
+This test verifies that DUT will move off-channel if it is sent a
+Spectrum Management action frame that contains a Channel Move
+element.  Such frames are sent on a DFS network to vacate the
+channel if radar is detected.
+"""
+
+
+from autotest_lib.server.cros.wlan import hostap_config
+
+
+def run(machine):
+    a_mode = hostap_config.HostapConfig.MODE_11A
+    configurations = [(hostap_config.HostapConfig(channel=64, mode=a_mode), 36)]
+    host = hosts.create_host(machine)
+    job.run_test('network_WiFi_Regulatory',
+                 host=host,
+                 raw_cmdline_args=args,
+                 additional_params=configurations)
+
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/network_WiFi_Regulatory/network_WiFi_Regulatory.py b/server/site_tests/network_WiFi_Regulatory/network_WiFi_Regulatory.py
new file mode 100644
index 0000000..e643775
--- /dev/null
+++ b/server/site_tests/network_WiFi_Regulatory/network_WiFi_Regulatory.py
@@ -0,0 +1,53 @@
+# 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.
+
+from autotest_lib.client.common_lib import error
+from autotest_lib.client.common_lib.cros.network import xmlrpc_datatypes
+from autotest_lib.server import site_linux_system
+from autotest_lib.server.cros import wifi_test_utils
+from autotest_lib.server.cros.wlan import wifi_cell_test_base
+
+
+class network_WiFi_Regulatory(wifi_cell_test_base.WiFiCellTestBase):
+    """Test that the client vacates the channel and can no longer ping after
+    notification from the AP that it should switch channels."""
+    version = 1
+
+
+    def parse_additional_arguments(self, commandline_args, additional_params):
+        """Hook into super class to take control files parameters.
+
+        @param commandline_args dict of parsed parameters from the autotest.
+        @param additional_params list of dicts describing router configs.
+
+        """
+        self._configurations = additional_params
+
+
+    def run_once(self):
+        """Sets up a router, connects to it, then tests a channel switch."""
+        for router_conf, alternate_channel in self._configurations:
+            self.context.router.require_capabilities(
+                  [site_linux_system.LinuxSystem.
+                          CAPABILITY_SEND_MANAGEMENT_FRAME])
+            self.context.configure(router_conf)
+            assoc_params = xmlrpc_datatypes.AssociationParameters()
+            assoc_params.ssid = self.context.router.get_ssid()
+            self.context.assert_connect_wifi(assoc_params)
+            ping_ip = self.context.get_wifi_addr(ap_num=0)
+            result = self.context.client.ping(ping_ip, {}, ignore_status=True)
+            for attempt in range(10):
+                self.context.router.send_management_frame(
+                        'channel_switch:%d' % alternate_channel)
+                # This should fail at some point.  Since the client
+                # might be in power-save, we are not guaranteed it will hear
+                # this message the first time around.
+                result = self.context.client.ping(ping_ip, {'count':3})
+                stats = wifi_test_utils.parse_ping_output(result)
+                if float(stats['loss']) > 60:
+                    break
+            else:
+                raise error.TestFail('Client never lost connectivity')
+            self.context.client.shill.disconnect(assoc_params.ssid)
+            self.context.router.deconfig()