conman:  Re-use successful provisioning APs.

When provisioning, if we have already provisionined successfully, try to
re-use that AP before trying others or scanning.

In addition to speeing up reprovisioning, this makes it easier to decide
that we don't have a known path to the ACS, which is useful for
controlling LED state.

Change-Id: I5c56ef6ee75ad4fcb28b7648dd17b10c1cb598d2
diff --git a/conman/connection_manager.py b/conman/connection_manager.py
index 26fe6b6..a2de7d6 100755
--- a/conman/connection_manager.py
+++ b/conman/connection_manager.py
@@ -389,6 +389,7 @@
       # This interface is not connected to the WLAN, so scan for potential
       # routes to the ACS for provisioning.
       if (not self.acs() 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)
         self._wifi_scan(wifi)
@@ -423,6 +424,9 @@
         self._status.connected_to_wlan = False
         if self.acs():
           logging.debug('Connected to ACS on %s', wifi.name)
+          wifi.last_successful_bss_info = getattr(wifi,
+                                                  'last_attempted_bss_info',
+                                                  None)
           now = time.time()
           if (self._wlan_configuration and
               hasattr(wifi, 'waiting_for_acs_since')):
@@ -461,9 +465,23 @@
     return result
 
   def _update_interfaces_and_routes(self):
+    """Touch each interface via update_routes."""
+
     self.bridge.update_routes()
     for wifi in self.wifi:
       wifi.update_routes()
+      # If wifi is connected to something that's not the WLAN, it must be a
+      # provisioning attempt, and in particular that attempt must be via
+      # last_attempted_bss_info.  If that is the same as the
+      # last_successful_bss_info (i.e. the last attempt was successful) and we
+      # aren't connected to the ACS after calling update_routes (which expires
+      # the connection status cache), then this BSS is no longer successful.
+      if (wifi.wpa_supplicant and
+          not self._connected_to_wlan(wifi) and
+          (getattr(wifi, 'last_successful_bss_info', None) ==
+           getattr(wifi, 'last_attempted_bss_info', None)) and
+          not wifi.acs()):
+        wifi.last_successful_bss_info = None
 
     # Make sure these get called semi-regularly so that exported status is up-
     # to-date.
@@ -619,19 +637,22 @@
     if not hasattr(wifi, 'cycler'):
       return False
 
-    bss_info = wifi.cycler.next()
+    last_successful_bss_info = getattr(wifi, 'last_successful_bss_info', None)
+    bss_info = last_successful_bss_info or wifi.cycler.next()
     if bss_info is not None:
+      logging.debug('Attempting to connect to SSID %s for provisioning',
+                    bss_info.ssid)
       self._status.trying_open = True
-      connected = subprocess.call(self.WIFI_SETCLIENT +
-                                  ['--ssid', bss_info.ssid,
-                                   '--band', wifi.bands[0],
-                                   '--bssid', bss_info.bssid]) == 0
+      connected = self._try_bssid(wifi, bss_info)
       if connected:
         self._status.connected_to_open = True
         now = time.time()
         wifi.waiting_for_acs_since = now
         wifi.complain_about_acs_at = now + 5
         logging.info('Attempting to provision via SSID %s', bss_info.ssid)
+      # 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
       return connected
     else:
       # TODO(rofrankel):  There are probably more cases in which this should be
@@ -643,6 +664,13 @@
 
     return False
 
+  def _try_bssid(self, wifi, bss_info):
+    wifi.last_attempted_bss_info = bss_info
+    return subprocess.call(self.WIFI_SETCLIENT +
+                           ['--ssid', bss_info.ssid,
+                            '--band', wifi.bands[0],
+                            '--bssid', bss_info.bssid]) == 0
+
   def _connected_to_wlan(self, wifi):
     return (wifi.wpa_supplicant and
             any(config.client_up for band, config
diff --git a/conman/connection_manager_test.py b/conman/connection_manager_test.py
index 9e582e7..6b1142d 100755
--- a/conman/connection_manager_test.py
+++ b/conman/connection_manager_test.py
@@ -221,6 +221,10 @@
       self.interface_by_name(wifi)._initially_connected = True
 
     self.scan_has_results = False
+    # Should we be able to connect to open network s2?
+    self.s2_connect = True
+    # Will s2 fail rather than providing ACS access?
+    self.s2_fail = False
 
   @property
   def IP_LINK(self):
@@ -235,13 +239,8 @@
         wifi = self.wifi_for_band(wlan_configuration.band)
         wifi.add_terminating_event()
 
-  def _try_next_bssid(self, wifi):
-    if hasattr(wifi, 'cycler'):
-      bss_info = wifi.cycler.peek()
-      if bss_info:
-        self.last_provisioning_attempt = bss_info
-
-    super(ConnectionManager, self)._try_next_bssid(wifi)
+  def _try_bssid(self, wifi, bss_info):
+    super(ConnectionManager, self)._try_bssid(wifi, bss_info)
 
     socket = os.path.join(self._wpa_control_interface, wifi.name)
 
@@ -255,13 +254,21 @@
       return True
 
     if bss_info and bss_info.ssid == 's2':
-      if wifi.attached():
-        wifi.add_connected_event()
+      if self.s2_connect:
+        if wifi.attached():
+          wifi.add_connected_event()
+        else:
+          open(socket, 'w')
+        if self.s2_fail:
+          connection_check_result = 'fail'
+          logging.debug('s2 configured to have no ACS access')
+        else:
+          connection_check_result = 'restricted'
+        wifi.set_connection_check_result(connection_check_result)
+        self.ifplugd_action(wifi.name, True)
+        return True
       else:
-        open(socket, 'w')
-      wifi.set_connection_check_result('restricted')
-      self.ifplugd_action(wifi.name, True)
-      return True
+        logging.debug('s2 configured not to connect')
 
     return False
 
@@ -532,8 +539,11 @@
   for _ in range(3):
     c.run_once()
     wvtest.WVPASS(c.has_status_files([status.P.CONNECTED_TO_OPEN]))
-  wvtest.WVPASSEQ(c.last_provisioning_attempt.ssid, 's2')
-  wvtest.WVPASSEQ(c.last_provisioning_attempt.bssid, '01:23:45:67:89:ab')
+
+  last_bss_info = c.wifi_for_band('2.4').last_attempted_bss_info
+  wvtest.WVPASSEQ(last_bss_info.ssid, 's2')
+  wvtest.WVPASSEQ(last_bss_info.bssid, '01:23:45:67:89:ab')
+
   # Wait for the connection to be processed.
   c.run_once()
   wvtest.WVPASS(c.acs())
@@ -591,6 +601,75 @@
   wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
   wvtest.WVPASS(c.bridge.current_route())
 
+  # Now delete the config and bring down the bridge and make sure we reprovision
+  # via the last working BSS.
+  c.delete_wlan_config('2.4')
+  c.bridge.set_connection_check_result('fail')
+  scan_count_2_4 = c.wifi_for_band('2.4').wifi_scan_counter
+  c.run_until_interface_update()
+  wvtest.WVFAIL(c.acs())
+  wvtest.WVFAIL(c.internet())
+  # s2 is not what the cycler would suggest trying next.
+  wvtest.WVPASSNE('s2', c.wifi_for_band('2.4').cycler.peek())
+  # Run only once, so that only one BSS can be tried.  It should be the s2 one,
+  # since that worked previously.
+  c.run_once()
+  wvtest.WVPASS(c.acs())
+  # Make sure we didn't scan on 2.4.
+  wvtest.WVPASSEQ(scan_count_2_4, c.wifi_for_band('2.4').wifi_scan_counter)
+
+  # Now re-create the WLAN config, connect to the WLAN, and make sure that s2 is
+  # unset as last_successful_bss_info if it is no longer available.
+  c.write_wlan_config('2.4', ssid, psk)
+  c.run_once()
+  wvtest.WVPASS(c.acs())
+  wvtest.WVPASS(c.internet())
+
+  c.s2_connect = False
+  c.delete_wlan_config('2.4')
+  c.run_once()
+  wvtest.WVPASSEQ(c.wifi_for_band('2.4').last_successful_bss_info, None)
+
+  # Now do the same, except this time s2 is connected to but doesn't provide ACS
+  # access.  This requires first re-establishing s2 as successful, so there are
+  # four steps:
+  #
+  # 1) Connect to WLAN.
+  # 2) Disconnect, reprovision via s2 (establishing it as successful).
+  # 3) Reconnect to WLAN so that we can trigger re-provisioning by
+  #    disconnecting.
+  # 4) Connect to s2 but get no ACS access; see that last_successful_bss_info is
+  #    unset.
+  c.write_wlan_config('2.4', ssid, psk)
+  c.run_once()
+  wvtest.WVPASS(c.acs())
+  wvtest.WVPASS(c.internet())
+
+  c.delete_wlan_config('2.4')
+  c.run_once()
+  wvtest.WVFAIL(c.wifi_for_band('2.4').acs())
+
+  c.s2_connect = True
+  # Give it time to try all BSSIDs.
+  for _ in range(3):
+    c.run_once()
+  s2_bss = iw.BssInfo('01:23:45:67:89:ab', 's2')
+  wvtest.WVPASSEQ(c.wifi_for_band('2.4').last_successful_bss_info, s2_bss)
+
+  c.s2_fail = True
+  c.write_wlan_config('2.4', ssid, psk)
+  c.run_once()
+  wvtest.WVPASS(c.acs())
+  wvtest.WVPASS(c.internet())
+
+  wvtest.WVPASSEQ(c.wifi_for_band('2.4').last_successful_bss_info, s2_bss)
+  c.delete_wlan_config('2.4')
+  # Run once so that c will reconnect to s2.
+  c.run_once()
+  # Now run until it sees the lack of ACS access.
+  c.run_until_interface_update()
+  wvtest.WVPASSEQ(c.wifi_for_band('2.4').last_successful_bss_info, None)
+
 
 @wvtest.wvtest
 @connection_manager_test(WIFI_SHOW_OUTPUT_ONE_RADIO)
diff --git a/conman/iw.py b/conman/iw.py
index f751302..973d653 100755
--- a/conman/iw.py
+++ b/conman/iw.py
@@ -34,7 +34,7 @@
 
   def __eq__(self, other):
     # pylint: disable=protected-access
-    return self.__attrs() == other.__attrs()
+    return isinstance(other, BssInfo) and self.__attrs() == other.__attrs()
 
   def __hash__(self):
     return hash(self.__attrs())