Merge "conman:  Wait for provisioning properly."
diff --git a/conman/connection_manager.py b/conman/connection_manager.py
index 76a2d53..8307efd 100755
--- a/conman/connection_manager.py
+++ b/conman/connection_manager.py
@@ -23,11 +23,13 @@
 import experiment
 import interface
 import iw
+import ratchet
 import status
 
 
 HOSTNAME = socket.gethostname()
 TMP_HOSTS = '/tmp/hosts'
+CWMP_PATH = '/tmp/cwmp'
 
 experiment.register('WifiNo2GClient')
 
@@ -63,7 +65,7 @@
   WIFI_SETCLIENT = ['wifi', 'setclient', '--persist']
   WIFI_STOPCLIENT = ['wifi', 'stopclient', '--persist']
 
-  def __init__(self, band, wifi, command_lines, _status, wpa_control_interface):
+  def __init__(self, band, wifi, command_lines, wpa_control_interface):
     self.band = band
     self.wifi = wifi
     self.command = command_lines.splitlines()
@@ -72,7 +74,6 @@
     self.passphrase = None
     self.interface_suffix = None
     self.access_point = None
-    self._status = _status
     self._wpa_control_interface = wpa_control_interface
 
     binwifi_option_attrs = {
@@ -161,22 +162,24 @@
     Returns:
       Whether the command succeeded.
     """
+    self.wifi.set_gateway_ip(None)
+    self.wifi.set_subnet(None)
     command = self.WIFI_SETCLIENT + ['--ssid', self.ssid, '--band', self.band]
     env = dict(os.environ)
     if self.passphrase:
       env['WIFI_CLIENT_PSK'] = self.passphrase
     try:
-      self._status.trying_wlan = True
+      self.wifi.status.trying_wlan = True
       subprocess.check_output(command, stderr=subprocess.STDOUT, env=env)
     except subprocess.CalledProcessError as e:
       logging.error('Failed to start wifi client: %s', e.output)
-      self._status.wlan_failed = True
+      self.wifi.status.wlan_failed = True
       return False
 
     return True
 
   def _post_start_client(self):
-    self._status.connected_to_wlan = True
+    self.wifi.status.connected_to_wlan = True
     logging.info('Started wifi client on %s GHz', self.band)
     self.wifi.attach_wpa_control(self._wpa_control_interface)
 
@@ -191,7 +194,7 @@
       subprocess.check_output(self.WIFI_STOPCLIENT + ['-b', self.band],
                               stderr=subprocess.STDOUT)
       # TODO(rofrankel): Make this work for dual-radio devices.
-      self._status.connected_to_wlan = False
+      self.wifi.status.connected_to_wlan = False
       logging.info('Stopped wifi client on %s GHz', self.band)
     except subprocess.CalledProcessError as e:
       logging.error('Failed to stop wifi client: %s', e.output)
@@ -221,6 +224,7 @@
   IFPLUGD_ACTION = ['/etc/ifplugd/ifplugd.action']
   BINWIFI = ['wifi']
   UPLOAD_LOGS_AND_WAIT = ['timeout', '60', 'upload-logs-and-wait']
+  CWMP_WAKEUP = ['cwmp', 'wakeup']
 
   def __init__(self,
                bridge_interface='br0',
@@ -229,8 +233,9 @@
                moca_tmp_dir='/tmp/cwmp/monitoring/moca2',
                wpa_control_interface='/var/run/wpa_supplicant',
                run_duration_s=1, interface_update_period=5,
-               wifi_scan_period_s=120, wlan_retry_s=15, acs_update_wait_s=10,
-               dhcp_wait_s=10, bssid_cycle_length_s=30):
+               wifi_scan_period_s=120, wlan_retry_s=15, associate_wait_s=15,
+               dhcp_wait_s=10, acs_start_wait_s=20, acs_finish_wait_s=120,
+               bssid_cycle_length_s=30):
 
     self._tmp_dir = tmp_dir
     self._config_dir = config_dir
@@ -242,8 +247,10 @@
     self._interface_update_period = interface_update_period
     self._wifi_scan_period_s = wifi_scan_period_s
     self._wlan_retry_s = wlan_retry_s
-    self._acs_update_wait_s = acs_update_wait_s
+    self._associate_wait_s = associate_wait_s
     self._dhcp_wait_s = dhcp_wait_s
+    self._acs_start_wait_s = acs_start_wait_s
+    self._acs_finish_wait_s = acs_finish_wait_s
     self._bssid_cycle_length_s = bssid_cycle_length_s
     self._wlan_configuration = {}
     self._try_to_upload_logs = False
@@ -263,7 +270,13 @@
 
     self.create_wifi_interfaces()
 
-    self._status = status.Status(self._status_dir)
+    for ifc in self.interfaces():
+      status_dir = os.path.join(self._status_dir, ifc.name)
+      if not os.path.exists(status_dir):
+        os.makedirs(status_dir)
+      ifc.status = status.Status(status_dir)
+    self._status = status.CompositeStatus(self._status_dir,
+                                          [i.status for i in self.interfaces()])
 
     wm = pyinotify.WatchManager()
     wm.add_watch(self._config_dir,
@@ -293,7 +306,7 @@
         wifi_up = self.is_interface_up(wifi.name)
         self.ifplugd_action(wifi.name, wifi_up)
         if wifi_up:
-          self._status.attached_to_wpa_supplicant = wifi.attach_wpa_control(
+          wifi.status.attached_to_wpa_supplicant = wifi.attach_wpa_control(
               self._wpa_control_interface)
 
     for path, prefix in ((self._tmp_dir, self.GATEWAY_FILE_PREFIX),
@@ -324,13 +337,32 @@
 
     # Now that we've read any existing state, it's okay to let interfaces touch
     # the routing table.
-    for ifc in [self.bridge] + self.wifi:
+    for ifc in self.interfaces():
       ifc.initialize()
       logging.info('%s initialized', ifc.name)
 
     self._interface_update_counter = 0
     self._try_wlan_after = {'5': 0, '2.4': 0}
 
+    for wifi in self.wifi:
+      ratchet_name = '%s provisioning' % wifi.name
+      wifi.provisioning_ratchet = ratchet.Ratchet(ratchet_name, [
+          ratchet.Condition('trying_open', wifi.connected_to_open,
+                            self._associate_wait_s,
+                            callback=wifi.expire_connection_status_cache),
+          ratchet.Condition('waiting_for_dhcp', wifi.gateway, self._dhcp_wait_s,
+                            callback=self.cwmp_wakeup),
+          ratchet.FileTouchedCondition('waiting_for_cwmp_wakeup',
+                                       os.path.join(CWMP_PATH, 'acscontact'),
+                                       self._acs_start_wait_s),
+          ratchet.FileTouchedCondition('waiting_for_acs_session',
+                                       os.path.join(CWMP_PATH, 'acsconnected'),
+                                       self._acs_finish_wait_s),
+      ], wifi.status)
+
+  def interfaces(self):
+    return [self.bridge] + self.wifi
+
   def create_wifi_interfaces(self):
     """Create Wifi interfaces."""
 
@@ -444,7 +476,7 @@
       if not wifi.attached():
         logging.debug('Attempting to attach to wpa control interface for %s',
                       wifi.name)
-        self._status.attached_to_wpa_supplicant = wifi.attach_wpa_control(
+        wifi.status.attached_to_wpa_supplicant = wifi.attach_wpa_control(
             self._wpa_control_interface)
       wifi.handle_wpa_events()
 
@@ -455,13 +487,13 @@
       # If this interface is connected to the user's WLAN, there is nothing else
       # to do.
       if self._connected_to_wlan(wifi):
-        self._status.connected_to_wlan = True
+        wifi.status.connected_to_wlan = True
         logging.debug('Connected to WLAN on %s, nothing else to do.', wifi.name)
         break
 
       # This interface is not connected to the WLAN, so scan for potential
       # routes to the ACS for provisioning.
-      if (not self.acs() and
+      if ((not self.acs() or self.provisioning_failed(wifi)) and
           not getattr(wifi, 'last_successful_bss_info', None) and
           time.time() > wifi.last_wifi_scan_time + self._wifi_scan_period_s):
         logging.debug('Performing scan on %s.', wifi.name)
@@ -479,12 +511,12 @@
           wlan_configuration.start_client()
           if self._connected_to_wlan(wifi):
             logging.info('Joined WLAN on %s.', wifi.name)
-            self._status.connected_to_wlan = True
+            wifi.status.connected_to_wlan = True
             self._try_wlan_after[band] = 0
             break
           else:
             logging.error('Failed to connect to WLAN on %s.', wifi.name)
-            self._status.connected_to_wlan = False
+            wifi.status.connected_to_wlan = False
             self._try_wlan_after[band] = time.time() + self._wlan_retry_s
       else:
         # If we are aren't on the WLAN, can ping the ACS, and haven't gotten a
@@ -494,7 +526,8 @@
         # 2) cwmpd isn't writing a configuration, possibly because the device
         #    isn't registered to any accounts.
         logging.debug('Unable to join WLAN on %s', wifi.name)
-        self._status.connected_to_wlan = False
+        wifi.status.connected_to_wlan = False
+        provisioning_failed = self.provisioning_failed(wifi)
         if self.acs():
           logging.debug('Connected to ACS')
           if self._try_to_upload_logs:
@@ -505,54 +538,52 @@
             wifi.last_successful_bss_info = getattr(wifi,
                                                     'last_attempted_bss_info',
                                                     None)
+            if provisioning_failed:
+              wifi.last_successful_bss_info = None
+
           now = time.time()
-          if (self._wlan_configuration and
-              hasattr(wifi, 'waiting_for_acs_since')):
-            if now - wifi.waiting_for_acs_since > self._acs_update_wait_s:
-              logging.info('ACS has not updated WLAN configuration; will retry '
-                           ' with old config.')
-              for w in self.wifi:
-                for b in w.bands:
-                  self._try_wlan_after[b] = now
-              continue
+          if self._wlan_configuration:
+            logging.info('ACS has not updated WLAN configuration; will retry '
+                         ' with old config.')
+            for w in self.wifi:
+              for b in w.bands:
+                self._try_wlan_after[b] = now
+            continue
           # We don't want to want to log this spammily, so do exponential
           # backoff.
           elif (hasattr(wifi, 'complain_about_acs_at')
                 and now >= wifi.complain_about_acs_at):
-            wait = wifi.complain_about_acs_at - wifi.waiting_for_acs_since
+            wait = wifi.complain_about_acs_at - self.provisioning_since(wifi)
             logging.info('Can ping ACS, but no WLAN configuration for %ds.',
                          wait)
             wifi.complain_about_acs_at += wait
-        # If we didn't manage to join the WLAN and we don't have an ACS
-        # connection, we should try to establish one.
-        else:
-          # If we are associated but waiting for a DHCP lease, try again later.
+        # If we didn't manage to join the WLAN, and we don't have an ACS
+        # connection or the ACS session failed, we should try another open AP.
+        if not self.acs() or provisioning_failed:
           now = time.time()
-          connected_to_open = (
-              wifi.wpa_status().get('wpa_state', None) == 'COMPLETED' and
-              wifi.wpa_status().get('key_mgmt', None) == 'NONE')
-          wait_for_dhcp = (
-              not wifi.gateway() and
-              hasattr(wifi, 'waiting_for_dhcp_since') and
-              now - wifi.waiting_for_dhcp_since < self._dhcp_wait_s)
-          if connected_to_open and wait_for_dhcp:
-            logging.debug('Waiting for DHCP lease after %ds.',
-                          now - wifi.waiting_for_acs_since)
+          if self._connected_to_open(wifi) and not provisioning_failed:
+            logging.debug('Waiting for provisioning for %ds.',
+                          now - self.provisioning_since(wifi))
           else:
-            logging.debug('Not connected to ACS')
+            logging.debug('Not connected to ACS or provisioning failed')
             self._try_next_bssid(wifi)
 
     time.sleep(max(0, self._run_duration_s - (time.time() - start_time)))
 
   def acs(self):
-    result = self.bridge.acs() or any(wifi.acs() for wifi in self.wifi)
-    self._status.can_reach_acs = result
+    result = False
+    for ifc in self.interfaces():
+      acs = ifc.acs()
+      ifc.status.can_reach_acs = acs
+      result |= acs
     return result
 
   def internet(self):
-    result = self.bridge.internet() or any(wifi.internet()
-                                           for wifi in self.wifi)
-    self._status.can_reach_internet = result
+    result = False
+    for ifc in self.interfaces():
+      internet = ifc.internet()
+      ifc.status.can_reach_internet = internet
+      result |= internet
     return result
 
   def _update_interfaces_and_routes(self):
@@ -585,7 +616,7 @@
   def _update_tmp_hosts(self):
     """Update the contents of /tmp/hosts."""
     lowest_metric_interface = None
-    for ifc in [self.bridge] + self.wifi:
+    for ifc in self.interfaces():
       route = ifc.current_routes().get('default', None)
       if route:
         metric = route.get('metric', 0)
@@ -640,7 +671,7 @@
     config.stop_access_point()
     del self._wlan_configuration[band]
     if not self._wlan_configuration:
-      self._status.have_config = False
+      self.wifi_for_band(band).status.have_config = False
 
   def _process_file(self, path, filename):
     """Process or ignore an updated file in a watched directory."""
@@ -674,7 +705,7 @@
           wifi = self.wifi_for_band(band)
           if wifi:
             self._update_wlan_configuration(
-                self.WLANConfiguration(band, wifi, contents, self._status,
+                self.WLANConfiguration(band, wifi, contents,
                                        self._wpa_control_interface))
       elif filename.startswith(self.ACCESS_POINT_FILE_PREFIX):
         match = re.match(self.ACCESS_POINT_FILE_REGEXP, filename)
@@ -725,7 +756,7 @@
           self.ifplugd_action('moca0', has_moca)
 
   def interface_by_name(self, interface_name):
-    for ifc in [self.bridge] + self.wifi:
+    for ifc in self.interfaces():
       if ifc.name == interface_name:
         return ifc
 
@@ -780,20 +811,19 @@
     if bss_info is not None:
       logging.info('Attempting to connect to SSID %s (%s) for provisioning',
                    bss_info.ssid, bss_info.bssid)
-      self._status.trying_open = True
-      wifi.set_gateway_ip(None)
+      self.start_provisioning(wifi)
       connected = self._try_bssid(wifi, bss_info)
       if connected:
-        self._status.connected_to_open = True
+        wifi.status.connected_to_open = True
         now = time.time()
-        wifi.waiting_for_acs_since = now
-        wifi.waiting_for_dhcp_since = now
         wifi.complain_about_acs_at = now + 5
         logging.info('Attempting to provision via SSID %s', bss_info.ssid)
         self._try_to_upload_logs = True
       # If we can no longer connect to this, it's no longer successful.
-      elif bss_info == last_successful_bss_info:
-        wifi.last_successful_bss_info = None
+      else:
+        wifi.status.connected_to_open = False
+        if bss_info == last_successful_bss_info:
+          wifi.last_successful_bss_info = None
       return connected
     else:
       # TODO(rofrankel):  There are probably more cases in which this should be
@@ -801,7 +831,8 @@
       # Relatedly, once we find ACS access on an open network we may want to
       # save that SSID/BSSID and that first in future.  If we do that then we
       # can declare that provisioning has failed much more aggressively.
-      self._status.provisioning_failed = True
+      logging.info('Ran out of BSSIDs to try on %s', wifi.name)
+      wifi.status.provisioning_failed = True
 
     return False
 
@@ -818,6 +849,11 @@
                 in self._wlan_configuration.iteritems()
                 if band in wifi.bands))
 
+  def _connected_to_open(self, wifi):
+    result = wifi.connected_to_open()
+    wifi.status.connected_to_open = result
+    return result
+
   def _update_wlan_configuration(self, wlan_configuration):
     band = wlan_configuration.band
     current = self._wlan_configuration.get(band, None)
@@ -831,7 +867,7 @@
              if wlan_configuration.interface_suffix else '') + band)
         wlan_configuration.access_point = os.path.exists(ap_file)
       self._wlan_configuration[band] = wlan_configuration
-      self._status.have_config = True
+      self.wifi_for_band(band).status.have_config = True
       logging.info('Updated WLAN configuration for %s GHz', band)
       self._update_access_point(wlan_configuration)
 
@@ -891,6 +927,31 @@
     if subprocess.call(self.UPLOAD_LOGS_AND_WAIT) != 0:
       logging.error('Failed to upload logs')
 
+  def cwmp_wakeup(self):
+    if subprocess.call(self.CWMP_WAKEUP) != 0:
+      logging.error('cwmp wakeup failed')
+
+  def start_provisioning(self, wifi):
+    wifi.set_gateway_ip(None)
+    wifi.set_subnet(None)
+    wifi.provisioning_ratchet.reset()
+
+  def provisioning_failed(self, wifi):
+    try:
+      wifi.provisioning_ratchet.check()
+      if wifi.provisioning_ratchet.done_after:
+        wifi.status.provisioning_completed = True
+        logging.info('%s successfully provisioned', wifi.name)
+      return False
+    except ratchet.TimeoutException:
+      wifi.status.provisioning_failed = True
+      logging.info('%s failed to provision: %s', wifi.name,
+                   wifi.provisioning_ratchet.current_step().name)
+      return True
+
+  def provisioning_since(self, wifi):
+    return wifi.provisioning_ratchet.t0
+
 
 def _wifi_show():
   try:
diff --git a/conman/connection_manager_test.py b/conman/connection_manager_test.py
index 7cf1785..a64ab79 100755
--- a/conman/connection_manager_test.py
+++ b/conman/connection_manager_test.py
@@ -255,8 +255,7 @@
       'WIFI_PSK=abcdWIFI_PSK=qwer', 'wifi', 'set', '-P', '-b', '5',
       '--bridge=br0', '-s', 'my ssid=1', '--interface-suffix', '_suffix',
   ])
-  config = WLANConfiguration('5', interface_test.Wifi('wcli0', 20), cmd, None,
-                             None)
+  config = WLANConfiguration('5', interface_test.Wifi('wcli0', 20), cmd, None)
 
   wvtest.WVPASSEQ('my ssid=1', config.ssid)
   wvtest.WVPASSEQ('abcdWIFI_PSK=qwer', config.passphrase)
@@ -291,6 +290,7 @@
   IFPLUGD_ACTION = ['echo', 'ifplugd.action']
   BINWIFI = ['echo', 'wifi']
   UPLOAD_LOGS_AND_WAIT = ['echo', 'upload-logs-and-wait']
+  CWMP_WAKEUP = ['echo', 'cwmp', 'wakeup']
 
   def __init__(self, *args, **kwargs):
     self._binwifi_commands = []
@@ -326,6 +326,7 @@
     # Will s3 fail to acquire a DHCP lease?
     self.dhcp_failure_on_s3 = False
     self.log_upload_count = 0
+    self.acs_session_fails = False
 
   def create_wifi_interfaces(self):
     super(ConnectionManager, self).create_wifi_interfaces()
@@ -461,6 +462,7 @@
     gateway_file = os.path.join(self._tmp_dir,
                                 self.GATEWAY_FILE_PREFIX + interface_name)
     with open(gateway_file, 'w') as f:
+      logging.debug('Writing gateway file %s', gateway_file)
       # This value doesn't matter to conman, so it's fine to hard code it here.
       f.write('192.168.1.1')
 
@@ -468,6 +470,7 @@
     subnet_file = os.path.join(self._tmp_dir,
                                self.SUBNET_FILE_PREFIX + interface_name)
     with open(subnet_file, 'w') as f:
+      logging.debug('Writing subnet file %s', subnet_file)
       # This value doesn't matter to conman, so it's fine to hard code it here.
       f.write('192.168.1.0/24')
 
@@ -509,6 +512,19 @@
   def has_status_files(self, files):
     return not set(files) - set(os.listdir(self._status_dir))
 
+  def cwmp_wakeup(self):
+    super(ConnectionManager, self).cwmp_wakeup()
+    self.write_acscontact()
+    if self.acs():
+      self.write_acsconnected()
+
+  def write_acscontact(self):
+    open(os.path.join(connection_manager.CWMP_PATH, 'acscontact'), 'w')
+
+  def write_acsconnected(self):
+    if not self.acs_session_fails:
+      open(os.path.join(connection_manager.CWMP_PATH, 'acsconnected'), 'w')
+
 
 def wlan_config_filename(path, band):
   return os.path.join(path, 'command.%s' % band)
@@ -560,7 +576,10 @@
       interface_update_period = 5
       wifi_scan_period = 15
       wifi_scan_period_s = run_duration_s * wifi_scan_period
+      associate_wait_s = 0
       dhcp_wait_s = .5
+      acs_start_wait_s = 0
+      acs_finish_wait_s = 0
 
       # pylint: disable=protected-access
       old_wifi_show = connection_manager._wifi_show
@@ -579,6 +598,7 @@
         moca_tmp_dir = tempfile.mkdtemp()
         wpa_control_interface = tempfile.mkdtemp()
         FrenzyWifi.WPACtrl.WIFIINFO_PATH = tempfile.mkdtemp()
+        connection_manager.CWMP_PATH = tempfile.mkdtemp()
 
         for band, access_point in wlan_configs.iteritems():
           write_wlan_config(config_dir, band, 'initial ssid', 'initial psk')
@@ -595,14 +615,13 @@
                               run_duration_s=run_duration_s,
                               interface_update_period=interface_update_period,
                               wifi_scan_period_s=wifi_scan_period_s,
+                              associate_wait_s=associate_wait_s,
                               dhcp_wait_s=dhcp_wait_s,
+                              acs_start_wait_s=acs_start_wait_s,
+                              acs_finish_wait_s=acs_finish_wait_s,
                               bssid_cycle_length_s=1,
                               **cm_kwargs)
 
-        c.test_interface_update_period = interface_update_period
-        c.test_wifi_scan_period = wifi_scan_period
-        c.test_dhcp_wait_s = dhcp_wait_s
-
         f(c)
       finally:
         if os.path.exists(connection_manager.TMP_HOSTS):
@@ -612,6 +631,7 @@
         shutil.rmtree(moca_tmp_dir)
         shutil.rmtree(wpa_control_interface)
         shutil.rmtree(FrenzyWifi.WPACtrl.WIFIINFO_PATH)
+        shutil.rmtree(connection_manager.CWMP_PATH)
         # pylint: disable=protected-access
         connection_manager._wifi_show = old_wifi_show
         connection_manager._get_quantenna_interfaces = old_gqi
@@ -889,7 +909,7 @@
   c.can_connect_to_s2 = True
   # Give it time to try all BSSIDs.  This means sleeping long enough that
   # everything in the cycler is active, then doing n+1 loops (the n+1st loop is
-  # when we decided that the SSID in the nth loop was successful).
+  # when we decide that the SSID in the nth loop was successful).
   time.sleep(c._bssid_cycle_length_s)
   for _ in range(len(c.wifi_for_band(band).cycler) + 1):
     c.run_once()
@@ -931,13 +951,68 @@
   last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
   wvtest.WVPASSEQ(last_bss_info.ssid, 's3')
   wvtest.WVPASSEQ(last_bss_info.bssid, 'ff:ee:dd:cc:bb:aa')
+  wvtest.WVPASS(c.has_status_files([status.P.WAITING_FOR_DHCP,
+                                    status.P.WAITING_FOR_PROVISIONING]))
   # Third iteration: sleep for dhcp_wait_s and check that we try another AP.
-  time.sleep(c.test_dhcp_wait_s)
+  time.sleep(c._dhcp_wait_s)
   c.run_once()
   last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
   wvtest.WVPASSNE(last_bss_info.ssid, 's3')
   wvtest.WVPASSNE(last_bss_info.bssid, 'ff:ee:dd:cc:bb:aa')
 
+  # Now repeat the above, but for an ACS session that takes a while.  We don't
+  # necessarily want to leave if it fails (so we don't want the third check),
+  # but we do want to make sure we don't leave while we're still waiting for it.
+  #
+  # Unlike DHCP, which we can always simulate working immediately above, it is
+  # wrong to simulate ACS sessions working for connections without ACS access.
+  # This means we can either always wait for the ACS session timeout in every
+  # test above, making the tests much slower, or we can set that timeout to 0
+  # and then be a little gross here and change it.  The latter is unfortunately
+  # the lesser evil, because slow tests are bad.
+  del c.wifi_for_band(band).cycler
+  c.dhcp_failure_on_s3 = False
+  c.acs_session_fails = True
+  # First iteration: check that we try s3.
+  c.run_until_scan(band)
+  last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
+  wvtest.WVPASSEQ(last_bss_info.ssid, 's3')
+  wvtest.WVPASSEQ(last_bss_info.bssid, 'ff:ee:dd:cc:bb:aa')
+
+  # This is the gross part.
+  # pylint: disable=protected-access
+  c.wifi_for_band(band).provisioning_ratchet.steps[3].timeout = 0.5
+
+  # Second iteration: check that we don't leave while waiting.
+  c.run_once()
+  last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
+  wvtest.WVPASSEQ(last_bss_info.ssid, 's3')
+  wvtest.WVPASSEQ(last_bss_info.bssid, 'ff:ee:dd:cc:bb:aa')
+  wvtest.WVPASS(c.has_status_files([status.P.WAITING_FOR_ACS_SESSION,
+                                    status.P.WAITING_FOR_PROVISIONING]))
+  time.sleep(0.5)
+  c.run_once()
+  wvtest.WVPASS(c.has_status_files([status.P.PROVISIONING_FAILED]))
+  c.wifi_for_band(band).provisioning_ratchet.steps[3].timeout = 0
+
+  # Finally, test successful provisioning.
+  del c.wifi_for_band(band).cycler
+  c.dhcp_failure_on_s3 = False
+  c.acs_session_fails = False
+  # First iteration: check that we try s3.
+  c.run_until_scan(band)
+  last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
+  wvtest.WVPASSEQ(last_bss_info.ssid, 's3')
+  wvtest.WVPASSEQ(last_bss_info.bssid, 'ff:ee:dd:cc:bb:aa')
+
+  c.run_once()
+  last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
+  wvtest.WVPASSEQ(last_bss_info.ssid, 's3')
+  wvtest.WVPASSEQ(last_bss_info.bssid, 'ff:ee:dd:cc:bb:aa')
+  wvtest.WVFAIL(c.has_status_files([status.P.WAITING_FOR_ACS_SESSION,
+                                    status.P.WAITING_FOR_PROVISIONING]))
+  wvtest.WVPASS(c.has_status_files([status.P.PROVISIONING_COMPLETED]))
+
 
 @wvtest.wvtest
 @connection_manager_test(WIFI_SHOW_OUTPUT_MARVELL8897)
diff --git a/conman/interface.py b/conman/interface.py
index 75e2631..cdeaf67 100755
--- a/conman/interface.py
+++ b/conman/interface.py
@@ -549,6 +549,10 @@
     self.initial_ssid = None
     super(Wifi, self).initialize()
 
+  def connected_to_open(self):
+    return (self.wpa_status().get('wpa_state', None) == 'COMPLETED' and
+            self.wpa_status().get('key_mgmt', None) == 'NONE')
+
 
 class FrenzyWPACtrl(object):
   """A WPACtrl for Frenzy devices.
diff --git a/conman/interface_test.py b/conman/interface_test.py
index 79c58ae..dd5e037 100755
--- a/conman/interface_test.py
+++ b/conman/interface_test.py
@@ -248,8 +248,10 @@
     super(Wifi, self).detach_wpa_control()
 
   def start_wpa_supplicant_testonly(self, path):
-    logging.debug('Starting fake wpa_supplicant for %s', self.name)
-    open(os.path.join(path, self.name), 'w')
+    wpa_socket = os.path.join(path, self.name)
+    logging.debug('Starting fake wpa_supplicant for %s: %s',
+                  self.name, wpa_socket)
+    open(wpa_socket, 'w')
 
   def kill_wpa_supplicant_testonly(self, path):
     logging.debug('Killing fake wpa_supplicant for %s', self.name)
diff --git a/conman/ratchet.py b/conman/ratchet.py
new file mode 100644
index 0000000..7873c39
--- /dev/null
+++ b/conman/ratchet.py
@@ -0,0 +1,181 @@
+#!/usr/bin/python -S
+
+"""Utility for ensuring a series of events occur or time out."""
+
+import logging
+import os
+import time
+
+# This has to be called before another module calls it with a higher log level.
+# pylint: disable=g-import-not-at-top
+logging.basicConfig(level=logging.DEBUG)
+
+
+class TimeoutException(Exception):
+  pass
+
+
+class Condition(object):
+  """Wrapper for a function that may time out."""
+
+  def __init__(self, name, evaluate, timeout, logger=None, callback=None):
+    self.name = name
+    if evaluate:
+      self.evaluate = evaluate
+    self.timeout = timeout
+    self.logger = logger or logging
+    self.callback = callback
+    self.reset()
+
+  def reset(self, t0=None, start_at=None):
+    """Reset the Condition to an initial state.
+
+    Takes two different timestamp values to account for uncertainty in when a
+    previous condition may have been met.
+
+    Args:
+      t0:  The timestamp after which to evaluate the condition.
+      start_at:  The timestamp from which to compute the timeout.
+    """
+    self.t0 = t0 or time.time()
+    self.start_at = start_at or time.time()
+    self.done_after = None
+    self.done_by = None
+    self.timed_out = False
+    self.not_done_before = self.t0
+
+  def check(self):
+    """Check whether the condition has completed or timed out."""
+    if self.timed_out:
+      raise TimeoutException()
+
+    if self.done_after:
+      return True
+
+    if self.evaluate():
+      self.mark_done()
+      return True
+
+    now = time.time()
+    if now > self.start_at + self.timeout:
+      self.timed_out = True
+      self.logger.info('%s timed out after %.2f seconds',
+                       self.name, now - self.t0)
+      raise TimeoutException()
+
+    self.not_done_before = time.time()
+    return False
+
+  def mark_done(self):
+    # In general, we don't know when a condition finished, but we know it was
+    # *after* whenever it was most recently not done.
+    self.done_after = self.not_done_before
+    self.done_by = time.time()
+    self.logger.info('%s completed after %.2f seconds',
+                     self.name, self.done_by - self.t0)
+
+    if self.callback:
+      self.callback()
+
+
+class FileExistsCondition(Condition):
+  """A condition that checks for the existence of a file."""
+
+  def __init__(self, name, filename, timeout):
+    self._filename = filename
+    super(FileExistsCondition, self).__init__(name, None, timeout)
+
+  def evaluate(self):
+    return os.path.exists(self._filename)
+
+  def mtime(self):
+    if os.path.exists(self._filename):
+      return os.stat(self._filename).st_mtime
+
+    return None
+
+  def mark_done(self):
+    super(FileExistsCondition, self).mark_done()
+    # We have to check this because the file could have been deleted while this
+    # was being called.  But this condition should almost always be true.
+    mtime = self.mtime()
+    if mtime:
+      self.done_after = self.done_by = mtime
+
+
+class FileTouchedCondition(FileExistsCondition):
+  """A condition that checks that a file was touched after a certain time."""
+
+  def reset(self, t0=None, start_at=None):
+    mtime = self.mtime
+    if t0 and mtime and mtime < t0:
+      self.initial_mtime = self.mtime()
+    else:
+      self.initial_mtime = None
+    super(FileTouchedCondition, self).reset(t0, start_at)
+
+  def evaluate(self):
+    if not super(FileTouchedCondition, self).evaluate():
+      return False
+
+    if self.initial_mtime:
+      return self.mtime() > self.initial_mtime
+
+    return self.mtime() >= self.t0
+
+
+class Ratchet(Condition):
+  """A condition that comprises a series of subconditions."""
+
+  def __init__(self, name, steps, status):
+    self.name = name
+    self.steps = steps
+    for step in self.steps:
+      step.logger = logging.getLogger(self.name)
+    self._status = status
+    super(Ratchet, self).__init__(name, None, 0)
+
+  def reset(self):
+    self._current_step = 0
+    for step in self.steps:
+      step.reset()
+      self._set_step_status(step, False)
+    self._set_current_step_status(True)
+    super(Ratchet, self).reset()
+
+  # Override check rather than evaluate because we don't want the Ratchet to
+  # time out unless one of its steps does.
+  def check(self):
+    if not self.done_after:
+      while self.current_step().check():
+        if not self.advance():
+          self.mark_done()
+          break
+
+    return self.done_after
+
+  def current_step(self):
+    return self.steps[self._current_step]
+
+  def on_final_step(self):
+    return self._current_step == len(self.steps) - 1
+
+  def advance(self):
+    if self.on_final_step():
+      return False
+    else:
+      prev_step = self.current_step()
+      self._current_step += 1
+      self.current_step().start_at = prev_step.done_by
+      self._set_current_step_status(True)
+      return True
+
+  def mark_done(self):
+    super(Ratchet, self).mark_done()
+    self.done_after = self.steps[-1].done_after
+
+  def _set_step_status(self, step, value):
+    setattr(self._status, step.name, value)
+
+  def _set_current_step_status(self, value):
+    self._set_step_status(self.current_step(), value)
diff --git a/conman/ratchet_test.py b/conman/ratchet_test.py
new file mode 100755
index 0000000..10c10ab
--- /dev/null
+++ b/conman/ratchet_test.py
@@ -0,0 +1,128 @@
+#!/usr/bin/python
+
+"""Tests for ratchet.py."""
+
+import os
+import shutil
+import tempfile
+import time
+
+import ratchet
+import status
+from wvtest import wvtest
+
+
+@wvtest.wvtest
+def condition_test():
+  """Test basic Condition functionality."""
+  x = y = 0
+  callback_sink = []
+  cx = ratchet.Condition('cx', lambda: x != 0, 0)
+  cy = ratchet.Condition('cx', lambda: y != 0, 0.1,
+                         callback=lambda: callback_sink.append([0]))
+  wvtest.WVEXCEPT(ratchet.TimeoutException, cx.check)
+  wvtest.WVFAIL(cy.check())
+  time.sleep(0.1)
+  wvtest.WVEXCEPT(ratchet.TimeoutException, cy.check)
+
+  x = 1
+  wvtest.WVEXCEPT(ratchet.TimeoutException, cx.check)
+  cx.reset()
+  wvtest.WVPASS(cx.check())
+
+  y = 1
+  cy.reset()
+  wvtest.WVPASS(cy.check())
+  wvtest.WVPASSEQ(len(callback_sink), 1)
+  # Callback shouldn't fire again.
+  wvtest.WVPASS(cy.check())
+  wvtest.WVPASSEQ(len(callback_sink), 1)
+  cy.reset()
+  wvtest.WVPASS(cy.check())
+  wvtest.WVPASSEQ(len(callback_sink), 2)
+
+
+@wvtest.wvtest
+def file_condition_test():
+  """Test File*Condition functionality."""
+  try:
+    _, filename = tempfile.mkstemp()
+    c_exists = ratchet.FileExistsCondition('c exists', filename, 0.1)
+    c_mtime = ratchet.FileTouchedCondition('c mtime', filename, 0.1)
+    wvtest.WVPASS(c_exists.check())
+    wvtest.WVFAIL(c_mtime.check())
+    # mtime precision is too low to notice that we're touching the file *after*
+    # capturing its initial mtime rather than at the same time, so take a short
+    # nap before touching it.
+    time.sleep(0.01)
+    open(filename, 'w')
+    wvtest.WVPASS(c_mtime.check())
+
+    # Test that old mtimes don't count.
+    time.sleep(0.01)
+    c_mtime.reset()
+    wvtest.WVFAIL(c_mtime.check())
+    time.sleep(0.1)
+    wvtest.WVEXCEPT(ratchet.TimeoutException, c_mtime.check)
+
+    # Test t0 and start_at.
+    os.unlink(filename)
+    now = time.time()
+    c_mtime.reset(t0=now, start_at=now + 0.2)
+    wvtest.WVFAIL(c_mtime.check())
+    time.sleep(0.15)
+    wvtest.WVFAIL(c_mtime.check())
+    open(filename, 'w')
+    wvtest.WVPASS(c_mtime.check())
+
+  finally:
+    os.unlink(filename)
+
+
+@wvtest.wvtest
+def ratchet_test():
+  """Test Ratchet functionality."""
+
+  class P(object):
+    X = 'X'
+    Y = 'Y'
+    Z = 'Z'
+  status.P = P
+  status.IMPLICATIONS = {}
+
+  status_export_path = tempfile.mkdtemp()
+  try:
+    x = y = z = 0
+    r = ratchet.Ratchet('test ratchet', [
+        ratchet.Condition('x', lambda: x, 0.1),
+        ratchet.Condition('y', lambda: y, 0.1),
+        ratchet.Condition('z', lambda: z, 0.1),
+    ], status.Status(status_export_path))
+    x = y = 1
+
+    # Test that timeouts are not just summed, but start whenever the previous
+    # step completed.
+    wvtest.WVPASSEQ(r._current_step, 0)  # pylint: disable=protected-access
+    wvtest.WVPASS(os.path.isfile(os.path.join(status_export_path, 'X')))
+    wvtest.WVFAIL(os.path.isfile(os.path.join(status_export_path, 'Y')))
+    wvtest.WVFAIL(os.path.isfile(os.path.join(status_export_path, 'Z')))
+    time.sleep(0.05)
+    wvtest.WVFAIL(r.check())
+    wvtest.WVPASSEQ(r._current_step, 2)  # pylint: disable=protected-access
+    wvtest.WVPASS(os.path.isfile(os.path.join(status_export_path, 'X')))
+    wvtest.WVPASS(os.path.isfile(os.path.join(status_export_path, 'Y')))
+    wvtest.WVPASS(os.path.isfile(os.path.join(status_export_path, 'Z')))
+    wvtest.WVFAIL(r.check())
+    wvtest.WVPASSEQ(r._current_step, 2)  # pylint: disable=protected-access
+    time.sleep(0.1)
+    wvtest.WVEXCEPT(ratchet.TimeoutException, r.check)
+
+    x = y = z = 1
+    r.reset()
+    wvtest.WVPASS(r.check())
+  finally:
+    shutil.rmtree(status_export_path)
+
+
+if __name__ == '__main__':
+  wvtest.wvtest_main()
diff --git a/conman/status.py b/conman/status.py
index 7f75682..8f8d3a1 100644
--- a/conman/status.py
+++ b/conman/status.py
@@ -32,17 +32,25 @@
   PROVISIONING_FAILED = 'PROVISIONING_FAILED'
   ATTACHED_TO_WPA_SUPPLICANT = 'ATTACHED_TO_WPA_SUPPLICANT'
 
+  WAITING_FOR_PROVISIONING = 'WAITING_FOR_PROVISIONING'
+  WAITING_FOR_DHCP = 'WAITING_FOR_DHCP'
+  WAITING_FOR_CWMP_WAKEUP = 'WAITING_FOR_CWMP_WAKEUP'
+  WAITING_FOR_ACS_SESSION = 'WAITING_FOR_ACS_SESSION'
+  PROVISIONING_COMPLETED = 'PROVISIONING_COMPLETED'
+
 
 # Format:  { proposition: (implications, counter-implications), ... }
 # If you want to add a new proposition to the Status class, just edit this dict.
 IMPLICATIONS = {
     P.TRYING_OPEN: (
         (),
-        (P.CONNECTED_TO_OPEN, P.TRYING_WLAN, P.CONNECTED_TO_WLAN)
+        (P.CONNECTED_TO_OPEN, P.TRYING_WLAN, P.CONNECTED_TO_WLAN,
+         P.WAITING_FOR_PROVISIONING)
     ),
     P.TRYING_WLAN: (
         (),
-        (P.TRYING_OPEN, P.CONNECTED_TO_OPEN, P.CONNECTED_TO_WLAN)
+        (P.TRYING_OPEN, P.CONNECTED_TO_OPEN, P.CONNECTED_TO_WLAN,
+         P.WAITING_FOR_PROVISIONING)
     ),
     P.WLAN_FAILED: (
         (),
@@ -54,7 +62,8 @@
     ),
     P.CONNECTED_TO_WLAN: (
         (P.HAVE_WORKING_CONFIG,),
-        (P.CONNECTED_TO_OPEN, P.TRYING_OPEN, P.TRYING_WLAN)
+        (P.CONNECTED_TO_OPEN, P.TRYING_OPEN, P.TRYING_WLAN,
+         P.WAITING_FOR_PROVISIONING)
     ),
     P.CAN_REACH_ACS: (
         (P.COULD_REACH_ACS,),
@@ -62,11 +71,11 @@
     ),
     P.COULD_REACH_ACS: (
         (),
-        (P.PROVISIONING_FAILED,),
+        (),
     ),
     P.PROVISIONING_FAILED: (
         (),
-        (P.COULD_REACH_ACS,),
+        (P.WAITING_FOR_PROVISIONING, P.PROVISIONING_COMPLETED,),
     ),
     P.HAVE_WORKING_CONFIG: (
         (P.HAVE_CONFIG,),
@@ -75,24 +84,60 @@
     P.ATTACHED_TO_WPA_SUPPLICANT: (
         (),
         (),
-    )
+    ),
+    P.WAITING_FOR_PROVISIONING: (
+        (P.CONNECTED_TO_OPEN,),
+        (),
+    ),
+    P.WAITING_FOR_DHCP: (
+        (P.WAITING_FOR_PROVISIONING,),
+        (P.WAITING_FOR_CWMP_WAKEUP, P.WAITING_FOR_ACS_SESSION),
+    ),
+    P.WAITING_FOR_CWMP_WAKEUP: (
+        (P.WAITING_FOR_PROVISIONING,),
+        (P.WAITING_FOR_DHCP, P.WAITING_FOR_ACS_SESSION),
+    ),
+    P.WAITING_FOR_ACS_SESSION: (
+        (P.WAITING_FOR_PROVISIONING,),
+        (P.WAITING_FOR_DHCP, P.WAITING_FOR_CWMP_WAKEUP),
+    ),
+    P.PROVISIONING_COMPLETED: (
+        (),
+        (P.WAITING_FOR_PROVISIONING, P.PROVISIONING_FAILED,),
+    ),
 }
 
 
-class Proposition(object):
+class ExportedValue(object):
+
+  def __init__(self, name, export_path):
+    self._name = name
+    self._export_path = export_path
+
+  def export(self):
+    filepath = os.path.join(self._export_path, self._name)
+    if self.value():
+      if not os.path.exists(filepath):
+        open(filepath, 'w')
+    else:
+      if os.path.exists(filepath):
+        os.unlink(filepath)
+
+
+class Proposition(ExportedValue):
   """Represents a proposition.
 
   May imply truth or falsity of other propositions.
   """
 
-  def __init__(self, name, export_path):
-    self._name = name
-    self._export_path = export_path
+  def __init__(self, *args, **kwargs):
+    super(Proposition, self).__init__(*args, **kwargs)
     self._value = None
     self._implications = set()
     self._counter_implications = set()
     self._impliers = set()
     self._counter_impliers = set()
+    self.parents = set()
 
   def implies(self, implication):
     self._counter_implications.discard(implication)
@@ -115,11 +160,20 @@
     self._counter_impliers.add(counter_implier)
 
   def set(self, value):
+    """Set this Proposition's value.
+
+    If the value changed, update any dependent values/files.
+
+    Args:
+      value:  The new value.
+    """
     if value == self._value:
       return
 
     self._value = value
     self.export()
+    for parent in self.parents:
+      parent.export()
     logging.debug('%s is now %s', self._name, self._value)
 
     if value:
@@ -135,14 +189,20 @@
       for implier in self._impliers:
         implier.set(False)
 
-  def export(self):
-    filepath = os.path.join(self._export_path, self._name)
-    if self._value:
-      if not os.path.exists(filepath):
-        open(filepath, 'w')
-    else:
-      if os.path.exists(filepath):
-        os.unlink(filepath)
+  def value(self):
+    return self._value
+
+
+class Disjunction(ExportedValue):
+
+  def __init__(self, name, export_path, disjuncts):
+    super(Disjunction, self).__init__(name, export_path)
+    self.disjuncts = disjuncts
+    for disjunct in self.disjuncts:
+      disjunct.parents.add(self)
+
+  def value(self):
+    return any(d.value() for d in self.disjuncts)
 
 
 class Status(object):
@@ -154,6 +214,9 @@
 
     self._export_path = export_path
 
+    self._set_up_propositions()
+
+  def _set_up_propositions(self):
     self._propositions = {
         p: Proposition(p, self._export_path)
         for p in dict(inspect.getmembers(P)) if not p.startswith('_')
@@ -166,7 +229,7 @@
         self._propositions[p].implies_not(
             self._propositions[counter_implication])
 
-  def _proposition(self, p):
+  def proposition(self, p):
     return self._propositions[p]
 
   def __setattr__(self, attr, value):
@@ -187,3 +250,17 @@
           return
 
     super(Status, self).__setattr__(attr, value)
+
+
+class CompositeStatus(Status):
+
+  def __init__(self, export_path, children):
+    self._children = children
+    super(CompositeStatus, self).__init__(export_path)
+
+  def _set_up_propositions(self):
+    self._propositions = {
+        p: Disjunction(p, self._export_path,
+                       [c.proposition(p) for c in self._children])
+        for p in dict(inspect.getmembers(P)) if not p.startswith('_')
+    }
diff --git a/conman/status_test.py b/conman/status_test.py
index 03223f8..befebbf 100755
--- a/conman/status_test.py
+++ b/conman/status_test.py
@@ -17,6 +17,10 @@
   return os.path.exists(os.path.join(path, filename))
 
 
+def has_file(s, filename):
+  return file_in(s._export_path, filename)
+
+
 @wvtest.wvtest
 def test_proposition():
   export_path = tempfile.mkdtemp()
@@ -71,51 +75,85 @@
 
 @wvtest.wvtest
 def test_status():
-  export_path = tempfile.mkdtemp()
+  export_path_s = tempfile.mkdtemp()
+  export_path_t = tempfile.mkdtemp()
+  export_path_st = tempfile.mkdtemp()
 
   try:
-    s = status.Status(export_path)
+    s = status.Status(export_path_s)
+    t = status.Status(export_path_t)
+    st = status.CompositeStatus(export_path_st, [s, t])
 
     # Sanity check that there are no contradictions.
     for p, (want_true, want_false) in status.IMPLICATIONS.iteritems():
       setattr(s, p.lower(), True)
-      wvtest.WVPASS(file_in(export_path, p))
+      wvtest.WVPASS(has_file(s, p))
       for wt in want_true:
-        wvtest.WVPASS(file_in(export_path, wt))
+        wvtest.WVPASS(has_file(s, wt))
       for wf in want_false:
-        wvtest.WVFAIL(file_in(export_path, wf))
+        wvtest.WVFAIL(has_file(s, wf))
+
+    def check_exported(check_s, check_t, filename):
+      wvtest.WVPASSEQ(check_s, has_file(s, filename))
+      wvtest.WVPASSEQ(check_t, has_file(t, filename))
+      wvtest.WVPASSEQ(check_s or check_t, has_file(st, filename))
 
     s.trying_wlan = True
-    wvtest.WVPASS(file_in(export_path, status.P.TRYING_WLAN))
-    wvtest.WVFAIL(file_in(export_path, status.P.CONNECTED_TO_WLAN))
+    t.trying_wlan = False
+    check_exported(True, False, status.P.TRYING_WLAN)
+    check_exported(False, False, status.P.CONNECTED_TO_WLAN)
 
     s.connected_to_open = True
-    wvtest.WVPASS(file_in(export_path, status.P.CONNECTED_TO_OPEN))
-    wvtest.WVFAIL(file_in(export_path, status.P.CONNECTED_TO_WLAN))
+    check_exported(True, False, status.P.CONNECTED_TO_OPEN)
+    check_exported(False, False, status.P.CONNECTED_TO_WLAN)
 
     s.connected_to_wlan = True
-    wvtest.WVPASS(file_in(export_path, status.P.CONNECTED_TO_WLAN))
-    wvtest.WVPASS(file_in(export_path, status.P.HAVE_WORKING_CONFIG))
-    wvtest.WVFAIL(file_in(export_path, status.P.CONNECTED_TO_OPEN))
-    wvtest.WVFAIL(file_in(export_path, status.P.TRYING_WLAN))
-    wvtest.WVFAIL(file_in(export_path, status.P.TRYING_OPEN))
+    t.trying_wlan = True
+    check_exported(True, False, status.P.CONNECTED_TO_WLAN)
+    check_exported(True, False, status.P.HAVE_WORKING_CONFIG)
+    check_exported(False, False, status.P.CONNECTED_TO_OPEN)
+    check_exported(False, True, status.P.TRYING_WLAN)
+    check_exported(False, False, status.P.TRYING_OPEN)
 
     s.can_reach_acs = True
     s.can_reach_internet = True
-    wvtest.WVPASS(file_in(export_path, status.P.CAN_REACH_ACS))
-    wvtest.WVPASS(file_in(export_path, status.P.COULD_REACH_ACS))
-    wvtest.WVPASS(file_in(export_path, status.P.CAN_REACH_INTERNET))
-    wvtest.WVFAIL(file_in(export_path, status.P.PROVISIONING_FAILED))
+    check_exported(True, False, status.P.CAN_REACH_ACS)
+    check_exported(True, False, status.P.COULD_REACH_ACS)
+    check_exported(True, False, status.P.CAN_REACH_INTERNET)
+    check_exported(False, False, status.P.PROVISIONING_FAILED)
 
     # These should not have changed
-    wvtest.WVPASS(file_in(export_path, status.P.CONNECTED_TO_WLAN))
-    wvtest.WVPASS(file_in(export_path, status.P.HAVE_WORKING_CONFIG))
-    wvtest.WVFAIL(file_in(export_path, status.P.CONNECTED_TO_OPEN))
-    wvtest.WVFAIL(file_in(export_path, status.P.TRYING_WLAN))
-    wvtest.WVFAIL(file_in(export_path, status.P.TRYING_OPEN))
+    check_exported(True, False, status.P.CONNECTED_TO_WLAN)
+    check_exported(True, False, status.P.HAVE_WORKING_CONFIG)
+    check_exported(False, False, status.P.CONNECTED_TO_OPEN)
+    check_exported(False, True, status.P.TRYING_WLAN)
+    check_exported(False, False, status.P.TRYING_OPEN)
+
+    # Test provisioning statuses.
+    s.waiting_for_dhcp = True
+    check_exported(False, True, status.P.TRYING_WLAN)
+    check_exported(False, False, status.P.TRYING_OPEN)
+    check_exported(False, False, status.P.CONNECTED_TO_WLAN)
+    check_exported(True, False, status.P.CONNECTED_TO_OPEN)
+    check_exported(True, False, status.P.WAITING_FOR_PROVISIONING)
+    check_exported(True, False, status.P.WAITING_FOR_DHCP)
+    s.waiting_for_cwmp_wakeup = True
+    check_exported(False, False, status.P.WAITING_FOR_DHCP)
+    check_exported(True, False, status.P.WAITING_FOR_CWMP_WAKEUP)
+    s.waiting_for_acs_session = True
+    check_exported(False, False, status.P.WAITING_FOR_DHCP)
+    check_exported(False, False, status.P.WAITING_FOR_CWMP_WAKEUP)
+    check_exported(True, False, status.P.WAITING_FOR_ACS_SESSION)
+    s.provisioning_completed = True
+    check_exported(False, False, status.P.WAITING_FOR_PROVISIONING)
+    check_exported(False, False, status.P.WAITING_FOR_DHCP)
+    check_exported(False, False, status.P.WAITING_FOR_CWMP_WAKEUP)
+    check_exported(False, False, status.P.WAITING_FOR_CWMP_WAKEUP)
 
   finally:
-    shutil.rmtree(export_path)
+    shutil.rmtree(export_path_s)
+    shutil.rmtree(export_path_t)
+    shutil.rmtree(export_path_st)
 
 
 if __name__ == '__main__':