autotest: Refactor WiFi security credentials into per type classes

This lets us be a little more abstract about the types of security
credentials we support in testing, and concentrates the relevant logic
into one place.

TEST=Passes both:
./run_remote_tests.sh --remote=chromeos1-shelf1-host3 check11b
./run_remote_tests.sh --remote=chromeos1-shelf1-host3 checkStaticWEP40
which indicates this works for both open and WEP security types.
BUG=chromium:249096
CQ-DEPEND=CL:58588

Change-Id: I085665db9bead45fcd8d0b94a136b9dac2f646da
Reviewed-on: https://gerrit.chromium.org/gerrit/58589
Tested-by: Christopher Wiley <wiley@chromium.org>
Reviewed-by: Paul Stewart <pstew@chromium.org>
Commit-Queue: Christopher Wiley <wiley@chromium.org>
diff --git a/client/common_lib/cros/network/shill_xmlrpc_server.py b/client/common_lib/cros/network/shill_xmlrpc_server.py
index 9bcfc2b..514b690 100755
--- a/client/common_lib/cros/network/shill_xmlrpc_server.py
+++ b/client/common_lib/cros/network/shill_xmlrpc_server.py
@@ -112,7 +112,7 @@
         raw = self._shill_proxy.connect_to_wifi_network(
                 params.ssid,
                 params.security,
-                params.psk,
+                params.security_parameters,
                 params.save_credentials,
                 # FIXME(wiley): comment out b/c this breaks RvR tests
                 #station_type=params.station_type,
diff --git a/client/common_lib/cros/network/xmlrpc_datatypes.py b/client/common_lib/cros/network/xmlrpc_datatypes.py
index 2fbd581..2483f8d 100644
--- a/client/common_lib/cros/network/xmlrpc_datatypes.py
+++ b/client/common_lib/cros/network/xmlrpc_datatypes.py
@@ -3,12 +3,12 @@
 # found in the LICENSE file.
 
 from autotest_lib.client.common_lib.cros import xmlrpc_datatypes
+from autotest_lib.client.common_lib.cros.network import xmlrpc_security_types
+
 
 class AssociationParameters(xmlrpc_datatypes.XmlRpcStruct):
     """Describes parameters used in WiFi connection attempts."""
 
-    DEFAULT_SECURITY = 'none'
-    DEFAULT_PSK = ''
     DEFAULT_DISCOVERY_TIMEOUT = 15
     DEFAULT_ASSOCIATION_TIMEOUT = 15
     DEFAULT_CONFIGURATION_TIMEOUT = 15
@@ -17,6 +17,18 @@
     # Mode for certain kinds of p2p networks like old Android phone hotspots.
     STATION_TYPE_IBSS = 'ibss'
 
+    @property
+    def security(self):
+        """@return string security type for this network."""
+        return self.security_config.security
+
+
+    @property
+    def security_parameters(self):
+        """@return dict of service property/value pairs related to security."""
+        return self.security_config.get_shill_service_properties()
+
+
     def __init__(self, serialized=None):
         """Construct an AssociationParameters.
 
@@ -28,10 +40,11 @@
             serialized = {}
         # The network to connect to (e.g. 'GoogleGuest').
         self.ssid = serialized.get('ssid', None)
-        # Which encryption to use (e.g. 'wpa').
-        self.security = serialized.get('security', self.DEFAULT_SECURITY)
-        # Passphrase for this network (e.g. 'password123').
-        self.psk = serialized.get('psk', self.DEFAULT_PSK)
+        # Marshall our bundle of security configuration.
+        serialized_security_config = serialized.get('security_config', {})
+        self.security_config = (
+                xmlrpc_security_types.deserialize(serialized_security_config) or
+                xmlrpc_security_types.SecurityConfig())
         # Max delta in seconds between XMLRPC call to connect in the proxy
         # and when shill finds the service.  Presumably callers call connect
         # quickly after configuring the AP so that this is an approximation
diff --git a/client/common_lib/cros/network/xmlrpc_security_types.py b/client/common_lib/cros/network/xmlrpc_security_types.py
new file mode 100644
index 0000000..d5ccecb
--- /dev/null
+++ b/client/common_lib/cros/network/xmlrpc_security_types.py
@@ -0,0 +1,97 @@
+# 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 sys
+
+from autotest_lib.client.common_lib.cros import xmlrpc_datatypes
+
+TYPE_KEY = 'security_class_type_key'
+
+def deserialize(serialized):
+    """Deserialize a SecurityConfig.
+
+    Because Python XMLRPC doesn't understand anything more than basic
+    types, we're forced to reinvent type marshalling.  This is one way
+    to do it.
+
+    @param serialized dict representing a serialized SecurityConfig.
+    @return a SecurityConfig object built from |serialized|.
+
+    """
+    if TYPE_KEY not in serialized:
+        return None
+
+    return getattr(sys.modules[__name__],
+                   serialized[TYPE_KEY])(serialized=serialized)
+
+
+class SecurityConfig(xmlrpc_datatypes.XmlRpcStruct):
+    """Abstracts the security configuration for a WiFi network.
+
+    This bundle of credentials can be passed to both HostapConfig and
+    AssociationParameters so that both shill and hostapd can set up and connect
+    to an encrypted WiFi network.  By default, we'll assume we're connecting
+    to an open network.
+
+    """
+    SERVICE_PROPERTY_PASSPHRASE = 'Passphrase'
+
+    def __init__(self, serialized=None, security=None):
+        super(SecurityConfig, self).__init__()
+        setattr(self, TYPE_KEY, self.__class__.__name__)
+        if serialized is None:
+            serialized = {}
+        # Default to no security.
+        self.security = serialized.get('security', security or 'none')
+
+
+    def get_hostapd_config(self):
+        """@return dict fragment of hostapd configuration for security."""
+        return {}
+
+
+    def get_shill_service_properties(self):
+        """@return dict of shill service properties."""
+        return {}
+
+
+    def __repr__(self):
+        return '%s(security=%r)' % (self.__class__.__name__, self.security)
+
+
+class WEPConfig(SecurityConfig):
+    """Abstracts security configuration for a WiFi network using static WEP."""
+
+    def __init__(self, serialized=None, wep_keys=None, wep_default_key=None):
+        super(WEPConfig, self).__init__(serialized=serialized, security='wep')
+        if serialized is None:
+            serialized = {}
+        self.wep_keys = serialized.get('wep_keys', wep_keys or [])
+        self.wep_default_key = serialized.get('wep_default_key',
+                                              wep_default_key or 0)
+        if self.wep_keys and len(self.wep_keys) > 4:
+            raise error.TestFail('More than 4 WEP keys specified (%d).' %
+                                 len(self.wep_keys))
+
+
+    def get_hostapd_config(self):
+        """@return dict fragment of hostapd configuration for security."""
+        ret = {}
+        for idx,key in enumerate(self.wep_keys):
+            ret['wep_key%d' % idx] = key
+        ret['wep_default_key'] = self.wep_default_key
+        return ret
+
+
+    def get_shill_service_properties(self):
+        """@return dict of shill service properties."""
+        return {self.SERVICE_PROPERTY_PASSPHRASE: '%d:%s' % (
+                        self.wep_default_key,
+                        self.wep_keys[self.wep_default_key])}
+
+
+    def __repr__(self):
+        return '%s(wep_keys=%r, wep_default_key=%r)' % (self.__class__.__name__,
+                                                        self.wep_keys,
+                                                        self.wep_default_key)
diff --git a/server/cros/wlan/hostap_config.py b/server/cros/wlan/hostap_config.py
index e2ea7a5..0428a15 100644
--- a/server/cros/wlan/hostap_config.py
+++ b/server/cros/wlan/hostap_config.py
@@ -5,6 +5,7 @@
 import logging
 
 from autotest_lib.client.common_lib import error
+from autotest_lib.client.common_lib.cros.network import xmlrpc_security_types
 
 
 class HostapConfig(object):
@@ -116,7 +117,7 @@
     def __init__(self, mode=None, channel=None, frequency=None,
                  n_capabilities=None, hide_ssid=None, beacon_interval=None,
                  dtim_period=None, frag_threshold=None, ssid=None, bssid=None,
-                 force_wmm=None, wep_keys=None, wep_default_key=None):
+                 force_wmm=None, security_config=None):
         """Construct a HostapConfig.
 
         You may specify channel or frequency, but not both.  Both options
@@ -135,8 +136,7 @@
         @param bssid string like 00:11:22:33:44:55.
         @param force_wmm True if we should force WMM on, False if we should
             force it off, None if we shouldn't force anything.
-        @param wep_keys list of string wep keys
-        @param wep_default_key int index into wep_keys to use as default key.
+        @param security_config SecurityConfig object.
 
         """
         super(HostapConfig, self).__init__()
@@ -223,21 +223,15 @@
         self.bssid = bssid
         if force_wmm is not None:
             self.wmm_enabled = force_wmm
-        if wep_keys and len(wep_keys) > 4:
-            raise error.TestFail('More than 4 WEP keys specified (%d).' %
-                                 len(self.wep_keys))
-
-        self.wep_keys = wep_keys
-        self.wep_default_key = wep_default_key
-        if self.wep_default_key is None and self.wep_keys:
-            self.wep_default_key = 0
+        self.security_config = (security_config or
+                                xmlrpc_security_types.SecurityConfig())
 
 
     def __repr__(self):
         return ('%s(mode=%r, channel=%r, frequency=%r, '
                 'n_capabilities=%r, hide_ssid=%r, beacon_interval=%r, '
                 'dtim_period=%r, frag_threshold=%r, ssid=%r, bssid=%r, '
-                'wmm_enabled=%r, wep_keys=%r, wep_default_key=%r)' % (
+                'wmm_enabled=%r, security_config=%r)' % (
                         self.__class__.__name__,
                         self.hw_mode,
                         self.channel,
@@ -250,8 +244,7 @@
                         self.ssid,
                         self.bssid,
                         self.wmm_enabled,
-                        self.wep_keys,
-                        self.wep_default_key))
+                        self.security_config))
 
 
     def get_ssid(self, default_ssid):
@@ -264,10 +257,5 @@
         return self.ssid or (default_ssid + self.ssid_suffix)[-32:]
 
 
-    def get_shill_compatible_psk(self):
-        """@return string shill psk for the encryption in this configuration."""
-        if self.wep_keys:
-            return '%d:%s' % (self.wep_default_key,
-                              self.wep_keys[self.wep_default_key])
-
-        raise error.TestFail('Failed to build shill compatible psk.')
+    def get_security_hostapd_conf(self):
+        return self.security_config.get_hostapd_config()
diff --git a/server/site_linux_router.py b/server/site_linux_router.py
index a089f26..3af1d05 100644
--- a/server/site_linux_router.py
+++ b/server/site_linux_router.py
@@ -277,10 +277,7 @@
             conf['dtim_period'] = configuration.dtim_period
         if configuration.frag_threshold:
             conf['fragm_threshold'] = configuration.frag_threshold
-        if configuration.wep_keys:
-            for idx,key in enumerate(configuration.wep_keys):
-                conf['wep_key%d' % idx] = key
-            conf['wep_default_key'] = configuration.wep_default_key
+        conf.update(configuration.get_security_hostapd_conf())
 
         self.start_hostapd(conf, {})
         # Configure transmit power
diff --git a/server/site_tests/network_WiFi_SimpleConnect/control.checkStaticWEP40 b/server/site_tests/network_WiFi_SimpleConnect/control.checkStaticWEP40
index 15b6510..a8abf8e 100644
--- a/server/site_tests/network_WiFi_SimpleConnect/control.checkStaticWEP40
+++ b/server/site_tests/network_WiFi_SimpleConnect/control.checkStaticWEP40
@@ -14,24 +14,26 @@
 WEP open system authentication with 40-bit pre-shared keys.
 """
 
+import copy
 
 from autotest_lib.client.common_lib.cros.network import xmlrpc_datatypes
+from autotest_lib.client.common_lib.cros.network import xmlrpc_security_types
 from autotest_lib.server.cros.wlan import hostap_config
 
 
 def run(machine):
     wep_keys = ['0123456789', '89abcdef01', '9876543210', 'fedcba9876']
-    print wep_keys
+    master_wep_config = xmlrpc_security_types.WEPConfig(wep_keys=wep_keys)
     configurations = []
-    for idx,key in enumerate(wep_keys):
+    for i in range(len(wep_keys)):
+        wep_config = copy.copy(master_wep_config)
+        wep_config.wep_default_key = i
         ap_config = hostap_config.HostapConfig(
                 frequency=2412,
                 mode=hostap_config.HostapConfig.MODE_11G,
-                wep_keys=wep_keys,
-                wep_default_key=idx)
+                security_config=wep_config)
         assoc_params = xmlrpc_datatypes.AssociationParameters()
-        assoc_params.security = 'wep'
-        assoc_params.psk = ap_config.get_shill_compatible_psk()
+        assoc_params.security_config = wep_config
         configurations.append((ap_config, assoc_params))
     host = hosts.create_host(machine)
     job.run_test('network_WiFi_SimpleConnect',