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.')