autotest: Refactor router configuration

1) Add new HostapConfig object which abstracts and aids router
    configuration.
2) Change new WiFi tests to configure routers through a configure()
    method on the WiFiTestContextManager.  This facillitates packet
    captures since we start the captures at configuration time.  Routing
    this call through the context lets us share a common point to
    start client/server/router captures, similar to site_wifitest.

TEST=network_WiFi_SimpleConnect/control.* pass.  Manually inspected the
802.11n parameters for correctness.
BUG=chromium:231429

Change-Id: Ifa4b92ce96094451864769bb5b3df806017773a1
Reviewed-on: https://gerrit.chromium.org/gerrit/48668
Tested-by: Christopher Wiley <wiley@chromium.org>
Reviewed-by: Paul Stewart <pstew@chromium.org>
Commit-Queue: Christopher Wiley <wiley@chromium.org>
diff --git a/server/cros/wlan/hostap_config.py b/server/cros/wlan/hostap_config.py
new file mode 100644
index 0000000..231be60
--- /dev/null
+++ b/server/cros/wlan/hostap_config.py
@@ -0,0 +1,164 @@
+# 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
+
+from autotest_lib.client.common_lib import error
+
+
+class HostapConfig(object):
+    """Parameters for router configuration."""
+
+    # A mapping of frequency to channel number.  This includes some
+    # frequencies used outside the US.
+    CHANNEL_MAP = {2412: 1,
+                   2417: 2,
+                   2422: 3,
+                   2427: 4,
+                   2432: 5,
+                   2437: 6,
+                   2442: 7,
+                   2447: 8,
+                   2452: 9,
+                   2457: 10,
+                   2462: 11,
+                   # 12, 13 are only legitimate outside the US.
+                   2467: 12,
+                   2472: 13,
+                   # 34 valid in Japan.
+                   5170: 34,
+                   # 36-116 valid in the US, except 38, 42, and 46, which have
+                   # mixed international support.
+                   5180: 36,
+                   5190: 38,
+                   5200: 40,
+                   5210: 42,
+                   5220: 44,
+                   5230: 46,
+                   5240: 48,
+                   5260: 52,
+                   5280: 56,
+                   5300: 60,
+                   5320: 64,
+                   5500: 100,
+                   5520: 104,
+                   5540: 108,
+                   5560: 112,
+                   5580: 116,
+                   # 120, 124, 128 valid in Europe/Japan.
+                   5600: 120,
+                   5620: 124,
+                   5640: 128,
+                   # 132+ valid in US.
+                   5660: 132,
+                   5680: 136,
+                   5700: 140,
+                   5745: 149,
+                   5765: 153,
+                   5785: 157,
+                   5805: 161,
+                   5825: 165}
+
+    MODE_11A = 'a'
+    MODE_11B = 'b'
+    MODE_11G = 'g'
+    MODE_11N_MIXED = 'n-mixed'
+    MODE_11N_PURE = 'n-only'
+
+    N_CAPABILITY_WMM = object()
+    N_CAPABILITY_HT20 = object()
+    N_CAPABILITY_HT40 = object()
+    N_CAPABILITY_HT40_PLUS = object()
+    N_CAPABILITY_HT40_MINUS = object()
+    N_CAPABILITY_GREENFIELD = object()
+    N_CAPABILITY_SHORT_GI = object()
+
+
+    def __init__(self, mode=None, channel=None, frequency=None,
+                 n_capabilities=None, hide_ssid=None):
+        """Construct a HostapConfig.
+
+        You may specify channel or frequency, but not both.  Both options
+        are checked for validity (i.e. you can't specify an invalid channel
+        or a frequency that will not be accepted).
+
+        @param mode string MODE_11x defined above.
+        @param channel int channel number.
+        @param frequency int frequency of channel.
+        @param n_capabilities list of N_CAPABILITY_x defined above.
+        @param hide_ssid True if we should set up a hidden SSID.
+
+        """
+        super(HostapConfig, self).__init__()
+        if channel is not None and frequency is not None:
+            raise error.TestError('Specify either frequency or channel '
+                                  'but not both.')
+
+        if channel is None and frequency is None:
+            raise error.TestError('Specify either frequency or channel.')
+
+        for real_frequency, real_channel in self.CHANNEL_MAP.iteritems():
+            if frequency == real_frequency or channel == real_channel:
+                self.frequency = real_frequency
+                self.channel = real_channel
+                break
+        else:
+            raise error.TestError('Invalid channel %r or frequency %r '
+                                  'specified.' % channel, frequency)
+
+        self.is_11n = False
+        self.require_ht = False
+        if mode in (self.MODE_11N_MIXED, self.MODE_11N_PURE) or n_capabilities:
+            if mode == self.MODE_11N_PURE:
+                self.require_ht = True
+            # For their own historical reasons, hostapd wants it this way.
+            if self.frequency > 5000:
+                mode = self.MODE_11A
+            else:
+                mode = self.MODE_11G
+            self.is_11n = True
+        if self.frequency > 5000 and mode != self.MODE_11A:
+            raise error.TestError('Must use 11a or 11n mode for '
+                                  'frequency > 5Ghz')
+
+        if self.frequency < 5000 and mode == self.MODE_11A:
+            raise error.TestError('Cannot use 11a with frequency %d.' %
+                                  self.frequency)
+
+        if not mode in (self.MODE_11A, self.MODE_11B, self.MODE_11G, None):
+            raise error.TestError('Invalid router mode %r' % mode)
+
+        self.wmm_enabled = False
+        self.hw_mode = mode or self.MODE_11B
+        self.ssid_suffix = '_ch%d' % self.channel
+        if n_capabilities is None:
+            n_capabilities = []
+        self.n_capabilities = set()
+        for cap in n_capabilities:
+            if cap == self.N_CAPABILITY_HT40:
+                self.wmm_enabled = True
+                self.n_capabilities.add('[HT40-]')
+                self.n_capabilities.add('[HT40+]')
+            elif cap == self.N_CAPABILITY_HT40_PLUS:
+                self.wmm_enabled = True
+                self.n_capabilities.add('[HT40+]')
+            elif cap == self.N_CAPABILITY_HT40_MINUS:
+                self.wmm_enabled = True
+                self.n_capabilities.add('[HT40-]')
+            elif cap == self.N_CAPABILITY_GREENFIELD:
+                logging.warning('Greenfield flag is ignored for hostap...')
+                #TODO(wiley) Why does this not work?
+                #self.n_capabilities.add('[GF]')
+            elif cap == self.N_CAPABILITY_SHORT_GI:
+                self.n_capabilities.add('[SHORT-GI-20]')
+                self.n_capabilities.add('[SHORT-GI-40]')
+            elif cap == self.N_CAPABILITY_HT20:
+                # This isn't a real thing.  HT mode implies 20 supported.
+                self.wmm_enabled = True
+            elif cap == self.N_CAPABILITY_WMM:
+                self.wmm_enabled = True
+            else:
+                raise error.TestError('Unknown capability: %r' % cap)
+
+        self.hide_ssid = hide_ssid
diff --git a/server/cros/wlan/wifi_test_context_manager.py b/server/cros/wlan/wifi_test_context_manager.py
index 88ade4d..fb11662 100644
--- a/server/cros/wlan/wifi_test_context_manager.py
+++ b/server/cros/wlan/wifi_test_context_manager.py
@@ -87,6 +87,19 @@
         return self.server.wifi_ip
 
 
+    def configure(self, configuration_parameters):
+        """Configure a router with the given parameters.
+
+        Configures an AP according to the specified parameters and
+        enables whatever packet captures are appropriate.
+
+        @param configuration_parameters HostapConfig object.
+
+        """
+        self.router.hostap_configure(configuration_parameters)
+        # TODO(wiley) enable packet captures here.
+
+
     def setup(self):
         """Construct the state used in a WiFi test."""
         if utils.host_is_in_lab_zone(self.client.host.hostname):
diff --git a/server/site_linux_router.py b/server/site_linux_router.py
index da0f85e..4065644 100644
--- a/server/site_linux_router.py
+++ b/server/site_linux_router.py
@@ -160,6 +160,7 @@
         @param params dict of site_wifitest parameters.
 
         """
+        logging.info('Starting hostapd with parameters: %r', conf)
         # Figure out the correct interface.
         interface = self._get_wlanif(self.hostapd['frequency'],
                                      self.phytype,
@@ -225,6 +226,57 @@
         """Kill all hostapd instances."""
         self.kill_hostapd_instance(None)
 
+
+    def __get_default_hostap_config(self):
+        """@return dict of default options for hostapd."""
+        conf = self.hostapd['conf']
+        # default RTS and frag threshold to ``off''
+        conf['rts_threshold'] = '2347'
+        conf['fragm_threshold'] = '2346'
+        conf['driver'] = self.hostapd['driver']
+        return conf
+
+
+    def hostap_configure(self, configuration, multi_interface=None):
+        """Build up a hostapd configuration file and start hostapd.
+
+        Also setup a local server if this router supports them.
+
+        @param configuration HosetapConfig object.
+        @param multi_interface bool True iff multiple interfaces allowed.
+
+        """
+        if multi_interface is None and (self.hostapd['configured'] or
+                                        self.station['configured']):
+            self.deconfig()
+        # Start with the default hostapd config parameters.
+        conf = self.__get_default_hostap_config()
+        conf['ssid'] = (self.defssid + configuration.ssid_suffix)[-32:]
+        conf['channel'] = configuration.channel
+        self.hostapd['frequency'] = configuration.frequency
+        conf['hw_mode'] = configuration.hw_mode
+        if configuration.hide_ssid:
+            conf['ignore_broadcast_ssid'] = 1
+        if configuration.is_11n:
+            conf['ieee80211n'] = 1
+            conf['ht_capab'] = ''.join(configuration.n_capabilities)
+        if configuration.wmm_enabled:
+            conf['wmm_enabled'] = 1
+        if configuration.require_ht:
+            conf['require_ht'] = 1
+        # TODO(wiley) beacon interval support
+        self.start_hostapd(conf, {})
+        # 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({})
+        logging.info('AP configured.')
+        self.hostapd['configured'] = True
+
+
     def hostap_config(self, params):
         """Configure the AP per test requirements.
 
@@ -244,18 +296,10 @@
 
         local_server = params.pop('local_server', False)
 
-        # Construct the hostapd.conf file and start hostapd.
-        conf = self.hostapd['conf']
-        # default RTS and frag threshold to ``off''
-        conf['rts_threshold'] = '2347'
-        conf['fragm_threshold'] = '2346'
-
+        conf = self.__get_default_hostap_config()
         tx_power_params = {}
         htcaps = set()
 
-        conf['driver'] = params.get('hostapd_driver',
-            self.hostapd['driver'])
-
         for k, v in params.iteritems():
             if k == 'ssid':
                 conf['ssid'] = v
diff --git a/server/site_tests/network_WiFi_SimpleConnect/control.check11a b/server/site_tests/network_WiFi_SimpleConnect/control.check11a
index d55c080..8ab9451 100644
--- a/server/site_tests/network_WiFi_SimpleConnect/control.check11a
+++ b/server/site_tests/network_WiFi_SimpleConnect/control.check11a
@@ -13,19 +13,18 @@
 """
 
 
+from autotest_lib.server.cros.wlan import hostap_config
+
+
 def run(machine):
-    # TODO(tgao): do we need to test connection on more channels?
-    channels = [{'channel': '5240',
-                 'mode': '11a',
-                 'ssid_suffix': 'ch48'},
-                {'channel': '5320',
-                 'mode': '11a',
-                 'ssid_suffix': 'ch64'}]
+    a_mode = hostap_config.HostapConfig.MODE_11A
+    configurations = [hostap_config.HostapConfig(channel=48, mode=a_mode),
+                      hostap_config.HostapConfig(channel=64, mode=a_mode)]
     host = hosts.create_host(machine)
     job.run_test('network_WiFi_SimpleConnect',
                  host=host,
                  raw_cmdline_args=args,
-                 additional_params=channels)
+                 additional_params=configurations)
 
 
 parallel_simple(run, machines)
diff --git a/server/site_tests/network_WiFi_SimpleConnect/control.check11b b/server/site_tests/network_WiFi_SimpleConnect/control.check11b
index a3f7759..1274866 100644
--- a/server/site_tests/network_WiFi_SimpleConnect/control.check11b
+++ b/server/site_tests/network_WiFi_SimpleConnect/control.check11b
@@ -13,21 +13,19 @@
 """
 
 
+from autotest_lib.server.cros.wlan import hostap_config
+
+
 def run(machine):
-    channels = [{'channel': '2412',
-                 'mode': '11b',
-                 'ssid_suffix': 'ch1'},
-                {'channel': '2437',
-                 'mode': '11b',
-                 'ssid_suffix': 'ch6'},
-                {'channel': '2462',
-                 'mode': '11b',
-                 'ssid_suffix': 'ch11'} ]
+    b_mode = hostap_config.HostapConfig.MODE_11B
+    configurations = [hostap_config.HostapConfig(channel=1, mode=b_mode),
+                      hostap_config.HostapConfig(channel=6, mode=b_mode),
+                      hostap_config.HostapConfig(channel=11, mode=b_mode)]
     host = hosts.create_host(machine)
     job.run_test('network_WiFi_SimpleConnect',
                  host=host,
                  raw_cmdline_args=args,
-                 additional_params=channels)
+                 additional_params=configurations)
 
 
 parallel_simple(run, machines)
diff --git a/server/site_tests/network_WiFi_SimpleConnect/control.check11g b/server/site_tests/network_WiFi_SimpleConnect/control.check11g
index 0f83c22..0f23ffa 100644
--- a/server/site_tests/network_WiFi_SimpleConnect/control.check11g
+++ b/server/site_tests/network_WiFi_SimpleConnect/control.check11g
@@ -13,24 +13,19 @@
 """
 
 
+from autotest_lib.server.cros.wlan import hostap_config
+
+
 def run(machine):
-    channels = [{'channel': '2412',
-                 'mode': '11g',
-                 'pureg': None,
-                 'ssid_suffix': 'ch1'},
-                {'channel': '2437',
-                 'mode': '11g',
-                 'pureg': None,
-                 'ssid_suffix': 'ch6'},
-                {'channel': '2462',
-                 'mode': '11g',
-                 'pureg': None,
-                 'ssid_suffix': 'ch11'}]
+    g_mode = hostap_config.HostapConfig.MODE_11G
+    configurations = [hostap_config.HostapConfig(channel=1, mode=g_mode),
+                      hostap_config.HostapConfig(channel=6, mode=g_mode),
+                      hostap_config.HostapConfig(channel=11, mode=g_mode)]
     host = hosts.create_host(machine)
     job.run_test('network_WiFi_SimpleConnect',
                  host=host,
                  raw_cmdline_args=args,
-                 additional_params=channels)
+                 additional_params=configurations)
 
 
 parallel_simple(run, machines)
diff --git a/server/site_tests/network_WiFi_SimpleConnect/control.check24HT20 b/server/site_tests/network_WiFi_SimpleConnect/control.check24HT20
index 7e526e7..e1b4fef 100644
--- a/server/site_tests/network_WiFi_SimpleConnect/control.check24HT20
+++ b/server/site_tests/network_WiFi_SimpleConnect/control.check24HT20
@@ -13,24 +13,22 @@
 """
 
 
+from autotest_lib.server.cros.wlan import hostap_config
+
+
 def run(machine):
-    channels = [{'channel': '2412',
-                 'ht20': None,
-                 'puren': None,
-                 'ssid_suffix': 'ch1'},
-                {'channel': '2437',
-                 'ht20': None,
-                 'puren': None,
-                 'ssid_suffix': 'ch6'},
-                {'channel': '2462',
-                 'ht20': None,
-                 'puren': None,
-                 'ssid_suffix': 'ch11'}]
+    caps = [hostap_config.HostapConfig.N_CAPABILITY_GREENFIELD,
+            hostap_config.HostapConfig.N_CAPABILITY_HT20]
+    n = hostap_config.HostapConfig.MODE_11N_PURE
+    configurations = [
+            hostap_config.HostapConfig(channel=1, mode=n, n_capabilities=caps),
+            hostap_config.HostapConfig(channel=6, mode=n, n_capabilities=caps),
+            hostap_config.HostapConfig(channel=11, mode=n, n_capabilities=caps)]
     host = hosts.create_host(machine)
     job.run_test('network_WiFi_SimpleConnect',
                  host=host,
                  raw_cmdline_args=args,
-                 additional_params=channels)
+                 additional_params=configurations)
 
 
 parallel_simple(run, machines)
diff --git a/server/site_tests/network_WiFi_SimpleConnect/control.check24HT40 b/server/site_tests/network_WiFi_SimpleConnect/control.check24HT40
index 9d0aaae..d968c1a 100644
--- a/server/site_tests/network_WiFi_SimpleConnect/control.check24HT40
+++ b/server/site_tests/network_WiFi_SimpleConnect/control.check24HT40
@@ -13,13 +13,21 @@
 """
 
 
+
+
 def run(machine):
-    channels = [{'channel': '2437', 'ht40': None, 'puren': None}]
+    from autotest_lib.server.cros.wlan import hostap_config
+    caps = [hostap_config.HostapConfig.N_CAPABILITY_GREENFIELD,
+            hostap_config.HostapConfig.N_CAPABILITY_HT40]
+    n = hostap_config.HostapConfig.MODE_11N_PURE
+    configurations = [hostap_config.HostapConfig(frequency=2437,
+                                                 mode=n,
+                                                 n_capabilities=caps)]
     host = hosts.create_host(machine)
     job.run_test('network_WiFi_SimpleConnect',
                  host=host,
                  raw_cmdline_args=args,
-                 additional_params=channels)
+                 additional_params=configurations)
 
 
 parallel_simple(run, machines)
diff --git a/server/site_tests/network_WiFi_SimpleConnect/control.check5HT20 b/server/site_tests/network_WiFi_SimpleConnect/control.check5HT20
index 6b49ef5..52aef3c 100644
--- a/server/site_tests/network_WiFi_SimpleConnect/control.check5HT20
+++ b/server/site_tests/network_WiFi_SimpleConnect/control.check5HT20
@@ -13,13 +13,20 @@
 """
 
 
+from autotest_lib.server.cros.wlan import hostap_config
+
+
 def run(machine):
-    channels = [{'channel': '5240', 'ht20': None, 'puren': None}]
+    caps = [hostap_config.HostapConfig.N_CAPABILITY_GREENFIELD,
+            hostap_config.HostapConfig.N_CAPABILITY_HT20]
+    n = hostap_config.HostapConfig.MODE_11N_PURE
+    configurations = [hostap_config.HostapConfig(frequency=5240, mode=n,
+                                                 n_capabilities=caps)]
     host = hosts.create_host(machine)
     job.run_test('network_WiFi_SimpleConnect',
                  host=host,
                  raw_cmdline_args=args,
-                 additional_params=channels)
+                 additional_params=configurations)
 
 
 parallel_simple(run, machines)
diff --git a/server/site_tests/network_WiFi_SimpleConnect/control.check5HT40 b/server/site_tests/network_WiFi_SimpleConnect/control.check5HT40
index 2b00af3..d635c7c 100644
--- a/server/site_tests/network_WiFi_SimpleConnect/control.check5HT40
+++ b/server/site_tests/network_WiFi_SimpleConnect/control.check5HT40
@@ -14,13 +14,21 @@
 """
 
 
+from autotest_lib.server.cros.wlan import hostap_config
+
+
 def run(machine):
-    channels = [{'channel': '5240', 'ht40-': None, 'puren': None}]
+    caps = [hostap_config.HostapConfig.N_CAPABILITY_GREENFIELD,
+            hostap_config.HostapConfig.N_CAPABILITY_HT40_MINUS]
+    n = hostap_config.HostapConfig.MODE_11N_PURE
+    configurations = [hostap_config.HostapConfig(frequency=5240,
+                                                 mode=n,
+                                                 n_capabilities=caps)]
     host = hosts.create_host(machine)
     job.run_test('network_WiFi_SimpleConnect',
                  host=host,
                  raw_cmdline_args=args,
-                 additional_params=channels)
+                 additional_params=configurations)
 
 
 parallel_simple(run, machines)
diff --git a/server/site_tests/network_WiFi_SimpleConnect/network_WiFi_SimpleConnect.py b/server/site_tests/network_WiFi_SimpleConnect/network_WiFi_SimpleConnect.py
index 0fa8fad..a584474 100644
--- a/server/site_tests/network_WiFi_SimpleConnect/network_WiFi_SimpleConnect.py
+++ b/server/site_tests/network_WiFi_SimpleConnect/network_WiFi_SimpleConnect.py
@@ -17,13 +17,13 @@
         @param additional_params list of dicts describing router configs.
 
         """
-        self._channels = additional_params
+        self._configurations = additional_params
 
 
     def run_once_impl(self):
         """Sets up a router, connects to it, pings it, and repeats."""
-        for channel in self._channels:
-            self.context.router.config(channel)
+        for configuration in self._configurations:
+            self.context.configure(configuration)
             assoc_params = xmlrpc_datatypes.AssociationParameters()
             assoc_params.ssid = self.context.router.get_ssid()
             self.assert_connect_wifi(assoc_params)