Merge "Add chameleon diags dependency"
diff --git a/conman/connection_manager.py b/conman/connection_manager.py
index 3808458..374d495 100755
--- a/conman/connection_manager.py
+++ b/conman/connection_manager.py
@@ -79,7 +79,7 @@
         '-s': 'ssid',
         '--ssid': 'ssid',
         '-S': 'interface_suffix',
-        '--interface_suffix': 'interface_suffix',
+        '--interface-suffix': 'interface_suffix',
     }
     attr = None
     for line in self.command:
@@ -91,7 +91,7 @@
       attr = binwifi_option_attrs.get(line, None)
 
       if line.startswith('WIFI_PSK='):
-        self.passphrase = line.split('WIFI_PSK=')[-1]
+        self.passphrase = line[len('WIFI_PSK='):]
 
     if self.ssid is None:
       raise ValueError('Command file does not specify SSID')
@@ -601,6 +601,8 @@
     if lowest_metric_interface:
       ip = lowest_metric_interface[1].get_ip_address()
       ip_line = '%s %s\n' % (ip, HOSTNAME) if ip else ''
+      logging.info('Lowest metric default route is on dev %r',
+                   lowest_metric_interface[1].name)
 
     new_tmp_hosts = '%s127.0.0.1 localhost' % ip_line
 
diff --git a/conman/connection_manager_test.py b/conman/connection_manager_test.py
index 271cac7..7cf1785 100755
--- a/conman/connection_manager_test.py
+++ b/conman/connection_manager_test.py
@@ -248,6 +248,21 @@
       f.write(value)
 
 
+@wvtest.wvtest
+def WLANConfigurationParseTest():  # pylint: disable=invalid-name
+  """Test WLANConfiguration parsing."""
+  cmd = '\n'.join([
+      '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)
+
+  wvtest.WVPASSEQ('my ssid=1', config.ssid)
+  wvtest.WVPASSEQ('abcdWIFI_PSK=qwer', config.passphrase)
+  wvtest.WVPASSEQ('_suffix', config.interface_suffix)
+
+
 class Wifi(interface_test.Wifi):
 
   def __init__(self, *args, **kwargs):
diff --git a/conman/interface.py b/conman/interface.py
index cd4d323..2d0fbbe 100755
--- a/conman/interface.py
+++ b/conman/interface.py
@@ -17,7 +17,6 @@
 METRIC_5GHZ = 20
 METRIC_24GHZ_5GHZ = 21
 METRIC_24GHZ = 22
-METRIC_TEMPORARY_CONNECTION_CHECK = 99
 
 RFC2385_MULTICAST_ROUTE = '239.0.0.0/8'
 
@@ -83,21 +82,7 @@
                    ' (ACS)' if check_acs else '', self.name)
       return False
 
-    # Temporarily add a route to make sure the connection check can be run.
-    # Give it a high metric so that it won't interfere with normal default
-    # routes.
-    added_temporary_route = False
-    if 'default' not in self.current_routes():
-      logging.debug('Adding temporary connection check routes for dev %s',
-                    self.name)
-      self._ip_route('add', self._gateway_ip,
-                     'dev', self.name,
-                     'metric', str(METRIC_TEMPORARY_CONNECTION_CHECK))
-      self._ip_route('add', 'default',
-                     'via', self._gateway_ip,
-                     'dev', self.name,
-                     'metric', str(METRIC_TEMPORARY_CONNECTION_CHECK))
-      added_temporary_route = True
+    self.add_routes()
 
     cmd = [self.CONNECTION_CHECK, '-I', self.name]
     if check_acs:
@@ -110,17 +95,6 @@
                    self.name,
                    'passed' if result else 'failed')
 
-    # Delete the temporary route.
-    if added_temporary_route:
-      logging.debug('Deleting temporary connection check routes for dev %s',
-                    self.name)
-      self._ip_route('del', 'default',
-                     'dev', self.name,
-                     'metric', str(METRIC_TEMPORARY_CONNECTION_CHECK))
-      self._ip_route('del', self._gateway_ip,
-                     'dev', self.name,
-                     'metric', str(METRIC_TEMPORARY_CONNECTION_CHECK))
-
     return result
 
   def gateway(self):
@@ -147,40 +121,48 @@
       logging.info('Cannot add route for %s without a metric.', self.name)
       return
 
-    if self._gateway_ip is None:
-      logging.info('Cannot add route for %s without a gateway IP.', self.name)
-      return
-
     # If the current routes are the same, there is nothing to do.  If either
     # exists but is different, delete it before adding an updated one.
     current = self.current_routes()
-    default = current.get('default', {})
-    if ((default.get('via', None), default.get('metric', None)) !=
-        (self._gateway_ip, str(self.metric))):
-      logging.debug('Adding default route for dev %s', self.name)
-      self.delete_route('default')
-      self._ip_route('add', 'default',
-                     'via', self._gateway_ip,
-                     'dev', self.name,
-                     'metric', str(self.metric))
+
+    to_add = []
 
     subnet = current.get('subnet', {})
-    if (self._subnet and
-        (subnet.get('via', None), subnet.get('metric', None)) !=
-        (self._gateway_ip, str(self.metric))):
-      logging.debug('Adding subnet route for dev %s', self.name)
-      self.delete_route('subnet')
-      self._ip_route('add', self._subnet,
-                     'dev', self.name,
-                     'metric', str(self.metric))
+    if self._subnet:
+      if ((subnet.get('route', None), subnet.get('metric', None)) !=
+          (self._subnet, str(self.metric))):
+        logging.debug('Adding subnet route for dev %s', self.name)
+        to_add.append(('subnet', ('add', self._subnet, 'dev', self.name,
+                                  'metric', str(self.metric))))
+        subnet = self._subnet
+    else:
+      subnet = None
+      self.delete_route('default', 'subnet')
+
+    default = current.get('default', {})
+    if self._gateway_ip:
+      if (subnet and
+          (default.get('via', None), default.get('metric', None)) !=
+          (self._gateway_ip, str(self.metric))):
+        logging.debug('Adding default route for dev %s', self.name)
+        to_add.append(('default',
+                       ('add', 'default', 'via', self._gateway_ip,
+                        'dev', self.name, 'metric', str(self.metric))))
+    else:
+      self.delete_route('default')
 
     # RFC2365 multicast route.
     if current.get('multicast', {}).get('metric', None) != str(self.metric):
       logging.debug('Adding multicast route for dev %s', self.name)
-      self.delete_route('multicast')
-      self._ip_route('add', RFC2385_MULTICAST_ROUTE,
-                     'dev', self.name,
-                     'metric', str(self.metric))
+      to_add.append(('multicast', ('add', RFC2385_MULTICAST_ROUTE,
+                                   'dev', self.name,
+                                   'metric', str(self.metric))))
+
+    for route_type, _ in to_add[::-1]:
+      self.delete_route(route_type)
+
+    for _, cmd in to_add:
+      self._ip_route(*cmd)
 
   def delete_route(self, *args):
     """Delete default and/or subnet routes for this interface.
@@ -198,7 +180,8 @@
       raise ValueError(
           'Must specify at least one of default, subnet, multicast to delete.')
 
-    for route_type in args:
+    # Use a sorted list to ensure that default comes before subnet.
+    for route_type in sorted(list(args)):
       while route_type in self.current_routes():
         logging.debug('Deleting %s route for dev %s', route_type, self.name)
         self._ip_route('del', self.current_routes()[route_type]['route'],
@@ -242,17 +225,17 @@
                    ' '.join(self.IP_ROUTE), ' '.join(args))
       return ''
 
-    return self._really_ip_route(*args)
-
-  def _really_ip_route(self, *args):
     try:
       logging.debug('%s calling ip route %s', self.name, ' '.join(args))
-      return subprocess.check_output(self.IP_ROUTE + list(args))
+      return self._really_ip_route(*args)
     except subprocess.CalledProcessError as e:
       logging.error('Failed to call "ip route" with args %r: %s', args,
                     e.message)
       return ''
 
+  def _really_ip_route(self, *args):
+    return subprocess.check_output(self.IP_ROUTE + list(args))
+
   def _ip_addr_show(self):
     try:
       return subprocess.check_output(self.IP_ADDR_SHOW + [self.name])
diff --git a/conman/interface_test.py b/conman/interface_test.py
index e8ab4ca..79c58ae 100755
--- a/conman/interface_test.py
+++ b/conman/interface_test.py
@@ -5,6 +5,9 @@
 import logging
 import os
 import shutil
+import socket
+import struct
+import subprocess
 import tempfile
 import time
 
@@ -38,6 +41,13 @@
     self.routing_table = {}
     self.ip_testonly = None
 
+  def _connection_check(self, *args, **kwargs):
+    result = super(FakeInterfaceMixin, self)._connection_check(*args, **kwargs)
+    if (self.current_routes().get('default', {}).get('via', None) !=
+        self._gateway_ip):
+      return False
+    return result
+
   def set_connection_check_result(self, result):
     if result in ['succeed', 'fail', 'restricted']:
       # pylint: disable=invalid-name
@@ -46,6 +56,29 @@
       raise ValueError('Invalid fake connection_check script.')
 
   def _really_ip_route(self, *args):
+    def can_add_route():
+      def ip_to_int(ip):
+        return struct.unpack('!I', socket.inet_pton(socket.AF_INET, ip))[0]
+
+      if args[1] != 'default':
+        return True
+
+      via = ip_to_int(args[args.index('via') + 1])
+      for (ifc, route, _), _ in self.routing_table.iteritems():
+        if ifc != self.name:
+          continue
+
+        netmask = 0
+        if '/' in route:
+          route, netmask = route.split('/')
+          netmask = 32 - int(netmask)
+        route = ip_to_int(route)
+
+        if (route >> netmask) == (via >> netmask):
+          return True
+
+      return False
+
     if not args:
       return '\n'.join(self.routing_table.values() +
                        ['1.2.3.4/24 dev fake0 proto kernel scope link',
@@ -61,6 +94,10 @@
       route = args[1]
     key = (self.name, route, metric)
     if args[0] == 'add' and key not in self.routing_table:
+      if not can_add_route():
+        raise subprocess.CalledProcessError(
+            'Tried to add default route without subnet route: %r',
+            self.routing_table)
       logging.debug('Adding route for %r', key)
       self.routing_table[key] = ' '.join(args[1:])
     elif args[0] == 'del':
@@ -94,8 +131,8 @@
   """Fake wpactrl.WPACtrl."""
 
   # pylint: disable=unused-argument
-  def __init__(self, socket):
-    self._socket = socket
+  def __init__(self, wpa_socket):
+    self._socket = wpa_socket
     self.events = []
     self.attached = False
     self.connected = False
@@ -344,8 +381,8 @@
 
     b.add_moca_station(0)
     wvtest.WVFAIL(os.path.exists(autoprov_filepath))
-    b.set_gateway_ip('192.168.1.1')
     b.set_subnet('192.168.1.0/24')
+    b.set_gateway_ip('192.168.1.1')
     wvtest.WVFAIL(os.path.exists(autoprov_filepath))
     # Everything should fail because the interface is not initialized.
     wvtest.WVFAIL(b.acs())
@@ -403,6 +440,41 @@
     b.ip_testonly = '192.168.1.100'
     wvtest.WVPASSEQ(b.get_ip_address(), '192.168.1.100')
 
+    # Get a new gateway/subnet (e.g. due to joining a new network).
+    # Not on the subnet; adding IP should fail.
+    b.set_gateway_ip('192.168.2.1')
+    wvtest.WVFAIL('default' in b.current_routes())
+    wvtest.WVPASS('subnet' in b.current_routes())
+    # Without a default route, the connection check should fail.
+    wvtest.WVFAIL(b.acs())
+
+    # Now we get the subnet and should add updated subnet and gateway routes.
+    b.set_subnet('192.168.2.0/24')
+    wvtest.WVPASSEQ(b.current_routes()['default']['via'], '192.168.2.1')
+    wvtest.WVPASSLE(int(b.current_routes()['default']['metric']), 50)
+    wvtest.WVPASSEQ(b.current_routes()['subnet']['route'], '192.168.2.0/24')
+    wvtest.WVPASSLE(int(b.current_routes()['subnet']['metric']), 50)
+    wvtest.WVPASS(b.acs())
+
+    # If we have no subnet, make sure that both subnet and default routes are
+    # removed.
+    b.set_subnet(None)
+    wvtest.WVFAIL('subnet' in b.current_routes())
+    wvtest.WVFAIL('default' in b.current_routes())
+
+    # Now repeat the new-network test, but with a faulty connection.  Make sure
+    # the metrics are set appropriately.
+    b.set_connection_check_result('fail')
+    b.set_subnet('192.168.3.0/24')
+    b.set_gateway_ip('192.168.3.1')
+    wvtest.WVPASSGE(int(b.current_routes()['default']['metric']), 50)
+    wvtest.WVPASSGE(int(b.current_routes()['subnet']['metric']), 50)
+
+    # Now test deleting only the gateway IP.
+    b.set_gateway_ip(None)
+    wvtest.WVPASS('subnet' in b.current_routes())
+    wvtest.WVFAIL('default' in b.current_routes())
+
   finally:
     shutil.rmtree(tmp_dir)
 
@@ -505,6 +577,7 @@
     b = Bridge('br0', '10', acs_autoprovisioning_filepath=autoprov_filepath)
     b.add_moca_station(0)
     b.set_gateway_ip('192.168.1.1')
+    b.set_subnet('192.168.1.0/24')
     b.set_connection_check_result('succeed')
     b.initialize()
 
diff --git a/ledpattern/ledpatterns b/ledpattern/ledpatterns
index 2e0ab63..0b86fb7 100644
--- a/ledpattern/ledpatterns
+++ b/ledpattern/ledpatterns
@@ -1,12 +1,28 @@
-HALTED,P,R
-NO_LASER_CHANNEL,P,P
-SET_LASER_FAILED,P,R,R
-LOSLOF_ALARM,P,R,B
-OTHER_ALARM,P,R,P
-GPON_INITIAL,P,B,R
-GPON_STANDBY,P,B,P
-GPON_SERIAL,P,P,R
-GPON_RANGING,P,P,B
+SET_LASER_FAILED_0,P,R,R,R,R
+SET_LASER_FAILED_1,P,R,R,R,B
+SET_LASER_FAILED_2,P,R,R,B,R
+SET_LASER_FAILED_3,P,R,R,B,B
+SET_LASER_FAILED_4,P,R,B,R,R
+SET_LASER_FAILED_5,P,R,B,R,B
+SET_LASER_FAILED_6,P,R,B,B,R
+SET_LASER_FAILED_7,P,R,B,B,B
+SET_LASER_FAILED_8,P,B,R,R,R
+SET_LASER_FAILED_9,P,B,R,R,B
+SET_LASER_FAILED_10,P,B,R,B,R
+SET_LASER_FAILED_11,P,B,R,B,B
+SET_LASER_FAILED_12,P,B,B,R,R
+SET_LASER_FAILED_13,P,B,B,R,B
+SET_LASER_FAILED_14,P,B,B,B,R
+SET_LASER_FAILED_15,P,B,B,B,B
+GPON_INITIAL,P,P,R,R
+GPON_STANDBY,P,P,R,B
+GPON_SERIAL,P,P,B,R
+GPON_RANGING,P,P,B,B
+HALTED,P,R,R
+NO_LASER_CHANNEL,P,R,B
+LOSLOF_ALARM,P,R,P
+OTHER_ALARM,P,B,R
 WAIT_ACS,P,B,B
-ALL_OK,P,B,B,B
-UNKNOWN_ERROR,P,R,R,R
+ALL_OK,P,B
+UNKNOWN_ERROR,P,R
+
diff --git a/ledpattern/ledtapcode.sh b/ledpattern/ledtapcode.sh
index 6841f2f..7793993 100755
--- a/ledpattern/ledtapcode.sh
+++ b/ledpattern/ledtapcode.sh
@@ -44,8 +44,15 @@
 if [ -f "$LASER_STATUS_FILE" ]; then
   laser_status=$(cat "$LASER_STATUS_FILE")
   if [ "$laser_status" -ne 0 ]; then
-    echo "Playing SET_LASER_FAILED pattern"
-    PlayPatternAndExit SET_LASER_FAILED
+    # Blink out requested laser channel that we failed to tune to
+    laser_channel=$(cat "$LASER_CHANNEL_FILE")
+    if [ "$laser_channel" -eq -1 ]; then
+      echo "$LASER_STATUS_FILE indicates success but there is no requested
+        channel in $LASER_CHANNEL_FILE"
+      PlayPatternAndExit UNKNOWN_ERROR
+    fi
+    echo "Playing SET_LASER_FAILED_${laser_channel} pattern"
+    PlayPatternAndExit "SET_LASER_FAILED_${laser_channel}"
   fi
 fi
 
@@ -89,19 +96,35 @@
   PlayPatternAndExit GPON_RANGING
 fi
 
-laser_channel=$(cat "$LASER_CHANNEL_FILE")
-if [ ! -f "$ACS_FILE" ] && [ "$laser_channel" -eq "-1" ]; then
-  echo "Playing NO_LASER_CHANNEL pattern"
-  PlayPatternAndExit NO_LASER_CHANNEL
-elif [ ! -f "$ACS_FILE" ] && [ $laser_channel -ne "-1" ]; then
-  echo "Playing WAIT_ACS pattern"
-  PlayPatternAndExit WAIT_ACS
-elif [ -f "$ACS_FILE" ] && [ $laser_channel -ne "-1" ]; then
-  echo "Playing ALL_OK pattern"
-  PlayPatternAndExit ALL_OK
-else
-  # If we get all the way here and nothing triggered on the way then this really
-  # is an unknown error...
-  echo "Nothing triggered? Playing UNKNOWN_ERROR pattern..."
-  PlayPatternAndExit UNKNOWN_ERROR
+# GFLT110 does not have tuneable laser
+tuneable_laser="false"
+if startswith "$(cat /etc/platform)" "GFLT3"; then
+  tuneable_laser="true"
 fi
+
+if [ "$tuneable_laser" = false ]; then
+  if [ ! -f "$ACS_FILE" ]; then
+    echo "Playing WAIT_ACS pattern"
+    PlayPatternAndExit WAIT_ACS
+  else
+    echo "Playing ALL_OK pattern"
+    PlayPatternAndExit ALL_OK
+  fi
+else
+  laser_channel=$(cat "$LASER_CHANNEL_FILE")
+  if [ ! -f "$ACS_FILE" ] && [ "$laser_channel" -eq "-1" ]; then
+    echo "Playing NO_LASER_CHANNEL pattern"
+    PlayPatternAndExit NO_LASER_CHANNEL
+  elif [ ! -f "$ACS_FILE" ] && [ "$laser_channel" -ne "-1" ]; then
+    echo "Playing WAIT_ACS pattern"
+    PlayPatternAndExit WAIT_ACS
+  elif [ -f "$ACS_FILE" ] && [ "$laser_channel" -eq "-1" ]; then
+    echo "Has ACS but no laser channel"
+    echo "Playing NO_LASER_CHANNEL pattern"
+    PlayPatternAndExit NO_LASER_CHANNEL
+  else
+    echo "Playing ALL_OK pattern"
+    PlayPatternAndExit ALL_OK
+  fi
+fi
+
diff --git a/wifi/quantenna.py b/wifi/quantenna.py
index 1408574..7aad0d0 100755
--- a/wifi/quantenna.py
+++ b/wifi/quantenna.py
@@ -34,20 +34,27 @@
   raise utils.BinWifiException('no VLAN ID for interface %s' % hif)
 
 
-def _get_interface(mode, suffix):
+def _get_interfaces(mode, suffix):
   # Each host interface (hif) maps to exactly one LHOST interface (lif) based on
   # the VLAN ID as follows: the lif is wifiX where X is the VLAN ID - 2 (VLAN
   # IDs start at 2). The client interface must map to wifi0, so it must have
   # VLAN ID 2.
   prefix = 'wlan' if mode == 'ap' else 'wcli'
-  suffix = '_' + suffix if suffix else ''
+  suffix = r'.*' if suffix == 'ALL' else suffix
   for hif in _get_quantenna_interfaces():
-    if re.match(prefix + r'\d*' + suffix, hif):
+    if re.match(r'^' + prefix + r'\d*' + suffix + r'$', hif):
       vlan = _get_vlan(hif)
       lif = 'wifi%d' % (vlan - 2)
       mac = _get_external_mac(hif)
-      return hif, lif, mac, vlan
-  return None, None, None, None
+      yield hif, lif, mac, vlan
+
+
+def _get_interface(mode, suffix):
+  return next(_get_interfaces(mode, suffix), (None, None, None, None))
+
+
+def _set_link_state(hif, state):
+  subprocess.check_output(['ip', 'link', 'set', 'dev', hif, state])
 
 
 def _ifplugd_action(hif, state):
@@ -145,6 +152,7 @@
     _qcsapi('vlan_config', 'pcie0', 'trunk', vlan)
 
     _qcsapi('block_bss', lif, 0)
+    _set_link_state(hif, 'up')
     _ifplugd_action(hif, 'up')
   except:
     stop_ap_wifi(opt)
@@ -188,6 +196,7 @@
     _qcsapi('vlan_config', 'pcie0', 'enable')
     _qcsapi('vlan_config', 'pcie0', 'trunk', vlan)
 
+    _set_link_state(hif, 'up')
     _ifplugd_action(hif, 'up')
   except:
     stop_client_wifi(opt)
@@ -198,34 +207,32 @@
 
 def stop_ap_wifi(opt):
   """Disable AP."""
-  hif, lif, _, _ = _get_interface('ap', opt.interface_suffix)
-  if not hif:
-    return False
+  hif = None
+  for hif, lif, _, _ in _get_interfaces('ap', opt.interface_suffix):
+    try:
+      _qcsapi('wifi_remove_bss', lif)
+    except subprocess.CalledProcessError:
+      pass
 
-  try:
-    _qcsapi('wifi_remove_bss', lif)
-  except subprocess.CalledProcessError:
-    pass
+    _set_link_state(hif, 'down')
+    _ifplugd_action(hif, 'down')
 
-  _ifplugd_action(hif, 'down')
-
-  return True
+  return hif is not None
 
 
 def stop_client_wifi(opt):
   """Disable client."""
-  hif, lif, _, _ = _get_interface('sta', opt.interface_suffix)
-  if not hif:
-    return False
+  hif = None
+  for hif, lif, _, _ in _get_interfaces('sta', opt.interface_suffix):
+    try:
+      _qcsapi('remove_ssid', lif, _qcsapi('get_ssid_list', lif, 1))
+    except subprocess.CalledProcessError:
+      pass
 
-  try:
-    _qcsapi('remove_ssid', lif, _qcsapi('get_ssid_list', lif, 1))
-  except subprocess.CalledProcessError:
-    pass
+    _set_link_state(hif, 'down')
+    _ifplugd_action(hif, 'down')
 
-  _ifplugd_action(hif, 'down')
-
-  return True
+  return hif is not None
 
 
 def scan_wifi(_):
diff --git a/wifi/quantenna_test.py b/wifi/quantenna_test.py
index 177f557..b0fb485 100755
--- a/wifi/quantenna_test.py
+++ b/wifi/quantenna_test.py
@@ -35,7 +35,7 @@
   quantenna._get_vlan = lambda _: 3
   wvtest.WVPASSEQ(quantenna._get_interface('ap', ''),
                   ('wlan0', 'wifi1', '00:00:00:00:00:00', 3))
-  wvtest.WVPASSEQ(quantenna._get_interface('ap', 'portal'),
+  wvtest.WVPASSEQ(quantenna._get_interface('ap', '_portal'),
                   ('wlan0_portal', 'wifi1', '00:00:00:00:00:00', 3))
   wvtest.WVPASSEQ(quantenna._get_interface('sta', ''),
                   (None, None, None, None))