privetd: Complete end to end bootstrapping test

BUG=brillo:8
TEST=privet_PrivetSetupFlow passes 30/30 times.

Change-Id: Idab9495c52c7db2b9f43cf4f2f1fc81bd9c102bb
Reviewed-on: https://chromium-review.googlesource.com/246662
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/avahi_utils.py b/client/common_lib/cros/avahi_utils.py
index 3eebe71..69e068f 100644
--- a/client/common_lib/cros/avahi_utils.py
+++ b/client/common_lib/cros/avahi_utils.py
@@ -4,6 +4,9 @@
 
 import ConfigParser
 import io
+import collections
+import logging
+import shlex
 import time
 
 from autotest_lib.client.bin import utils
@@ -13,6 +16,12 @@
 BUS_NAME = 'org.freedesktop.Avahi'
 INTERFACE_SERVER = 'org.freedesktop.Avahi.Server'
 
+ServiceRecord = collections.namedtuple(
+        'ServiceRecord',
+        ['interface', 'protocol', 'name', 'record_type', 'domain',
+         'hostname', 'address', 'port', 'txt'])
+
+
 def avahi_config(options, src_file='/etc/avahi/avahi-daemon.conf', host=None):
     """Creates a temporary avahi-daemon.conf file with the specified changes.
 
@@ -146,3 +155,46 @@
             BUS_NAME, INTERFACE_SERVER, '/', 'GetDomainName',
             host=host, timeout_seconds=2, tolerate_failures=True)
     return None if result is None else result.response
+
+
+def avahi_browse(host=None, ignore_local=True):
+    """Browse mDNS service records with avahi-browse.
+
+    Some example avahi-browse output (lines are wrapped for readability):
+
+    localhost ~ # avahi-browse -tarlp
+    +;eth1;IPv4;E58E8561-3BCA-4910-ABC7-BD8779D7D761;_serbus._tcp;local
+    +;eth1;IPv4;E58E8561-3BCA-4910-ABC7-BD8779D7D761;_privet._tcp;local
+    =;eth1;IPv4;E58E8561-3BCA-4910-ABC7-BD8779D7D761;_serbus._tcp;local;\
+        9bcd92bbc1f91f2ee9c9b2e754cfd22e.local;172.22.23.237;0;\
+        "ver=1.0" "services=privet" "id=11FB0AD6-6C87-433E-8ACB-0C68EE78CDBD"
+    =;eth1;IPv4;E58E8561-3BCA-4910-ABC7-BD8779D7D761;_privet._tcp;local;\
+        9bcd92bbc1f91f2ee9c9b2e754cfd22e.local;172.22.23.237;8080;\
+        "ty=Unnamed Device" "txtvers=3" "services=_camera" "model_id=///" \
+        "id=FEE9B312-1F2B-4B9B-813C-8482FA75E0DB" "flags=AB" "class=BB"
+
+    @param host: An optional host object if running against a remote host.
+    @param ignore_local: boolean True to ignore local service records.
+    @return list of ServiceRecord objects parsed from output.
+
+    """
+    run = utils.run if host is None else host.run
+    flags = ['--terminate',  # Terminate after looking for a short time.
+             '--all',  # Show all services, regardless of type.
+             '--resolve',  # Resolve the services discovered.
+             '--parsable',  # Print service records in a parsable format.
+    ]
+    if ignore_local:
+        flags.append('--ignore-local')
+    result = run('avahi-browse %s' % ' '.join(flags))
+    records = []
+    for line in result.stdout.strip().splitlines():
+        parts = line.split(';')
+        if parts[0] == '+':
+            # Skip it, just parse the resolved record.
+            continue
+        # Do minimal parsing of the TXT record.
+        parts[-1] = shlex.split(parts[-1])
+        records.append(ServiceRecord(*parts[1:]))
+        logging.debug('Found %r', records[-1])
+    return records
diff --git a/client/common_lib/cros/tendo/privetd_helper.py b/client/common_lib/cros/tendo/privetd_helper.py
index e9272fc..98f62c5 100644
--- a/client/common_lib/cros/tendo/privetd_helper.py
+++ b/client/common_lib/cros/tendo/privetd_helper.py
@@ -128,7 +128,7 @@
         @param device_whitelist: list of string network interface names to
                 consider exclusively for connectivity monitoring (e.g.
                 ['eth0', 'wlan0']).
-        @param disable_security: bool True to disable pairing security
+        @param disable_pairing_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.
@@ -250,14 +250,24 @@
     """Delegate class containing logic useful with privetd."""
 
 
-    def __init__(self, host=None):
+    def __init__(self, host=None, hostname='localhost',
+                 http_port=DEFAULT_HTTP_PORT, https_port=DEFAULT_HTTPS_PORT):
+        """Construct a PrivetdHelper
+
+        @param host: host object where we should run the HTTP requests from.
+        @param hostname: string hostname of host to issue HTTP requests against.
+        @param http_port: int HTTP port to use when making HTTP requests.
+        @param https_port: int HTTPS port to use when making HTTPs requests.
+
+        """
         self._host = None
         self._run = utils.run
         if host is not None:
             self._host = host
             self._run = host.run
-        self._http_port = DEFAULT_HTTP_PORT
-        self._https_port = DEFAULT_HTTPS_PORT
+        self._hostname = hostname
+        self._http_port = http_port
+        self._https_port = https_port
 
 
     def _build_privet_url(self, path_fragment, use_https=True):
@@ -274,8 +284,8 @@
         if use_https:
             protocol = 'https'
             port = self._https_port
-        hostname = '127.0.0.1'
-        url = '%s://%s:%s/privet/%s' % (protocol, hostname, port, path_fragment)
+        url = '%s://%s:%s/privet/%s' % (protocol, self._hostname, port,
+                                        path_fragment)
         return url
 
 
@@ -439,4 +449,3 @@
                                             auth_token=auth_token)
         return (response['wifi']['status'] == 'success' and
                 response['wifi']['ssid'] == ssid)
-
diff --git a/server/site_linux_router.py b/server/site_linux_router.py
index 4c03702..65b1da8 100644
--- a/server/site_linux_router.py
+++ b/server/site_linux_router.py
@@ -587,6 +587,22 @@
         return instance.interface
 
 
+    def get_station_interface(self, instance):
+        """Get the name of the interface associated with a station.
+
+        @param instance: int station instance number.
+        @return string interface name (e.g. 'managed0').
+
+        """
+        if instance not in range(len(self.station_instances)):
+            raise error.TestFail('Invalid instance number (%d) with %d '
+                                 'instances configured.' %
+                                 (instance, len(self.station_instances)))
+
+        instance = self.station_instances[instance]
+        return instance.interface
+
+
     def get_hostapd_mac(self, ap_num):
         """Return the MAC address of an AP in the test.
 
diff --git a/server/site_tests/privetd_PrivetSetupFlow/privetd_PrivetSetupFlow.py b/server/site_tests/privetd_PrivetSetupFlow/privetd_PrivetSetupFlow.py
index 68471a1..d2c01bd 100644
--- a/server/site_tests/privetd_PrivetSetupFlow/privetd_PrivetSetupFlow.py
+++ b/server/site_tests/privetd_PrivetSetupFlow/privetd_PrivetSetupFlow.py
@@ -2,21 +2,31 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import logging
 import time
 
 from autotest_lib.client.common_lib import error
+from autotest_lib.client.common_lib.cros import avahi_utils
 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 peerd_config
 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
+from autotest_lib.server.cros.network import wifi_client
+
 
 PASSPHRASE = 'chromeos'
+
 PRIVET_AP_STARTUP_TIMEOUT_SECONDS = 30
+PRIVET_MDNS_RECORD_TIMEOUT_SECONDS = 10
+PRIVET_CONNECT_TIMEOUT_SECONDS = 30
+
+POLLING_PERIOD = 0.5
 
 
 class privetd_PrivetSetupFlow(test.test):
@@ -25,6 +35,7 @@
 
     def warmup(self, host, router_hostname=None):
         self._router = None
+        self._shill_xmlrpc_proxy = None
         self._privet_config = privetd_helper.PrivetdConfig(
                 log_verbosity=3,
                 enable_ping=True,
@@ -35,19 +46,23 @@
                 test_name=self.__class__.__name__,
                 client_hostname=host.hostname,
                 router_addr=router_hostname)
+        self._shill_xmlrpc_proxy = wifi_client.get_xmlrpc_proxy(host)
+        # Cleans up profiles, wifi credentials, sandboxes our new credentials.
+        self._shill_xmlrpc_proxy.init_test_network_state()
+        peerd_config.PeerdConfig(verbosity_level=3).restart_with_config(
+                host=host)
 
 
     def cleanup(self, host):
-        privetd_helper.PrivetdConfig.naive_restart(host=host)
+        if self._shill_xmlrpc_proxy is not None:
+            self._shill_xmlrpc_proxy.clean_profiles()
         if self._router is not None:
             self._router.close()
+        privetd_helper.PrivetdConfig.naive_restart(host=host)
 
 
     def run_once(self, host):
-        helper = privetd_helper.PrivetdHelper(host=host)
-        helper.ping_server()  # Make sure the server is up and running.
-
-        # We should see a bootstrapping network broadcasting from the device.
+        logging.info('Looking for privet bootstrapping network from DUT.')
         scan_interface = self._router.get_wlanif(2437, 'managed')
         self._router.host.run('%s link set %s up' %
                               (self._router.cmd_ip, scan_interface))
@@ -85,16 +100,85 @@
         self._router.configure_managed_station(
                 privet_bss.ssid, privet_bss.frequency,
                 ap_netblock.get_addr_in_block(200))
+        station_interface = self._router.get_station_interface(instance=0)
+        logging.debug('Set up station on %s', station_interface)
         self._router.ping(ping_runner.PingConfig(ap_netblock.addr, count=3))
 
+        logging.info('Looking for privet webserver in mDNS records.')
+        start_time = time.time()
+        while time.time() - start_time < PRIVET_MDNS_RECORD_TIMEOUT_SECONDS:
+            all_records = avahi_utils.avahi_browse(host=self._router.host)
+            records = [record for record in all_records
+                       if (record.interface == station_interface and
+                           record.record_type == '_privet._tcp')]
+            if records:
+                break
+            time.sleep(POLLING_PERIOD)
+        if not records:
+            raise error.TestFail('Did not find privet mDNS records in time.')
+        if len(records) > 1:
+            raise error.TestFail('Should not see multiple privet records.')
+        privet_record = records[0]
+        # TODO(wiley) pull the HTTPs port number out of the /info API.
+        helper = privetd_helper.PrivetdHelper(
+                host=host, hostname=privet_record.address,
+                http_port=int(privet_record.port))
+        helper.ping_server()
 
-        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.
+        # Now configure the client with WiFi credentials.
+        auth_token = helper.privet_auth()
+        ssid = self._router.get_ssid()
+        data = helper.setup_add_wifi_credentials(ssid, PASSPHRASE)
+        helper.setup_start(data, auth_token)
+
+        logging.info('Waiting for DUT to connect to router network.')
+        start_time = time.time()
+        # Wait for the DUT to take down the AP.
+        while time.time() - start_time < PRIVET_CONNECT_TIMEOUT_SECONDS:
+            if not dut_iw_runner.list_interfaces(desired_if_type='AP'):
+                break
+            time.sleep(POLLING_PERIOD)
+        else:
+            raise error.TestFail('Timeout waiting for DUT to take down AP.')
+
+        # But we should be able to ping the client from the router's AP.
+        while time.time() - start_time < PRIVET_CONNECT_TIMEOUT_SECONDS:
+            if dut_iw_runner.list_interfaces(desired_if_type='managed'):
+                break
+            time.sleep(POLLING_PERIOD)
+        else:
+            raise error.TestFail('Timeout waiting for DUT managerd interface.')
+
+        while time.time() - start_time < PRIVET_CONNECT_TIMEOUT_SECONDS:
+            devs = dut_iw_runner.list_interfaces(desired_if_type='managed')
+            if devs:
+                managed_interface = interface.Interface(devs[0].if_name,
+                                                        host=host)
+                # Check if we have an IP yet.
+                if managed_interface.ipv4_address_and_prefix:
+                    break
+            time.sleep(POLLING_PERIOD)
+        else:
+            raise error.TestFail('Timeout waiting for DUT managerd interface.')
+
+        managed_netblock = netblock.from_addr(
+                managed_interface.ipv4_address_and_prefix)
+        while time.time() - start_time < PRIVET_CONNECT_TIMEOUT_SECONDS:
+            PING_COUNT = 3
+            result = self._router.ping(
+                    ping_runner.PingConfig(managed_netblock.addr,
+                                           ignore_result=True,
+                                           count=PING_COUNT))
+            if result.received == PING_COUNT:
+                break
+            time.sleep(POLLING_PERIOD)
+        else:
+            raise error.TestFail('Timeout before ping was successful.')
+
+        # And privetd should think it is online as well.
+        helper = privetd_helper.PrivetdHelper(
+                host=host, hostname=managed_netblock.addr,
+                http_port=int(privet_record.port),
+                https_port=int(privet_record.port) + 1)
+        if not helper.wifi_setup_was_successful(ssid, auth_token):
+            raise error.TestFail('Device claims to be offline, but is online.')