privetd: Add end to end WiFi bootstrapping test

For now, this test merely sets up a router that we'll later attempt to
connect to, and confirms that the DUT is setting up its AP with an SSID
in the format that we expect.  It ends by connecting the router to the
DUTs softAP as a client.

Some work is done to add additional configuration options for
the privetd device model, class, and name.  This allows us
to pick out most of the characters in the SSID.

Some additional work is done to allow the router to connect to
the unencrypted bootstrapping network as a client.

BUG=brillo:8
TEST=This test passes up to the final TestNA indicating that it is
not complete.  network_WiFi_TDLSPing still passes after changes to
add_connected_peer()

Change-Id: Ie46dad4c3442fadf38b7923334d522fb0113037d
Reviewed-on: https://chromium-review.googlesource.com/245462
Reviewed-by: Christopher Wiley <wiley@chromium.org>
Tested-by: Christopher Wiley <wiley@chromium.org>
Commit-Queue: Christopher Wiley <wiley@chromium.org>
diff --git a/client/common_lib/cros/tendo/privetd_helper.py b/client/common_lib/cros/tendo/privetd_helper.py
index b278907..dff95e7 100644
--- a/client/common_lib/cros/tendo/privetd_helper.py
+++ b/client/common_lib/cros/tendo/privetd_helper.py
@@ -4,6 +4,8 @@
 
 import json
 import logging
+import random
+import string
 import time
 
 from autotest_lib.client.common_lib import error
@@ -34,6 +36,8 @@
 DEFAULT_HTTP_PORT = 8080
 DEFAULT_HTTPS_PORT = 8081
 
+DEFAULT_DEVICE_CLASS = 'AB'  # = development board
+DEFAULT_DEVICE_MODEL_ID = 'AAA'  # = unregistered
 
 def privetd_is_installed(host=None):
     """Check if the privetd binary is installed.
@@ -55,6 +59,8 @@
 class PrivetdConfig(object):
     """An object that knows how to restart privetd in various configurations."""
 
+    _num_names_generated = 0
+
     @staticmethod
     def naive_restart(host=None):
         """Restart privetd without modifying any settings.
@@ -69,6 +75,20 @@
         run('start privetd')
 
 
+    @staticmethod
+    def build_unique_device_name():
+        """@return a test-unique name for a Privet device."""
+        RAND_CHARS = string.ascii_lowercase + string.digits
+        NUM_RAND_CHARS = 4
+        rand_token = ''.join([random.choice(RAND_CHARS)
+                              for _ in range(NUM_RAND_CHARS)])
+        name = 'CrOS Core %s_%2d' % (rand_token,
+                                     PrivetdConfig._num_names_generated)
+        PrivetdConfig._num_names_generated += 1
+        logging.debug('Generated unique device name %s', name)
+        return name
+
+
     def __init__(self,
                  wifi_bootstrap_mode=BOOTSTRAP_CONFIG_DISABLED,
                  gcd_bootstrap_mode=BOOTSTRAP_CONFIG_DISABLED,
@@ -82,7 +102,10 @@
                  http_port=DEFAULT_HTTP_PORT,
                  https_port=DEFAULT_HTTPS_PORT,
                  device_whitelist=None,
-                 disable_pairing_security=False):
+                 disable_pairing_security=False,
+                 device_name=None,
+                 device_class=DEFAULT_DEVICE_CLASS,
+                 device_model_id=DEFAULT_DEVICE_MODEL_ID):
         """Construct a privetd configuration.
 
         @param wifi_bootstrap_mode: one of BOOTSTRAP_CONFIG_* above.
@@ -106,6 +129,10 @@
                 consider exclusively for connectivity monitoring (e.g.
                 ['eth0', 'wlan0']).
         @param disable_security: bool True to disable pairing security
+        @param device_name: string Device name.  A 'unique' device name
+                will be generated if this is unspecified.
+        @param device_class: string device class, a two character string.
+        @param device_model_id: string device model ID, a 3 character string.
 
         """
         self.wifi_bootstrap_mode = wifi_bootstrap_mode
@@ -121,6 +148,10 @@
         self.https_port = https_port
         self.device_whitelist = device_whitelist
         self.disable_pairing_security = disable_pairing_security
+        self.device_name = (device_name or
+                            PrivetdConfig.build_unique_device_name())
+        self.device_class = device_class
+        self.device_model_id = device_model_id
 
 
     def restart_with_config(self, host=None):
@@ -138,6 +169,9 @@
                 'monitor_timeout_seconds': self.monitor_timeout_seconds,
                 'connect_timeout_seconds': self.connect_timeout_seconds,
                 'bootstrap_timeout_seconds': self.bootstrap_timeout_seconds,
+                'device_class': self.device_class,
+                'device_model_id': self.device_model_id,
+                'device_name': self.device_name,
         }
         flag_list = []
         flag_list.append('PRIVETD_LOG_LEVEL=%d' % self.log_verbosity)
@@ -167,6 +201,51 @@
         run('start privetd %s' % ' '.join(flag_list))
 
 
+    def is_softap_ssid(self, ssid):
+        """Check whether |ssid| could represent privetd with this config.
+
+        @param ssid: string SSID of network.
+        @return True iff this could be a network started by privetd configured
+                with settings in |self|.
+
+        """
+        logging.debug('Checking whether softAP SSID "%s" '
+                      'looks like a privet SSID', ssid)
+
+        if len(ssid) > 31:
+            logging.debug('SSID was too long')
+            return False
+        if ssid.find('.') < 0:
+            logging.debug('Missing SSID separator')
+            return False
+        name, suffix = ssid.split('.', 1)
+        if len(suffix) != 10:
+            logging.debug('Suffix was %d characters, rather than 10.',
+                          len(suffix))
+            return False
+        device_class = suffix[0:2]
+        model_id = suffix[2:5]
+        flags = suffix[5:7]
+        version = suffix[7:10]
+        if version != 'prv':
+            logging.debug('Suffix should end with prv, not %s', suffix)
+            return False
+        if self.device_class is not None and device_class != self.device_class:
+            logging.debug('Expected device_class=%s, but got %s',
+                          self.device_class, device_class)
+            return False
+        if self.device_model_id and model_id != self.device_model_id:
+            logging.debug('Expected model_id=%s, but got %s',
+                          self.device_model_id, model_id)
+            return False
+        # TODO(wiley) Add flag support
+        if not name.startswith(self.device_name):
+            logging.debug('Expected SSID to start with "%s" but got "%s".',
+                          self.device_name, name)
+            return False
+        return True
+
+
 class PrivetdHelper(object):
     """Delegate class containing logic useful with privetd."""
 
diff --git a/server/site_linux_router.py b/server/site_linux_router.py
index 0806f1c..4c03702 100644
--- a/server/site_linux_router.py
+++ b/server/site_linux_router.py
@@ -843,6 +843,33 @@
         hostap_conf = self.hostapd_instances[instance].config_dict
         frequency = hostap_config.HostapConfig.get_frequency_for_channel(
                 hostap_conf['channel'])
+        self.configure_managed_station(
+                ssid, frequency, self.local_peer_ip_address(instance))
+        interface = self.station_instances[0].interface
+        # Since we now have two network interfaces connected to the same
+        # network, we need to disable the kernel's protection against
+        # incoming packets to an "unexpected" interface.
+        self.router.run('echo 2 > /proc/sys/net/ipv4/conf/%s/rp_filter' %
+                        interface)
+
+        # Similarly, we'd like to prevent the hostap interface from
+        # replying to ARP requests for the peer IP address and vice
+        # versa.
+        self.router.run('echo 1 > /proc/sys/net/ipv4/conf/%s/arp_ignore' %
+                        interface)
+        self.router.run('echo 1 > /proc/sys/net/ipv4/conf/%s/arp_ignore' %
+                        hostap_conf['interface'])
+
+
+    def configure_managed_station(self, ssid, frequency, ip_addr):
+        """Configure a router interface to connect as a client to a network.
+
+        @param ssid: string SSID of network to join.
+        @param frequency: int frequency required to join the network.
+        @param ip_addr: IP address to assign to this interface
+                        (e.g. '192.168.1.200').
+
+        """
         interface = self.get_wlanif(frequency, 'managed')
 
         # TODO(pstew): Configure other bits like PSK, 802.11n if tests
@@ -872,23 +899,7 @@
 
         # Assign an IP address to this interface.
         self.router.run('%s addr add %s/24 dev %s' %
-                        (self.cmd_ip, self.local_peer_ip_address(instance),
-                         interface))
-
-        # Since we now have two network interfaces connected to the same
-        # network, we need to disable the kernel's protection against
-        # incoming packets to an "unexpected" interface.
-        self.router.run('echo 2 > /proc/sys/net/ipv4/conf/%s/rp_filter' %
-                        interface)
-
-        # Similarly, we'd like to prevent the hostap interface from
-        # replying to ARP requests for the peer IP address and vice
-        # versa.
-        self.router.run('echo 1 > /proc/sys/net/ipv4/conf/%s/arp_ignore' %
-                        interface)
-        self.router.run('echo 1 > /proc/sys/net/ipv4/conf/%s/arp_ignore' %
-                        hostap_conf['interface'])
-
+                        (self.cmd_ip, ip_addr, interface))
         self.station_instances.append(
                 StationInstance(ssid=ssid, interface=interface,
                                 dev_type='managed'))
diff --git a/server/site_tests/privetd_PrivetSetupFlow/control b/server/site_tests/privetd_PrivetSetupFlow/control
index 11d1e33..bd7fe16 100644
--- a/server/site_tests/privetd_PrivetSetupFlow/control
+++ b/server/site_tests/privetd_PrivetSetupFlow/control
@@ -13,9 +13,15 @@
 
 """
 
+from autotest_lib.client.common_lib import utils
+
 def run(machine):
+    CMDLINE_ROUTER_HOSTNAME = 'router_addr'
+    cmdline_args = utils.args_to_dict(args)
+    router_hostname = cmdline_args.get(CMDLINE_ROUTER_HOSTNAME, None)
     job.run_test('privetd_PrivetSetupFlow',
-                 host=hosts.create_host(machine))
+                 host=hosts.create_host(machine),
+                 router_hostname=router_hostname)
 
 
 parallel_simple(run, machines)
diff --git a/server/site_tests/privetd_PrivetSetupFlow/privetd_PrivetSetupFlow.py b/server/site_tests/privetd_PrivetSetupFlow/privetd_PrivetSetupFlow.py
index 39181ce..68471a1 100644
--- a/server/site_tests/privetd_PrivetSetupFlow/privetd_PrivetSetupFlow.py
+++ b/server/site_tests/privetd_PrivetSetupFlow/privetd_PrivetSetupFlow.py
@@ -2,23 +2,99 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import time
+
+from autotest_lib.client.common_lib import error
+from autotest_lib.client.common_lib.cros.network import interface
+from autotest_lib.client.common_lib.cros.network import iw_runner
+from autotest_lib.client.common_lib.cros.network import netblock
+from autotest_lib.client.common_lib.cros.network import ping_runner
+from autotest_lib.client.common_lib.cros.network import xmlrpc_security_types
 from autotest_lib.client.common_lib.cros.tendo import privetd_helper
+from autotest_lib.server import site_linux_router
 from autotest_lib.server import test
+from autotest_lib.server.cros.network import hostap_config
+
+PASSPHRASE = 'chromeos'
+PRIVET_AP_STARTUP_TIMEOUT_SECONDS = 30
+
 
 class privetd_PrivetSetupFlow(test.test):
     """This test validates the privet pairing/authentication/setup flow."""
     version = 1
 
-    def warmup(self, host):
-        config =privetd_helper.PrivetdConfig(log_verbosity=3, enable_ping=True)
-        config.restart_with_config(host=host)
+    def warmup(self, host, router_hostname=None):
+        self._router = None
+        self._privet_config = privetd_helper.PrivetdConfig(
+                log_verbosity=3,
+                enable_ping=True,
+                wifi_bootstrap_mode=privetd_helper.BOOTSTRAP_CONFIG_AUTOMATIC,
+                disable_pairing_security=True)
+        self._privet_config.restart_with_config(host=host)
+        self._router = site_linux_router.build_router_proxy(
+                test_name=self.__class__.__name__,
+                client_hostname=host.hostname,
+                router_addr=router_hostname)
 
 
     def cleanup(self, host):
         privetd_helper.PrivetdConfig.naive_restart(host=host)
+        if self._router is not None:
+            self._router.close()
 
 
     def run_once(self, host):
         helper = privetd_helper.PrivetdHelper(host=host)
         helper.ping_server()  # Make sure the server is up and running.
-        # TODO(avakulenko): implement this
+
+        # We should see a bootstrapping network broadcasting from the device.
+        scan_interface = self._router.get_wlanif(2437, 'managed')
+        self._router.host.run('%s link set %s up' %
+                              (self._router.cmd_ip, scan_interface))
+        start_time = time.time()
+        privet_bss = None
+        while time.time() - start_time < PRIVET_AP_STARTUP_TIMEOUT_SECONDS:
+            bss_list = self._router.iw_runner.scan(scan_interface)
+            for bss in bss_list or []:
+                if self._privet_config.is_softap_ssid(bss.ssid):
+                    privet_bss = bss
+        if privet_bss is None:
+            raise error.TestFail('Device did not start soft AP in time.')
+        self._router.release_interface(scan_interface)
+
+        # Get the netblock of the interface running the AP.
+        dut_iw_runner = iw_runner.IwRunner(remote_host=host)
+        devs = dut_iw_runner.list_interfaces(desired_if_type='AP')
+        if not devs:
+            raise error.TestFail('No AP devices on DUT?')
+        ap_interface = interface.Interface(devs[0].if_name, host=host)
+        ap_netblock = netblock.from_addr(ap_interface.ipv4_address_and_prefix)
+
+        # Set up an AP on the router in the 5Ghz range with WPA2 security.
+        wpa_config = xmlrpc_security_types.WPAConfig(
+                psk=PASSPHRASE,
+                wpa_mode=xmlrpc_security_types.WPAConfig.MODE_PURE_WPA2,
+                wpa2_ciphers=[xmlrpc_security_types.WPAConfig.CIPHER_CCMP])
+        router_conf = hostap_config.HostapConfig(
+                frequency=5240, security_config=wpa_config,
+                mode=hostap_config.HostapConfig.MODE_11N_PURE)
+        self._router.hostap_configure(router_conf)
+
+        # Connect the other interface on the router to the AP on the client
+        # at a hardcoded IP address.
+        self._router.configure_managed_station(
+                privet_bss.ssid, privet_bss.frequency,
+                ap_netblock.get_addr_in_block(200))
+        self._router.ping(ping_runner.PingConfig(ap_netblock.addr, count=3))
+
+
+        raise error.TestNAError('Finished implemented part of test.')
+        # TODO(wiley): The following:
+        #   Use avahi-browse to look around from the router and find privet
+        #       mDNS records.
+        #   Use ip/port information in those records to call the /info API.
+        #   Then call /pairing/start
+        #   Then call /pairing/finish
+        #   Then call /setup/start
+        #   Confirm that the AP on the client goes down
+        #   Confirm that the client connects to the AP in the 5Ghz range.