Merge branch 'development'
diff --git a/README.md b/README.md
index 940e456..8e1cc3b 100644
--- a/README.md
+++ b/README.md
@@ -247,6 +247,7 @@
 InstanceSetup     | set_host_keys        | `false` skips generating host keys on first boot.
 InstanceSetup     | set_multiqueue       | `false` skips multiqueue driver support.
 IpForwarding      | ethernet_proto_id    | Protocol ID string for daemon added routes.
+MetadataScripts   | run_dir              | String base directory where metadata scripts are executed.
 MetadataScripts   | startup              | `false` disables startup script execution.
 MetadataScripts   | shutdown             | `false` disables shutdown script execution.
 NetworkInterfaces | dhcp_command         | String to execute to enable network interfaces.
diff --git a/build_packages.sh b/build_packages.sh
index a2c5cef..6205077 100755
--- a/build_packages.sh
+++ b/build_packages.sh
@@ -42,6 +42,7 @@
   fpm \
     -s python \
     -t "${pkg_type}" \
+    --conflicts 'irqbalance' \
     --depends "${depends}" \
     --depends 'python-boto' \
     --depends 'python-setuptools' \
diff --git a/google_compute_engine/accounts/accounts_utils.py b/google_compute_engine/accounts/accounts_utils.py
index fbdd5ef..09b1761 100644
--- a/google_compute_engine/accounts/accounts_utils.py
+++ b/google_compute_engine/accounts/accounts_utils.py
@@ -167,6 +167,8 @@
 
     Raises:
       IOError, raised when there is an exception updating a file.
+      OSError, raised when setting permissions or writing to a read-only
+          file system.
     """
     pw_entry = self._GetUser(user)
     if not pw_entry:
@@ -302,7 +304,7 @@
 
     try:
       self._UpdateAuthorizedKeys(user, ssh_keys)
-    except IOError as e:
+    except (IOError, OSError) as e:
       message = 'Could not update the authorized keys file for user %s. %s.'
       self.logger.warning(message, user, str(e))
       return False
diff --git a/google_compute_engine/instance_setup/instance_config.py b/google_compute_engine/instance_setup/instance_config.py
index 675db3a..10e7c71 100644
--- a/google_compute_engine/instance_setup/instance_config.py
+++ b/google_compute_engine/instance_setup/instance_config.py
@@ -62,6 +62,7 @@
           'ethernet_proto_id': '66',
       },
       'MetadataScripts': {
+          'run_dir': '',
           'startup': 'true',
           'shutdown': 'true',
       },
diff --git a/google_compute_engine/metadata_scripts/script_manager.py b/google_compute_engine/metadata_scripts/script_manager.py
index 2993c36..5cd0ebe 100755
--- a/google_compute_engine/metadata_scripts/script_manager.py
+++ b/google_compute_engine/metadata_scripts/script_manager.py
@@ -29,16 +29,17 @@
 
 
 @contextlib.contextmanager
-def _CreateTempDir(prefix):
+def _CreateTempDir(prefix, run_dir=None):
   """Context manager for creating a temporary directory.
 
   Args:
     prefix: string, the prefix for the temporary directory.
+    run_dir: string, the base directory location of the temporary directory.
 
   Yields:
     string, the temporary directory created.
   """
-  temp_dir = tempfile.mkdtemp(prefix=prefix + '-')
+  temp_dir = tempfile.mkdtemp(prefix=prefix + '-', dir=run_dir)
   try:
     yield temp_dir
   finally:
@@ -48,11 +49,12 @@
 class ScriptManager(object):
   """A class for retrieving and executing metadata scripts."""
 
-  def __init__(self, script_type, debug=False):
+  def __init__(self, script_type, run_dir=None, debug=False):
     """Constructor.
 
     Args:
       script_type: string, the metadata script type to run.
+      run_dir: string, the base directory location of the temporary directory.
       debug: bool, True if debug output should write to the console.
     """
     self.script_type = script_type
@@ -61,11 +63,15 @@
     self.logger = logger.Logger(name=name, debug=debug, facility=facility)
     self.retriever = script_retriever.ScriptRetriever(self.logger, script_type)
     self.executor = script_executor.ScriptExecutor(self.logger, script_type)
-    self._RunScripts()
+    self._RunScripts(run_dir=run_dir)
 
-  def _RunScripts(self):
-    """Retrieve metadata scripts and execute them."""
-    with _CreateTempDir(self.script_type) as dest_dir:
+  def _RunScripts(self, run_dir=None):
+    """Retrieve metadata scripts and execute them.
+
+    Args:
+      run_dir: string, the base directory location of the temporary directory.
+    """
+    with _CreateTempDir(self.script_type, run_dir=run_dir) as dest_dir:
       try:
         self.logger.info('Starting %s scripts.', self.script_type)
         script_dict = self.retriever.GetScripts(dest_dir)
@@ -92,7 +98,10 @@
 
   instance_config = config_manager.ConfigManager()
   if instance_config.GetOptionBool('MetadataScripts', script_type):
-    ScriptManager(script_type, debug=bool(options.debug))
+    ScriptManager(
+        script_type,
+        run_dir=instance_config.GetOptionString('MetadataScripts', 'run_dir'),
+        debug=bool(options.debug))
 
 
 if __name__ == '__main__':
diff --git a/google_compute_engine/metadata_scripts/tests/script_manager_test.py b/google_compute_engine/metadata_scripts/tests/script_manager_test.py
index 83fa879..ec7d61f 100644
--- a/google_compute_engine/metadata_scripts/tests/script_manager_test.py
+++ b/google_compute_engine/metadata_scripts/tests/script_manager_test.py
@@ -40,6 +40,7 @@
     mocks.attach_mock(mock_executor, 'executor')
     mocks.attach_mock(mock_logger, 'logger')
     mocks.attach_mock(mock_retriever, 'retriever')
+    run_dir = '/var/run'
     script_type = 'test'
     script_name = '%s-script' % script_type
     script_prefix = '%s-' % script_type
@@ -48,13 +49,13 @@
     mock_mkdir.return_value = test_dir
     mock_retriever_instance.GetScripts.return_value = test_dict
 
-    script_manager.ScriptManager(script_type)
+    script_manager.ScriptManager(script_type, run_dir=run_dir)
     expected_calls = [
         mock.call.logger.Logger(
             name=script_name, debug=False, facility=mock.ANY),
         mock.call.retriever.ScriptRetriever(mock_logger_instance, script_type),
         mock.call.executor.ScriptExecutor(mock_logger_instance, script_type),
-        mock.call.mkdir(prefix=script_prefix),
+        mock.call.mkdir(prefix=script_prefix, dir=run_dir),
         mock.call.logger.Logger().info(mock.ANY, script_type),
         mock.call.retriever.ScriptRetriever().GetScripts(test_dir),
         mock.call.executor.ScriptExecutor().RunScripts(test_dict),
diff --git a/google_compute_engine/metadata_watcher.py b/google_compute_engine/metadata_watcher.py
index 3ff648d..c24ac81 100644
--- a/google_compute_engine/metadata_watcher.py
+++ b/google_compute_engine/metadata_watcher.py
@@ -48,11 +48,14 @@
     while True:
       try:
         response = func(*args, **kwargs)
-      except urlerror.HTTPError as e:
-        if e.getcode() == httpclient.SERVICE_UNAVAILABLE:
-          time.sleep(5)
-        else:
-          raise
+      except (httpclient.HTTPException, socket.error, urlerror.URLError) as e:
+        time.sleep(5)
+        if (isinstance(e, urlerror.HTTPError) and
+            e.getcode() == httpclient.SERVICE_UNAVAILABLE):
+          continue
+        elif isinstance(e, socket.timeout):
+          continue
+        raise
       else:
         if response.getcode() == httpclient.OK:
           return response
@@ -173,7 +176,7 @@
           continue
         else:
           exception = e
-          self.logger.exception('GET request error retrieving metadata.')
+          self.logger.error('GET request error retrieving metadata. %s.', e)
 
   def WatchMetadata(
       self, handler, metadata_key='', recursive=True, timeout=None):
diff --git a/google_compute_engine/tests/metadata_watcher_test.py b/google_compute_engine/tests/metadata_watcher_test.py
index 2245711..e07ea94 100644
--- a/google_compute_engine/tests/metadata_watcher_test.py
+++ b/google_compute_engine/tests/metadata_watcher_test.py
@@ -94,12 +94,14 @@
     mock_unavailable = mock.Mock()
     mock_unavailable.getcode.return_value = (
         metadata_watcher.httpclient.SERVICE_UNAVAILABLE)
+    mock_timeout = metadata_watcher.socket.timeout('Test timeout')
     mock_success = mock.Mock()
     mock_success.getcode.return_value = metadata_watcher.httpclient.OK
 
     # Retry after a service unavailable error response.
     mock_open.open.side_effect = [
         metadata_watcher.StatusException(mock_unavailable),
+        mock_timeout,
         mock_success,
     ]
     request_url = '%s?' % self.url
@@ -117,14 +119,20 @@
         mock.call.proxy({}),
         mock.call.opener(mock_handler),
         mock.call.open.open(mock_request, timeout=timeout),
+        mock.call.time.sleep(mock.ANY),
+        mock.call.request(request_url, headers=headers),
+        mock.call.proxy({}),
+        mock.call.opener(mock_handler),
+        mock.call.open.open(mock_request, timeout=timeout),
     ]
     self.assertEqual(mocks.mock_calls, expected_calls)
 
+  @mock.patch('google_compute_engine.metadata_watcher.time')
   @mock.patch('google_compute_engine.metadata_watcher.urlrequest.build_opener')
   @mock.patch('google_compute_engine.metadata_watcher.urlrequest.ProxyHandler')
   @mock.patch('google_compute_engine.metadata_watcher.urlrequest.Request')
   def testGetMetadataRequestHttpException(
-      self, mock_request, mock_proxy, mock_opener):
+      self, mock_request, mock_proxy, mock_opener, mock_time):
     mock_open = mock.Mock()
     mock_handler = mock.Mock()
     mock_response = mock.Mock()
@@ -263,7 +271,7 @@
           timeout=None),
     ] * 4
     self.assertEqual(mock_response.mock_calls, expected_calls)
-    expected_calls = [mock.call.exception(mock.ANY)] * 2
+    expected_calls = [mock.call.error(mock.ANY, mock.ANY)] * 2
     self.assertEqual(self.mock_logger.mock_calls, expected_calls)
 
   def testWatchMetadata(self):
@@ -285,7 +293,7 @@
     mock_response = mock.Mock()
     mock_response.side_effect = metadata_watcher.socket.timeout()
     self.mock_watcher._GetMetadataUpdate = mock_response
-    self.mock_logger.exception.side_effect = RuntimeError()
+    self.mock_logger.error.side_effect = RuntimeError()
     metadata_key = 'instance/id'
     recursive = False
 
diff --git a/scripts/set_multiqueue b/scripts/set_multiqueue
index 5c722e3..9dfe45f 100755
--- a/scripts/set_multiqueue
+++ b/scripts/set_multiqueue
@@ -1,5 +1,5 @@
 #!/bin/bash
-# Copyright 2016 Google Inc. All Rights Reserved.
+# Copyright 2017 Google Inc. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -35,17 +35,17 @@
   ethtool -L "${1}" combined "${2}" > /dev/null 2>&1
 }
 
-echo "Running $(basename "$0")."
+echo "Running $(basename $0)."
 NET_DEVS=/sys/bus/virtio/drivers/virtio_net/virtio*
 
 # Loop through all the virtionet devices and enable multi-queue
-if [ -x /sbin/ethtool ]; then
+if [ -x "$(command -v ethtool)" ]; then
   for dev in $NET_DEVS; do
     ETH_DEVS=${dev}/net/*
     for eth_dev in $ETH_DEVS; do
       eth_dev=$(basename "$eth_dev")
       if ! errormsg=$(ethtool -l "$eth_dev" 2>&1); then
-        echo "/sbin/ethtool says that $eth_dev does not support virtionet multiqueue: $errormsg."
+        echo "ethtool says that $eth_dev does not support virtionet multiqueue: $errormsg."
         continue
       fi
       num_max_channels=$(ethtool -l "$eth_dev" | grep -m 1 Combined | cut -f2)
@@ -59,7 +59,7 @@
     done
   done
 else
-  echo "/sbin/ethtool not found: cannot configure virtionet multiqueue."
+  echo "ethtool not found: cannot configure virtionet multiqueue."
 fi
 
 for dev in $NET_DEVS
@@ -68,7 +68,7 @@
     irq_dir=/proc/irq/*
     for irq in $irq_dir
     do
-      smp_affinity="${irq}/smp_affinity"
+      smp_affinity="${irq}/smp_affinity_list"
       [ ! -f "${smp_affinity}" ] && continue
       # Classify this IRQ as virtionet intx, virtionet MSI-X, or non-virtionet
       # If the IRQ type is virtionet intx, a subdirectory with the same name as
@@ -78,7 +78,7 @@
       # a decimal integer ranging from 0 to K - 1 where K is the number of
       # input (output) queues in the virtionet device.
       virtionet_intx_dir="${irq}/${dev}"
-      virtionet_msix_dir_regex=".*/${dev}-(input|output)\.[0-9]+$"
+      virtionet_msix_dir_regex=".*/${dev}-(input|output)\.([0-9]+)$"
       if [ -d "${virtionet_intx_dir}" ]; then
         # All virtionet intx IRQs are delivered to CPU 0
         echo "Setting ${smp_affinity} to 01 for device ${dev}."
@@ -90,44 +90,46 @@
       for entry in ${irq}/${dev}*; do
         if [[ "$entry" =~ ${virtionet_msix_dir_regex} ]]; then
           virtionet_msix_found=1
+          queue_num=${BASH_REMATCH[2]}
         fi
       done
       affinity_hint="${irq}/affinity_hint"
       [ "$virtionet_msix_found" -eq 0 -o ! -f "${affinity_hint}" ] && continue
 
-      # The affinity hint file contains a CPU mask, consisting of
-      # groups of up to 8 hexadecimal digits, separated by a comma. Each bit
-      # position in the CPU mask hex value specifies whether this interrupt
-      # should be delivered to the corresponding CPU. For example, if bits 0
-      # and 3 are set in the affinity hint CPU mask hex value, then the
-      # interrupt should be delivered to CPUs 0 and 3. The virtionet device
-      # driver should set only a single bit in the affinity hint per MSI-X
-      # interrupt, ensuring each TX (RX) queue is used only by a single CPU.
-      # The virtionet driver will only specify an affinity hint if the number of
-      # TX (RX) queues equals the number of online CPUs. If no affinity hint is
-      # specified for an IRQ, the affinity hint file will contain all zeros.
-      affinity_cpumask=$(cat "${affinity_hint}")
-      affinity_hint_enabled=0
-      # Parse the affinity hint, skip if mask is invalid or is empty (all-zeros)
-      OIFS=${IFS}
-      IFS=","
-      for cpu_bitmap in ${affinity_cpumask}; do
-        bitmap_val=$(printf "%d" "0x${cpu_bitmap}" 2>/dev/null)
-        if [ "$?" -ne 0 ]; then
-          echo "Invalid affinity hint ${affinity_hint}: ${affinity_cpumask}."
-          affinity_hint_enabled=0
-          break
-        elif [ "${bitmap_val}" -ne 0 ]; then
-          affinity_hint_enabled=1
-        fi
-      done
-      IFS=${OIFS}
-      if [ "${affinity_hint_enabled}" -eq 0 ]; then
-        echo "Cannot set IRQ affinity ${smp_affinity}, affinity hint disabled."
-      else
-        # Set the IRQ CPU affinity to the virtionet-initialized affinity hint
-        echo "Setting ${smp_affinity} to ${affinity_cpumask} for device ${dev}."
-        echo "${affinity_cpumask}" > "${smp_affinity}"
-      fi
+      # Set the IRQ CPU affinity to the virtionet-initialized affinity hint
+      echo "Setting ${smp_affinity} to ${queue_num} for device ${dev}."
+      echo "${queue_num}" > "${smp_affinity}"
+      real_affinity=`cat ${smp_affinity}`
+      echo "${smp_affinity}: real affinity ${real_affinity}"
     done
 done
+
+XPS=/sys/class/net/e*/queues/tx*/xps_cpus
+num_cpus=$(nproc)
+
+num_queues=0
+for q in $XPS; do
+  num_queues=$((num_queues + 1))
+done
+
+# If we have more CPUs than queues, then stripe CPUs across tx affinity
+# as CPUNumber % queue_count.
+for q in $XPS; do
+  queue_re=".*tx-([0-9]+).*$"
+  if [[ "$q" =~ ${queue_re} ]]; then
+    queue_num=${BASH_REMATCH[1]}
+  fi
+
+  xps=0
+  for cpu in `seq $queue_num $num_queues $((num_cpus - 1))`; do
+    xps=$((xps | (1 << cpu)))
+  done
+
+  # Linux xps_cpus requires a hex number with commas every 32 bits.
+  # It ignores all bits above # cpus, so unconditionally write a
+  # 64 bit hex value, with a comma between dwords.
+  xps_string=`printf "%08x,%08x" $((xps >> 32 & 0xffffffff)) $((xps & 0xffffffff))`
+
+  echo ${xps_string} > $q
+  printf "Queue %d XPS=%s for %s\n" $queue_num `cat $q` $q
+done | sort -n -k2
diff --git a/setup.py b/setup.py
index 405682a..38c7470 100755
--- a/setup.py
+++ b/setup.py
@@ -32,7 +32,7 @@
     packages=setuptools.find_packages(),
     scripts=glob.glob('scripts/*'),
     url='https://github.com/GoogleCloudPlatform/compute-image-packages',
-    version='2.3.2',
+    version='2.3.3',
     # Entry points create scripts in /usr/bin that call a function.
     entry_points={
         'console_scripts': [