Merge "ledpatterns: add an led error code for channel 16"
diff --git a/bouncer/Makefile b/bouncer/Makefile
index bda3d81..db66253 100644
--- a/bouncer/Makefile
+++ b/bouncer/Makefile
@@ -1,6 +1,8 @@
 default: all
 
+INSTALL?=install
 BINDIR=$(DESTDIR)/bin
+LIBDIR=$(DESTDIR)/usr/bouncer
 GPYLINT=$(shell \
     if which gpylint >/dev/null; then \
       echo gpylint; \
@@ -8,26 +10,21 @@
       echo 'echo "(gpylint-missing)" >&2'; \
     fi \
 )
+NOINSTALL=options.py
 
-TARGETS=authorizer hash_mac_addr http_bouncer
-
-HOST_TARGETS=$(addprefix host-,$(TARGETS))
-
-all: $(TARGETS) $(HOST_TARGETS)
+all:
 
 install:
-	mkdir -p $(BINDIR)
-	cp $(TARGETS) $(BINDIR)
+	mkdir -p $(LIBDIR) $(BINDIR)
+	$(INSTALL) -m 0644 $(filter-out $(NOINSTALL) $(TARGETS), $(wildcard *.py)) $(LIBDIR)/
+	for t in authorizer hash_mac_addr http_bouncer; do \
+		$(INSTALL) -m 0755 $$t.py $(LIBDIR)/; \
+		ln -fs /usr/bouncer/$$t.py $(BINDIR)/$$t; \
+	done
 
 install-libs:
 	@echo "No libs to install."
 
-%: %.py
-	ln -s $< $@
-
-host-%: %.py
-	ln -s $< $@
-
 TESTS = $(wildcard test-*.sh) $(wildcard test-*.py) $(wildcard *_test.py)
 runtests: all $(TESTS)
 	set -e; \
diff --git a/bouncer/authorizer.py b/bouncer/authorizer.py
index f355917..0c5e0b1 100755
--- a/bouncer/authorizer.py
+++ b/bouncer/authorizer.py
@@ -43,6 +43,7 @@
 
 MAX_TRIES = 300
 
+in_progress_users = {}
 known_users = {}
 
 
@@ -55,13 +56,22 @@
   return x | y
 
 
+def is_valid_acceptance(response_obj):
+  accepted_time = response_obj.get('accepted')
+  return accepted_time + (opt.max_age * 86400) > time.time()
+
+
+def allow_mac_rule(mac_addr):
+  # iptables, unlike other Linux utilities, capitalizes MAC addresses
+  return ('-m', 'mac', '--mac-source', mac_addr.upper(), '-j', 'ACCEPT')
+
+
 class Checker(object):
   """Manage checking and polling for Terms of Service acceptance."""
 
-  def __init__(self, mac_addr, hashed_mac_addr, url):
+  def __init__(self, mac_addr, url):
     self.mac_addr = mac_addr
-    self.hashed_mac_addr = hashed_mac_addr
-    self.url = url % {'mac': hashed_mac_addr}
+    self.url = url % {'mac': hash_mac_addr.hash_mac_addr(self.mac_addr)}
     self.tries = 0
     self.callback = None
 
@@ -69,41 +79,43 @@
     """Check if a remote service knows about a device with a supplied MAC."""
     logging.info('Checking TOS for %s', self.mac_addr)
     http_client = tornado.httpclient.HTTPClient()
-    response = http_client.fetch(self.url, ca_certs=opt.ca_certs)
-    response_obj = tornado.escape.json_decode(response.body)
-    accepted_time = response_obj.get('accepted')
     self.tries += 1
 
-    accepted = False
-    if accepted_time:
-      if accepted_time + (opt.max_age * 86400) > time.time():
-        accepted = True
-        if self.callback: self.callback.stop()
-        logging.info('TOS accepted for %s', self.mac_addr)
+    try:
+      response = http_client.fetch(self.url, ca_certs=opt.ca_certs)
+      response_obj = tornado.escape.json_decode(response.body)
+      valid = is_valid_acceptance(response_obj)
+    except tornado.httpclient.HTTPError as e:
+      logging.warning('Error checking authorization: %r', e)
+      valid = False
 
-        known_users[self.mac_addr] = response_obj
-        result = ip46tables('-A', opt.filter_chain, '-m', 'mac',
-                            '--mac-source', self.mac_addr, '-j', 'ACCEPT')
-        result |= ip46tables('-t', 'nat', '-A', opt.nat_chain, '-m', 'mac',
-                             '--mac-source', self.mac_addr, '-j', 'ACCEPT')
-        if result:
-          logging.error('Could not update firewall for device %s',
-                        self.mac_addr)
-      else:
-        logging.info('TOS accepted too long ago for %s: %r',
-                     self.mac_addr, accepted_time)
+    if valid:
+      logging.info('TOS accepted for %s', self.mac_addr)
 
-    elif self.callback and self.tries > MAX_TRIES:
-      if not accepted:
-        logging.info('TOS not accepted for %s before timeout.',
-                     self.mac_addr)
-      self.callback.stop()
+      known_users[self.mac_addr] = response_obj
+      result = ip46tables('-A', opt.filter_chain,
+                          *allow_mac_rule(self.mac_addr))
+      result |= ip46tables('-t', 'nat', '-A', opt.nat_chain,
+                           *allow_mac_rule(self.mac_addr))
+      if result:
+        logging.error('Could not update firewall for device %s',
+                      self.mac_addr)
 
-    return response, accepted
+    if valid or self.tries > MAX_TRIES:
+      if self.callback:
+        self.callback.stop()
+      if self.mac_addr in in_progress_users:
+        del in_progress_users[self.mac_addr]
+    else:
+      in_progress_users[self.mac_addr] = self
+      self.poll()
+
+    return response
 
   def poll(self):
-    self.callback = tornado.ioloop.PeriodicCallback(self.check, 1000)
-    self.callback.start()
+    if not self.callback:
+      self.callback = tornado.ioloop.PeriodicCallback(self.check, 1000)
+      self.callback.start()
 
 
 def accept(connection, unused_address):
@@ -112,27 +124,46 @@
 
   maybe_mac_addr = cf.readline().strip()
   try:
-    mac_addr, hashed_mac_addr = hash_mac_addr.hash_mac_addr(maybe_mac_addr)
+    mac_addr = hash_mac_addr.normalize_mac_addr(maybe_mac_addr)
   except ValueError:
     logging.warning('can only check authorization for a MAC address.')
     cf.write('{}')
     return
 
   if mac_addr in known_users:
-    logging.info('TOS accepted (cached) for %s', mac_addr)
     cached_response = known_users[mac_addr]
-    cached_response['cached'] = True
-    cf.write(tornado.escape.json_encode(cached_response))
-    return
+    if is_valid_acceptance(cached_response):
+      logging.info('TOS accepted (cached) for %s', mac_addr)
+      cached_response['cached'] = True
+      cf.write(tornado.escape.json_encode(cached_response))
+      return
 
-  checker = Checker(mac_addr, hashed_mac_addr, opt.url)
-  response, accepted = checker.check()
-  if not accepted:
-    checker.poll()
+  if mac_addr in in_progress_users:
+    checker = in_progress_users[mac_addr]
+  else:
+    checker = Checker(mac_addr, opt.url)
 
+  response = checker.check()
   cf.write(response.body)
 
 
+def expire_cache():
+  """Remove users whose authorization has expired from the cache."""
+  expired_users = set(mac_addr for mac_addr, cached_response
+                      in known_users.items()
+                      if not is_valid_acceptance(cached_response))
+
+  for mac_addr in expired_users:
+    logging.info('Removing expired user %s', mac_addr)
+    del known_users[mac_addr]
+
+    result = ip46tables('-D', opt.filter_chain, *allow_mac_rule(mac_addr))
+    result |= ip46tables('-t', 'nat', '-D', opt.nat_chain,
+                         *allow_mac_rule(mac_addr))
+    if result:
+      logging.warning('Error removing expired user %s !', mac_addr)
+
+
 if __name__ == '__main__':
   o = options.Options(optspec)
   opt, flags, extra = o.parse(sys.argv[1:])
@@ -157,3 +188,6 @@
   logging.info('Started authorizer.')
   ioloop.start()
 
+  expirer = tornado.ioloop.PeriodicCallback(expire_cache, 60 * 60 * 1000)
+  expirer.start()
+
diff --git a/bouncer/hash_mac_addr.py b/bouncer/hash_mac_addr.py
index 961bf10..23b45d1 100755
--- a/bouncer/hash_mac_addr.py
+++ b/bouncer/hash_mac_addr.py
@@ -15,13 +15,16 @@
 """
 
 
-def hash_mac_addr(maybe_mac_addr):
+def normalize_mac_addr(maybe_mac_addr):
   if re.match('([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$', maybe_mac_addr):
-    mac_addr = maybe_mac_addr.lower()
+    return maybe_mac_addr.lower()
   else:
     raise ValueError('%r not a MAC address' % maybe_mac_addr)
 
-  return mac_addr, hashlib.sha1(mac_addr).hexdigest()
+
+def hash_mac_addr(maybe_mac_addr):
+  mac_addr = normalize_mac_addr(maybe_mac_addr)
+  return hashlib.sha1(mac_addr).hexdigest()
 
 
 if __name__ == '__main__':
@@ -32,7 +35,7 @@
     o.usage()
 
   try:
-    _, hashed_mac_addr = hash_mac_addr(str(opt.addr))
+    hashed_mac_addr = hash_mac_addr(str(opt.addr))
     print hashed_mac_addr
   except ValueError as e:
     print >>sys.stderr, 'error:', e.message
diff --git a/bouncer/http_bouncer.py b/bouncer/http_bouncer.py
index 380da4b..5fe0a53 100755
--- a/bouncer/http_bouncer.py
+++ b/bouncer/http_bouncer.py
@@ -68,8 +68,7 @@
     else:
       if self.substitute_mac:
         mac = mac_for_ip(self.request.remote_ip)
-        _, hashed_mac = hash_mac_addr.hash_mac_addr(mac)
-        self.redirect(opt.url % {'mac': hashed_mac})
+        self.redirect(opt.url % {'mac': hash_mac_addr.hash_mac_addr(mac)})
 
         if opt.unix_path:
           try:
diff --git a/bouncer/test-hash_mac_addr.sh b/bouncer/test-hash_mac_addr.sh
index 3c84424..db6f10b 100755
--- a/bouncer/test-hash_mac_addr.sh
+++ b/bouncer/test-hash_mac_addr.sh
@@ -4,7 +4,7 @@
 
 WVSTART "hash_mac_addr test"
 
-HASH_MAC_ADDR=./host-hash_mac_addr
+HASH_MAC_ADDR=./hash_mac_addr.py
 
 WVFAIL $HASH_MAC_ADDR
 WVFAIL $HASH_MAC_ADDR -a nonsense
diff --git a/bouncer/test-http_bouncer.sh b/bouncer/test-http_bouncer.sh
index dedd742..b5bd72a 100755
--- a/bouncer/test-http_bouncer.sh
+++ b/bouncer/test-http_bouncer.sh
@@ -5,7 +5,7 @@
 
 . ./wvtest/wvtest.sh
 
-HTTP_BOUNCER=./host-http_bouncer
+HTTP_BOUNCER=./http_bouncer.py
 PORT="1337"
 URL="http://example.com"
 
diff --git a/conman/connection_manager.py b/conman/connection_manager.py
index 8307efd..c27ef8d 100755
--- a/conman/connection_manager.py
+++ b/conman/connection_manager.py
@@ -620,9 +620,6 @@
       route = ifc.current_routes().get('default', None)
       if route:
         metric = route.get('metric', 0)
-        # Skip temporary connection_check routes.
-        if metric == '99':
-          continue
         candidate = (metric, ifc)
         if (lowest_metric_interface is None or
             candidate < lowest_metric_interface):
diff --git a/conman/interface.py b/conman/interface.py
index cdeaf67..b20709a 100755
--- a/conman/interface.py
+++ b/conman/interface.py
@@ -500,6 +500,7 @@
       except wpactrl.error as e:
         logging.error('wpa_control STATUS request failed %s args %s',
                       e.message, e.args)
+        lines = self.wpa_cli_status().splitlines()
       for line in lines:
         if '=' not in line:
           continue
@@ -553,6 +554,15 @@
     return (self.wpa_status().get('wpa_state', None) == 'COMPLETED' and
             self.wpa_status().get('key_mgmt', None) == 'NONE')
 
+  # TODO(rofrankel):  Remove this if and when the wpactrl failures are fixed.
+  def wpa_cli_status(self):
+    """Fallback for wpa_supplicant control interface status requests."""
+    try:
+      return subprocess.check_output(['wpa_cli', '-i', self.name, 'status'])
+    except subprocess.CalledProcessError:
+      logging.error('wpa_cli status request failed')
+      return ''
+
 
 class FrenzyWPACtrl(object):
   """A WPACtrl for Frenzy devices.
diff --git a/conman/interface_test.py b/conman/interface_test.py
index dd5e037..e8f0876 100755
--- a/conman/interface_test.py
+++ b/conman/interface_test.py
@@ -164,12 +164,7 @@
     if request_type == 'STATUS':
       if self.request_status_fails:
         raise wpactrl.error('test error')
-      if self.connected:
-        return ('foo\nwpa_state=COMPLETED\nssid=%s\nkey_mgmt=%s\nbar' %
-                (self.ssid_testonly,
-                 'WPA2-PSK' if self.secure_testonly else 'NONE'))
-      else:
-        return 'wpa_state=SCANNING\naddress=12:34:56:78:90:ab'
+      return self.wpa_cli_status_testonly()
     else:
       raise ValueError('Invalid request_type %s' % request_type)
 
@@ -198,6 +193,14 @@
     if not os.path.exists(self._socket):
       raise wpactrl.error(msg)
 
+  def wpa_cli_status_testonly(self):
+    if self.connected:
+      return ('foo\nwpa_state=COMPLETED\nssid=%s\nkey_mgmt=%s\nbar' %
+              (self.ssid_testonly,
+               'WPA2-PSK' if self.secure_testonly else 'NONE'))
+    else:
+      return 'wpa_state=SCANNING\naddress=12:34:56:78:90:ab'
+
 
 class Wifi(FakeInterfaceMixin, interface.Wifi):
   """Fake Wifi for testing."""
@@ -247,6 +250,11 @@
     self._secure_testonly = False
     super(Wifi, self).detach_wpa_control()
 
+  def wpa_cli_status(self):
+    # This is just a convenient way of keeping things dry; the actual wpa_cli
+    # status makes a subprocess call which returns the same string.
+    return self._wpa_control.wpa_cli_status_testonly()
+
   def start_wpa_supplicant_testonly(self, path):
     wpa_socket = os.path.join(path, self.name)
     logging.debug('Starting fake wpa_supplicant for %s: %s',
@@ -296,12 +304,6 @@
     self.add_terminating_event()
     super(FrenzyWPACtrl, self).detach()
 
-  def request(self, request_type):
-    if request_type == 'STATUS' and self.request_status_fails:
-      raise wpactrl.error('test error')
-
-    return super(FrenzyWPACtrl, self).request(request_type)
-
 
 class FrenzyWifi(FakeInterfaceMixin, interface.FrenzyWifi):
   WPACtrl = FrenzyWPACtrl
@@ -520,7 +522,7 @@
 
   wvtest.WVPASSNE(w.wpa_status(), {})
   w._wpa_control.request_status_fails = True
-  wvtest.WVPASSEQ(w.wpa_status(), {})
+  wvtest.WVPASSNE(w.wpa_status(), {})
 
   # The wpa_supplicant process disconnects and terminates.
   wpa_control.add_disconnected_event()
diff --git a/hnvram/hnvram_main.c b/hnvram/hnvram_main.c
index eb0f306..1148bb0 100644
--- a/hnvram/hnvram_main.c
+++ b/hnvram/hnvram_main.c
@@ -92,6 +92,8 @@
   {"MAC_ADDR_PON",         NVRAM_FIELD_MAC_ADDR_PON,      HNVRAM_MAC},
   {"PRODUCTION_UNIT",      NVRAM_FIELD_PRODUCTION_UNIT,   HNVRAM_STRING},
   {"BOOT_TARGET",          NVRAM_FIELD_BOOT_TARGET,       HNVRAM_STRING},
+  {"ANDROID_ACTIVE_PARTITION", NVRAM_FIELD_ANDROID_ACTIVE_PARTITION,
+   HNVRAM_STRING},
 };
 
 const hnvram_field_t* get_nvram_field(const char* name) {
diff --git a/logupload/client/debian/init b/logupload/client/debian/init
index b349b5e..bbf7cad 100755
--- a/logupload/client/debian/init
+++ b/logupload/client/debian/init
@@ -36,10 +36,11 @@
     # Our hostnames are something like hostname.cluster.whatever.com.
     # We want the hostname.cluster part to be part of the certname, but
     # sadly the name field in our certs isn't super happy about that, so
-    # let's use _ instead of dot, where relevant.
+    # let's use _ instead of dot and strip hyphens, where relevant.
     hostname -f |
       sed -e 's/\([^.]*\.[^.]*\).*/\1/' \
-          -e 's/\./_/g' |
+          -e 's/\./_/g' \
+          -e 's/-//g' |
       atomic_stdin /tmp/serial
     cd /
     upload-logs-loop </dev/null &