autotest: Automatically infer HT40+- in HostapConfig

Allow test writers to specify that they want to configure hostapd with a
strict HT40+ configuration, a strict HT40- configuration, or any
supported HT40 configuration depending on what the channel supports.

TEST=All HT40 tests remain passing.  A pending test that only specifies
an HT40 test correctly picks HT40+ vs HT40- depending on the channel
selected.
BUG=None

Change-Id: I4b3b724c23a0e89e38ee5b672be577ffcdda3df3
Reviewed-on: https://chromium-review.googlesource.com/169904
Reviewed-by: Christopher Wiley <wiley@chromium.org>
Commit-Queue: Christopher Wiley <wiley@chromium.org>
Tested-by: Christopher Wiley <wiley@chromium.org>
diff --git a/server/cros/network/hostap_config.py b/server/cros/network/hostap_config.py
index 14d4b4e..5c16fd4 100644
--- a/server/cros/network/hostap_config.py
+++ b/server/cros/network/hostap_config.py
@@ -68,7 +68,6 @@
     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()
@@ -76,6 +75,27 @@
     N_CAPABILITY_GREENFIELD = object()
     N_CAPABILITY_SGI20 = object()
     N_CAPABILITY_SGI40 = object()
+    ALL_N_CAPABILITIES = [N_CAPABILITY_HT20,
+                          N_CAPABILITY_HT40,
+                          N_CAPABILITY_HT40_PLUS,
+                          N_CAPABILITY_HT40_MINUS,
+                          N_CAPABILITY_GREENFIELD,
+                          N_CAPABILITY_SGI20,
+                          N_CAPABILITY_SGI40]
+
+    # This is a loose merging of the rules for US and EU regulatory
+    # domains as taken from IEEE Std 802.11-2012 Appendix E.  For instance,
+    # we tolerate HT40 in channels 149-161 (not allowed in EU), but also
+    # tolerate HT40+ on channel 7 (not allowed in the US).  We take the loose
+    # definition so that we don't prohibit testing in either domain.
+    HT40_ALLOW_MAP = {N_CAPABILITY_HT40_MINUS: range(6, 14) +
+                                               range(40, 65, 8) +
+                                               range(104, 137, 8) +
+                                               [153, 161],
+                      N_CAPABILITY_HT40_PLUS: range(1, 8) +
+                                              range(36, 61, 8) +
+                                              range(100, 133, 8) +
+                                              [149, 157]}
 
     PMF_SUPPORT_DISABLED = 0
     PMF_SUPPORT_ENABLED = 1
@@ -121,18 +141,51 @@
         @param value: int frequency in MHz.
 
         """
-        if value not in self.CHANNEL_MAP:
+        if value not in self.CHANNEL_MAP or not self.supports_frequency(value):
             raise error.TestFail('Tried to set an invalid frequency: %r.' %
                                  value)
 
-        if not self.supports_frequency(value):
-            raise error.TestFail('Invalid frequency %d for configuration %r' %
-                                 (value, self))
-
         self._frequency = value
 
 
     @property
+    def hostapd_ht_capabilities(self):
+        """@return string suitable for the ht_capab= line in a hostapd config"""
+        ret = []
+        if self.ht40_plus_allowed:
+            ret.append('[HT40+]')
+        elif self.ht40_minus_allowed:
+            ret.append('[HT40-]')
+        if self.N_CAPABILITY_GREENFIELD in self._n_capabilities:
+            logging.warning('Greenfield flag is ignored for hostap...')
+        if self.N_CAPABILITY_SGI20 in self._n_capabilities:
+            ret.append('[SHORT-GI-20]')
+        if self.N_CAPABILITY_SGI40 in self._n_capabilities:
+            ret.append('[SHORT-GI-40]')
+        return ''.join(ret)
+
+
+    @property
+    def ht40_plus_allowed(self):
+        """@return True iff HT40+ is enabled for this configuration."""
+        channel_supported = (self.channel in
+                             self.HT40_ALLOW_MAP[self.N_CAPABILITY_HT40_PLUS])
+        return ((self.N_CAPABILITY_HT40_PLUS in self._n_capabilities or
+                 self.N_CAPABILITY_HT40 in self._n_capabilities) and
+                channel_supported)
+
+
+    @property
+    def ht40_minus_allowed(self):
+        """@return True iff HT40- is enabled for this configuration."""
+        channel_supported = (self.channel in
+                             self.HT40_ALLOW_MAP[self.N_CAPABILITY_HT40_MINUS])
+        return ((self.N_CAPABILITY_HT40_MINUS in self._n_capabilities and
+                 self.N_CAPABILITY_HT40 in self._n_capabilities) and
+                channel_supported)
+
+
+    @property
     def ht_packet_capture_mode(self):
         """Get an appropriate packet capture HT parameter.
 
@@ -149,22 +202,10 @@
         if not self.is_11n:
             return None
 
-        is_plus = '[HT40+]' in self.n_capabilities
-        is_minus = '[HT40-]' in self.n_capabilities
-        if is_plus and is_minus:
-            # TODO(wiley) Apparently, for some channels, there are regulatory
-            #             rules for which side of the channel you may use with
-            #             HT40 mode.  For some channels, HT40 is disabled
-            #             altogether.
-            logging.warning('Packet capture may fail because both HT40+ and '
-                            'HT40- enabled.  hostap will choose one or the '
-                            'other, but we do not know that decision.')
-            return 'HT40-'
-
-        if is_plus:
+        if self.ht40_plus_allowed:
             return 'HT40+'
 
-        if is_minus:
+        if self.ht40_minus_allowed:
             return 'HT40-'
 
         return 'HT20'
@@ -255,10 +296,20 @@
             raise error.TestError('Specify either frequency or channel '
                                   'but not both.')
 
-        if n_capabilities and mode is None:
+        self.wmm_enabled = False
+        unknown_caps = [cap for cap in n_capabilities
+                        if cap not in self.ALL_N_CAPABILITIES]
+        if unknown_caps:
+            raise error.TestError('Unknown capabilities: %r' % unknown_caps)
+
+        self._n_capabilities = set(n_capabilities)
+        if self._n_capabilities:
+            self.wmm_enabled = True
+        if self._n_capabilities and mode is None:
             mode = self.MODE_11N_PURE
         self._mode = mode
 
+        self._frequency = None
         if channel:
             self.channel = channel
         elif frequency:
@@ -270,33 +321,6 @@
             raise error.TestFail('Configured a mode %s that does not support '
                                  'frequency %d' % (self._mode, self.frequency))
 
-        self.wmm_enabled = False
-        self.n_capabilities = set()
-        for cap in n_capabilities:
-            self.wmm_enabled = True
-            if cap == self.N_CAPABILITY_HT40:
-                self.n_capabilities.add('[HT40-]')
-                self.n_capabilities.add('[HT40+]')
-            elif cap == self.N_CAPABILITY_HT40_PLUS:
-                self.n_capabilities.add('[HT40+]')
-            elif cap == self.N_CAPABILITY_HT40_MINUS:
-                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_SGI20:
-                self.n_capabilities.add('[SHORT-GI-20]')
-            elif cap == self.N_CAPABILITY_SGI40:
-                self.n_capabilities.add('[SHORT-GI-40]')
-            elif cap == self.N_CAPABILITY_HT20:
-                # This isn't a real thing.  HT mode implies 20 supported.
-                pass
-            elif cap == self.N_CAPABILITY_WMM:
-                pass
-            else:
-                raise error.TestError('Unknown capability: %r' % cap)
-
         self.hide_ssid = hide_ssid
         self.beacon_interval = beacon_interval
         self.dtim_period = dtim_period
@@ -327,7 +351,7 @@
                         self._mode,
                         self.channel,
                         self.frequency,
-                        self.n_capabilities,
+                        self._n_capabilities,
                         self.hide_ssid,
                         self.beacon_interval,
                         self.dtim_period,
@@ -370,4 +394,24 @@
         if self._mode in (self.MODE_11B, self.MODE_11G) and frequency > 5000:
             return False
 
+        if frequency not in self.CHANNEL_MAP:
+            return False
+
+        channel = self.CHANNEL_MAP[frequency]
+        supports_plus = (channel in
+                         self.HT40_ALLOW_MAP[self.N_CAPABILITY_HT40_PLUS])
+        supports_minus = (channel in
+                          self.HT40_ALLOW_MAP[self.N_CAPABILITY_HT40_MINUS])
+        if (self.N_CAPABILITY_HT40_PLUS in self._n_capabilities and
+                not supports_plus):
+            return False
+
+        if (self.N_CAPABILITY_HT40_MINUS in self._n_capabilities and
+                not supports_minus):
+            return False
+
+        if (self.N_CAPABILITY_HT40 in self._n_capabilities and
+                not supports_plus and not supports_minus):
+            return False
+
         return True
diff --git a/server/site_linux_router.py b/server/site_linux_router.py
index abbfb3d..85a14f3 100644
--- a/server/site_linux_router.py
+++ b/server/site_linux_router.py
@@ -338,7 +338,7 @@
             conf['ignore_broadcast_ssid'] = 1
         if configuration.is_11n:
             conf['ieee80211n'] = 1
-            conf['ht_capab'] = ''.join(configuration.n_capabilities)
+            conf['ht_capab'] = configuration.hostapd_ht_capabilities
         if configuration.wmm_enabled:
             conf['wmm_enabled'] = 1
         if configuration.require_ht: