Merge "ginstall: Also check gfactive for active partition"
diff --git a/conman/connection_manager.py b/conman/connection_manager.py
index 11da31c..f43ee8d 100755
--- a/conman/connection_manager.py
+++ b/conman/connection_manager.py
@@ -26,6 +26,15 @@
 import ratchet
 import status
 
+try:
+  import monotime  # pylint: disable=unused-import,g-import-not-at-top
+except ImportError:
+  pass
+try:
+  _gettime = time.monotonic
+except AttributeError:
+  _gettime = time.time
+
 
 HOSTNAME = socket.gethostname()
 TMP_HOSTS = '/tmp/hosts'
@@ -446,7 +455,7 @@
        to join the WLAN again.
     7. Sleep for the rest of the duration of _run_duration_s.
     """
-    start_time = time.time()
+    start_time = _gettime()
     self.notifier.process_events()
     while self.notifier.check_events():
       self.notifier.read_events()
@@ -507,7 +516,7 @@
       # routes to the ACS for provisioning.
       if ((not self.acs() or provisioning_failed) and
           not getattr(wifi, 'last_successful_bss_info', None) and
-          time.time() > wifi.last_wifi_scan_time + self._wifi_scan_period_s):
+          _gettime() > wifi.last_wifi_scan_time + self._wifi_scan_period_s):
         logging.debug('Performing scan on %s.', wifi.name)
         self._wifi_scan(wifi)
 
@@ -518,7 +527,7 @@
       # case 5 is unavailable for some reason.
       for band in wifi.bands:
         wlan_configuration = self._wlan_configuration.get(band, None)
-        if wlan_configuration and time.time() >= self._try_wlan_after[band]:
+        if wlan_configuration and _gettime() >= self._try_wlan_after[band]:
           logging.info('Trying to join WLAN on %s.', wifi.name)
           wlan_configuration.start_client()
           if self._connected_to_wlan(wifi):
@@ -529,7 +538,7 @@
           else:
             logging.error('Failed to connect to WLAN on %s.', wifi.name)
             wifi.status.connected_to_wlan = False
-            self._try_wlan_after[band] = time.time() + self._wlan_retry_s
+            self._try_wlan_after[band] = _gettime() + self._wlan_retry_s
       else:
         # If we are aren't on the WLAN, can ping the ACS, and haven't gotten a
         # new WLAN configuration yet, there are two possibilities:
@@ -552,7 +561,7 @@
             if provisioning_failed:
               wifi.last_successful_bss_info = None
 
-          now = time.time()
+          now = _gettime()
           if self._wlan_configuration:
             logging.info('ACS has not updated WLAN configuration; will retry '
                          ' with old config.')
@@ -571,7 +580,7 @@
         # 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()
+          now = _gettime()
           if self._connected_to_open(wifi) and not provisioning_failed:
             logging.debug('Waiting for provisioning for %ds.',
                           now - self.provisioning_since(wifi))
@@ -579,7 +588,7 @@
             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)))
+    time.sleep(max(0, self._run_duration_s - (_gettime() - start_time)))
 
   def acs(self):
     result = False
@@ -784,7 +793,7 @@
   def _wifi_scan(self, wifi):
     """Perform a wifi scan and update wifi.cycler."""
     logging.info('Scanning on %s...', wifi.name)
-    wifi.last_wifi_scan_time = time.time()
+    wifi.last_wifi_scan_time = _gettime()
     subprocess.call(self.IFUP + [wifi.name])
     # /bin/wifi takes a --band option but then finds the right interface for it,
     # so it's okay to just pick the first band here.
@@ -825,7 +834,7 @@
         wifi.attach_wpa_control(self._wpa_control_interface)
         wifi.handle_wpa_events()
         wifi.status.connected_to_open = True
-        now = time.time()
+        now = _gettime()
         wifi.complain_about_acs_at = now + 5
         logging.info('Attempting to provision via SSID %s', bss_info.ssid)
         self._try_to_upload_logs = True
diff --git a/conman/cycler.py b/conman/cycler.py
index 23f2ce4..7795368 100755
--- a/conman/cycler.py
+++ b/conman/cycler.py
@@ -4,6 +4,15 @@
 
 import time
 
+try:
+  import monotime  # pylint: disable=unused-import,g-import-not-at-top
+except ImportError:
+  pass
+try:
+  _gettime = time.monotonic
+except AttributeError:
+  _gettime = time.time
+
 
 class AgingPriorityCycler(object):
   """A modified priority queue.
@@ -40,7 +49,7 @@
     try:
       self._items[item][0] = priority
     except KeyError:
-      self._items[item] = [priority, time.time()]
+      self._items[item] = [priority, _gettime()]
 
   def remove(self, item):
     if item in self._items:
@@ -65,7 +74,7 @@
     if self.empty():
       return
 
-    now = time.time()
+    now = _gettime()
 
     def aged_priority(key_value):
       _, (priority, birth) = key_value
@@ -86,7 +95,7 @@
     Args:
       items:  An iterable of (item, priority).
     """
-    now = time.time()
+    now = _gettime()
     new_items = {}
     for item, priority in items:
       t = now
diff --git a/conman/ratchet.py b/conman/ratchet.py
index 350ed47..07e61a8 100644
--- a/conman/ratchet.py
+++ b/conman/ratchet.py
@@ -6,6 +6,15 @@
 import os
 import time
 
+try:
+  import monotime  # pylint: disable=unused-import,g-import-not-at-top
+except ImportError:
+  pass
+try:
+  _gettime = time.monotonic
+except AttributeError:
+  _gettime = time.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)
@@ -37,8 +46,8 @@
       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.t0 = t0 or _gettime()
+    self.start_at = start_at or _gettime()
     self.done_after = None
     self.done_by = None
     self.timed_out = False
@@ -56,21 +65,21 @@
       self.mark_done()
       return True
 
-    now = time.time()
+    now = _gettime()
     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()
+    self.not_done_before = _gettime()
     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.done_by = _gettime()
     self.logger.info('%s completed after %.2f seconds',
                      self.name, self.done_by - self.t0)
 
diff --git a/waveguide/fake/devlist b/waveguide/fake/devlist
new file mode 100644
index 0000000..e46a05f
--- /dev/null
+++ b/waveguide/fake/devlist
@@ -0,0 +1,30 @@
+phy#1
+        Interface wlan1_portal
+                ifindex 13
+                wdev 0x100000002
+                addr aa:32:ed:07:7f:a8
+                ssid GFiberSetupAutomation
+                type AP
+                channel 153 (5765 MHz), width: 80 MHz, center1: 5775 MHz
+        Interface wlan1
+                ifindex 10
+                wdev 0x100000001
+                addr 88:dc:96:21:13:a1
+                ssid Tangent
+                type AP
+                channel 153 (5765 MHz), width: 80 MHz, center1: 5775 MHz
+phy#0
+        Interface wlan0_portal
+                ifindex 12
+                wdev 0x2
+                addr aa:3b:1c:64:41:93
+                ssid GFiberSetupAutomation
+                type AP
+                channel 1 (2412 MHz), width: 20 MHz, center1: 2412 MHz
+        Interface wlan0
+                ifindex 6
+                wdev 0x1
+                addr f4:f5:e8:81:54:77
+                ssid Tangent
+                type AP
+                channel 1 (2412 MHz), width: 20 MHz, center1: 2412 MHz
diff --git a/waveguide/fake/iw b/waveguide/fake/iw
index ba954f6..b642a29 100755
--- a/waveguide/fake/iw
+++ b/waveguide/fake/iw
@@ -52,6 +52,10 @@
     [ -r "stationdump.$dev" ] && cat "stationdump.$dev"
     exit 0
     ;;
+  dev)
+    [ -r "devlist" ] && cat "devlist"
+    exit 0
+    ;;
   *)
     exit 1
     ;;
diff --git a/waveguide/waveguide.py b/waveguide/waveguide.py
index 594f83f..8f0c3f2 100755
--- a/waveguide/waveguide.py
+++ b/waveguide/waveguide.py
@@ -16,6 +16,7 @@
 # pylint:disable=invalid-name
 """Wifi channel selection and roaming daemon."""
 
+import collections
 import errno
 import gc
 import json
@@ -204,10 +205,22 @@
 
 
 class WlanManager(object):
-  """A class representing one wifi interface on the local host."""
+  """A class representing one wifi interface on the local host.
+
+  Args:
+    phyname (str): name of the phy, like phy0
+    vdevname (str): name of the vdev, like wlan0 or wlan1_portal
+    high_power (bool): advertise the AP as high power
+    tv_box (bool): advertise the AP as a TV box
+    wifiblaster_controller(:obj:`WifiblasterController`): a shared
+      WifiblasterController to probe associating STAs
+    primary (bool): True if the primary AP on a radio, False otherwise.
+      If False, defers most functionality to the WlanManager for the primary AP
+      and logs associated stations only.
+  """
 
   def __init__(self, phyname, vdevname, high_power, tv_box,
-               wifiblaster_controller):
+               wifiblaster_controller, primary=True):
     self.phyname = phyname
     self.vdevname = vdevname
     self.mac = '\0\0\0\0\0\0'
@@ -234,6 +247,7 @@
     self.auto_disabled = None
     self.autochan_2g = self.autochan_5g = self.autochan_free = 0
     self.wifiblaster_controller = wifiblaster_controller
+    self.primary = primary
     helpers.Unlink(self.Filename('disabled'))
 
   def Filename(self, suffix):
@@ -253,7 +267,10 @@
 
   # TODO(apenwarr): when we have async subprocs, add those here
   def GetReadFds(self):
-    return [self.mcast.rsock]
+    if self.primary:
+      return [self.mcast.rsock]
+    else:
+      return []
 
   def NextTimeout(self):
     return self.next_scan_time
@@ -365,8 +382,10 @@
 
   def UpdateStationInfo(self):
     # These change in the background, not as the result of a scan
-    RunProc(callback=self._SurveyResults,
-            args=['iw', 'dev', self.vdevname, 'survey', 'dump'])
+    if self.primary:
+      RunProc(callback=self._SurveyResults,
+              args=['iw', 'dev', self.vdevname, 'survey', 'dump'])
+
     RunProc(callback=self._AssocResults,
             args=['iw', 'dev', self.vdevname, 'station', 'dump'])
 
@@ -837,15 +856,12 @@
       raise Exception('failed (%d) getting wifi dev list: %r' %
                       (errcode, stderr))
     phy = dev = devtype = None
-    phy_devs = {}
+    phy_devs = collections.defaultdict(list)
 
     def AddEntry():
       if phy and dev:
         if devtype == 'AP':
-          # We only want one vdev per PHY.  Special-purpose vdevs are
-          # probably the same name with an extension, so use the shortest one.
-          if phy not in phy_devs or len(phy_devs[phy]) > len(dev):
-            phy_devs[phy] = dev
+          phy_devs[phy].append(dev)
         else:
           log.Debug('Skipping dev %r because type %r != AP', dev, devtype)
 
@@ -867,17 +883,30 @@
       if g:
         devtype = g.group(1)
     AddEntry()
+
     existing_devs = dict((m.vdevname, m) for m in managers)
+    new_devs = set()
+
+    for phy, devs in phy_devs.items():
+      new_devs.update(devs)
+
+      # We only want one full-fledged vdev per PHY.  Special-purpose vdevs are
+      # probably the same name with an extension, so treat the vdev with the
+      # shortest name as the full-fledged one.
+      devs.sort(key=lambda dev: (len(dev), dev))
+      for i, dev in enumerate(devs):
+        primary = i == 0
+        if dev not in existing_devs:
+          log.Debug('Creating wlan manager for (%r, %r)', phy, dev)
+          managers.append(
+              WlanManager(phy, dev, high_power=high_power, tv_box=tv_box,
+                          wifiblaster_controller=wifiblaster_controller,
+                          primary=primary))
+
     for dev, m in existing_devs.iteritems():
-      if dev not in phy_devs.values():
+      if dev not in new_devs:
         log.Log('Forgetting interface %r.', dev)
         managers.remove(m)
-    for phy, dev in phy_devs.iteritems():
-      if dev not in existing_devs:
-        log.Debug('Creating wlan manager for (%r, %r)', phy, dev)
-        managers.append(
-            WlanManager(phy, dev, high_power=high_power, tv_box=tv_box,
-                        wifiblaster_controller=wifiblaster_controller))
 
   RunProc(callback=ParseDevList, args=['iw', 'dev'])
 
@@ -1171,7 +1200,9 @@
     #   node joins, so it can learn about the other nodes as quickly as
     #   possible.  But if we do that, we need to rate limit it somehow.
     for m in managers:
-      m.DoScans()
+      if m.primary:
+        m.DoScans()
+
     if ((opt.tx_interval and now - last_sent > opt.tx_interval) or (
         opt.autochan_interval and now - last_autochan > opt.autochan_interval)):
       if not opt.fake:
@@ -1182,12 +1213,15 @@
     if opt.tx_interval and now - last_sent > opt.tx_interval:
       last_sent = now
       for m in managers:
-        m.SendUpdate()
-        log.WriteEventFile('sentpacket')
+        if m.primary:
+          m.SendUpdate()
+          log.WriteEventFile('sentpacket')
     if opt.autochan_interval and now - last_autochan > opt.autochan_interval:
       last_autochan = now
       for m in managers:
-        m.ChooseChannel()
+        if m.primary:
+          m.ChooseChannel()
+
     if opt.print_interval and now - last_print > opt.print_interval:
       last_print = now
       selfmacs = set()
@@ -1249,10 +1283,10 @@
             else:
               can2G_count += 1
               capability = '2.4'
-            log.Log('Connected station %s supports %s GHz', station, capability)
+            m.Log('Connected station %s supports %s GHz', station, capability)
           species = clientinfo.taxonomize(station)
           if species:
-            log.Log('Connected station %s taxonomy: %s', station, species)
+            m.Log('Connected station %s taxonomy: %s', station, species)
       if log_sta_band_capabilities:
         log.Log('Connected stations: total %d, 5 GHz %d, 2.4 GHz %d',
                 can5G_count + can2G_count, can5G_count, can2G_count)
diff --git a/waveguide/waveguide_test.py b/waveguide/waveguide_test.py
index bd7a65d..9a67bbe 100644
--- a/waveguide/waveguide_test.py
+++ b/waveguide/waveguide_test.py
@@ -19,6 +19,13 @@
 from wvtest import wvtest
 
 
+class FakeOptDict(object):
+  """A fake options.OptDict containing default values."""
+
+  def __init__(self):
+    self.status_dir = '/tmp/waveguide'
+
+
 @wvtest.wvtest
 def IwTimeoutTest():
   old_timeout = waveguide.IW_TIMEOUT_SECS
@@ -30,5 +37,28 @@
   os.environ['PATH'] = old_path
   waveguide.IW_TIMEOUT_SECS = old_timeout
 
+
+@wvtest.wvtest
+def ParseDevListTest():
+  waveguide.opt = FakeOptDict()
+
+  old_path = os.environ['PATH']
+  os.environ['PATH'] = 'fake:' + os.environ['PATH']
+  managers = []
+  waveguide.CreateManagers(managers, False, False, None)
+
+  got_manager_summary = set((m.phyname, m.vdevname, m.primary)
+                            for m in managers)
+  want_manager_summary = set((
+      ('phy1', 'wlan1', True),
+      ('phy1', 'wlan1_portal', False),
+      ('phy0', 'wlan0', True),
+      ('phy0', 'wlan0_portal', False)))
+
+  wvtest.WVPASSEQ(got_manager_summary, want_manager_summary)
+
+  os.environ['PATH'] = old_path
+
+
 if __name__ == '__main__':
   wvtest.wvtest_main()
diff --git a/wifi/quantenna.py b/wifi/quantenna.py
index 7aad0d0..f3f96ef 100755
--- a/wifi/quantenna.py
+++ b/wifi/quantenna.py
@@ -54,7 +54,7 @@
 
 
 def _set_link_state(hif, state):
-  subprocess.check_output(['ip', 'link', 'set', 'dev', hif, state])
+  subprocess.check_output(['if' + state, hif])
 
 
 def _ifplugd_action(hif, state):