summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMax Illfelder <illfelder@google.com>2016-11-09 12:45:06 -0800
committerMax Illfelder <illfelder@google.com>2016-11-09 12:45:06 -0800
commit3cb1e5e85c1d268d99103100164264747f030c88 (patch)
tree6754b763f36405457ec5ad728ed4cec90e733830
parent14a50a6217a39e1f50f2ebade802cf76500a673a (diff)
parenta33d244812a53c8c5ff974ce3225f4f540ad7e59 (diff)
downloadgoogle-compute-image-packages-3cb1e5e85c1d268d99103100164264747f030c88.tar.gz
Merge branch 'development'
-rw-r--r--README.md5
-rwxr-xr-xgoogle_compute_engine/accounts/accounts_daemon.py5
-rw-r--r--google_compute_engine/accounts/accounts_utils.py14
-rw-r--r--google_compute_engine/accounts/tests/accounts_daemon_test.py2
-rw-r--r--google_compute_engine/accounts/tests/accounts_utils_test.py19
-rwxr-xr-xgoogle_compute_engine/ip_forwarding/ip_forwarding_daemon.py19
-rw-r--r--google_compute_engine/ip_forwarding/ip_forwarding_utils.py9
-rw-r--r--google_compute_engine/ip_forwarding/tests/ip_forwarding_daemon_test.py22
-rw-r--r--google_compute_engine/ip_forwarding/tests/ip_forwarding_utils_test.py30
-rw-r--r--google_compute_engine/metadata_scripts/script_retriever.py8
-rw-r--r--google_compute_engine/metadata_scripts/tests/script_retriever_test.py15
-rw-r--r--google_compute_engine/metadata_watcher.py42
-rw-r--r--google_compute_engine/tests/metadata_watcher_test.py31
-rwxr-xr-xsetup.py2
14 files changed, 169 insertions, 54 deletions
diff --git a/README.md b/README.md
index 17925a2..3422f64 100644
--- a/README.md
+++ b/README.md
@@ -227,6 +227,11 @@ using a configuration file. To make configuration changes, add settings to
`/etc/default/instance_configs.cfg.template`. Settings are not overridden in the
guest.
+Linux distributions looking to include their own defaults can specify settings
+in `/etc/default/instance_configs.cfg.distro`. These settings will not override
+`/etc/default/instance_configs.cfg.template`. This enables distribution settings
+that do not override user configuration during package update.
+
The following are valid user configuration options.
Section | Option | Value
diff --git a/google_compute_engine/accounts/accounts_daemon.py b/google_compute_engine/accounts/accounts_daemon.py
index 5b9779d..fd20f80 100755
--- a/google_compute_engine/accounts/accounts_daemon.py
+++ b/google_compute_engine/accounts/accounts_daemon.py
@@ -19,6 +19,7 @@ import datetime
import json
import logging.handlers
import optparse
+import random
from google_compute_engine import config_manager
from google_compute_engine import file_utils
@@ -51,7 +52,9 @@ class AccountsDaemon(object):
try:
with file_utils.LockFile(LOCKFILE):
self.logger.info('Starting Google Accounts daemon.')
- self.watcher.WatchMetadata(self.HandleAccounts, recursive=True)
+ timeout = 60 + random.randint(0, 30)
+ self.watcher.WatchMetadata(
+ self.HandleAccounts, recursive=True, timeout=timeout)
except (IOError, OSError) as e:
self.logger.warning(str(e))
diff --git a/google_compute_engine/accounts/accounts_utils.py b/google_compute_engine/accounts/accounts_utils.py
index 4096086..fbdd5ef 100644
--- a/google_compute_engine/accounts/accounts_utils.py
+++ b/google_compute_engine/accounts/accounts_utils.py
@@ -76,10 +76,16 @@ class AccountsUtils(object):
self.logger.warning('Could not create the sudoers group. %s.', str(e))
if not os.path.exists(self.google_sudoers_file):
- with open(self.google_sudoers_file, 'w') as group:
- message = '%{0} ALL=(ALL:ALL) NOPASSWD:ALL'.format(
- self.google_sudoers_group)
- group.write(message)
+ try:
+ with open(self.google_sudoers_file, 'w') as group:
+ message = '%{0} ALL=(ALL:ALL) NOPASSWD:ALL'.format(
+ self.google_sudoers_group)
+ group.write(message)
+ except IOError as e:
+ self.logger.error(
+ 'Could not write sudoers file. %s. %s',
+ self.google_sudoers_file, str(e))
+ return
file_utils.SetPermissions(
self.google_sudoers_file, mode=0o440, uid=0, gid=0)
diff --git a/google_compute_engine/accounts/tests/accounts_daemon_test.py b/google_compute_engine/accounts/tests/accounts_daemon_test.py
index 3fd830d..2b6700b 100644
--- a/google_compute_engine/accounts/tests/accounts_daemon_test.py
+++ b/google_compute_engine/accounts/tests/accounts_daemon_test.py
@@ -59,7 +59,7 @@ class AccountsDaemonTest(unittest.TestCase):
mock.call.lock.LockFile().__enter__(),
mock.call.logger.Logger().info(mock.ANY),
mock.call.watcher.MetadataWatcher().WatchMetadata(
- mock_handle, recursive=True),
+ mock_handle, recursive=True, timeout=mock.ANY),
mock.call.lock.LockFile().__exit__(None, None, None),
]
self.assertEqual(mocks.mock_calls, expected_calls)
diff --git a/google_compute_engine/accounts/tests/accounts_utils_test.py b/google_compute_engine/accounts/tests/accounts_utils_test.py
index c10d963..7401831 100644
--- a/google_compute_engine/accounts/tests/accounts_utils_test.py
+++ b/google_compute_engine/accounts/tests/accounts_utils_test.py
@@ -148,6 +148,25 @@ class AccountsUtilsTest(unittest.TestCase):
]
self.assertEqual(mocks.mock_calls, expected_calls)
+ @mock.patch('google_compute_engine.accounts.accounts_utils.os.path.exists')
+ def testCreateSudoersGroupWriteError(self, mock_exists):
+ mock_open = mock.mock_open()
+ mocks = mock.Mock()
+ mocks.attach_mock(mock_exists, 'exists')
+ mocks.attach_mock(self.mock_utils._GetGroup, 'group')
+ mocks.attach_mock(self.mock_logger, 'logger')
+ self.mock_utils._GetGroup.return_value = True
+ mock_exists.return_value = False
+ mock_open.side_effect = IOError()
+
+ accounts_utils.AccountsUtils._CreateSudoersGroup(self.mock_utils)
+ expected_calls = [
+ mock.call.group(self.sudoers_group),
+ mock.call.exists(self.sudoers_file),
+ mock.call.logger.error(mock.ANY, self.sudoers_file, mock.ANY),
+ ]
+ self.assertEqual(mocks.mock_calls, expected_calls)
+
@mock.patch('google_compute_engine.accounts.accounts_utils.pwd')
def testGetUser(self, mock_pwd):
mock_pwd.getpwnam.return_value = 'Test'
diff --git a/google_compute_engine/ip_forwarding/ip_forwarding_daemon.py b/google_compute_engine/ip_forwarding/ip_forwarding_daemon.py
index 04c0e09..45b0ce7 100755
--- a/google_compute_engine/ip_forwarding/ip_forwarding_daemon.py
+++ b/google_compute_engine/ip_forwarding/ip_forwarding_daemon.py
@@ -16,8 +16,9 @@
"""Manage IP forwarding on a Google Compute Engine instance.
Fetch a list of public endpoint IPs from the metadata server, compare it with
-the IPs configured on eth0, and add or remove addresses from eth0 to make them
-match. Only remove those which match our proto code.
+the IPs configured the associated interfaces, and add or remove addresses from
+the interfaces to make them match. Only remove those which match our proto
+code.
Command used to add IPs:
ip route add to local $IP/32 dev eth0 proto 66
@@ -27,6 +28,7 @@ Command used to fetch list of configured IPs:
import logging.handlers
import optparse
+import random
from google_compute_engine import config_manager
from google_compute_engine import file_utils
@@ -61,9 +63,10 @@ class IpForwardingDaemon(object):
try:
with file_utils.LockFile(LOCKFILE):
self.logger.info('Starting Google IP Forwarding daemon.')
+ timeout = 60 + random.randint(0, 30)
self.watcher.WatchMetadata(
self.HandleNetworkInterfaces, metadata_key=self.network_interfaces,
- recursive=True)
+ recursive=True, timeout=timeout)
except (IOError, OSError) as e:
self.logger.warning(str(e))
@@ -81,7 +84,7 @@ class IpForwardingDaemon(object):
if not to_add and not to_remove:
return
self.logger.info(
- 'Changing %s forwarded IPs from %s to %s by adding %s and removing %s.',
+ 'Changing %s IPs from %s to %s by adding %s and removing %s.',
interface, configured or None, desired or None, to_add or None,
to_remove or None)
@@ -125,14 +128,16 @@ class IpForwardingDaemon(object):
"""Called when network interface metadata changes.
Args:
- result: string, the metadata response with the new network interfaces.
+ result: dict, the metadata response with the new network interfaces.
"""
for network_interface in result:
mac_address = network_interface.get('mac')
interface = self.network_utils.GetNetworkInterface(mac_address)
+ ip_addresses = []
if interface:
- forwarded_ips = network_interface.get('forwardedIps')
- self._HandleForwardedIps(forwarded_ips, interface)
+ ip_addresses.extend(network_interface.get('forwardedIps', []))
+ ip_addresses.extend(network_interface.get('ipAliases', []))
+ self._HandleForwardedIps(ip_addresses, interface)
else:
message = 'Network interface not found for MAC address: %s.'
self.logger.warning(message, mac_address)
diff --git a/google_compute_engine/ip_forwarding/ip_forwarding_utils.py b/google_compute_engine/ip_forwarding/ip_forwarding_utils.py
index c7f6cdf..201d326 100644
--- a/google_compute_engine/ip_forwarding/ip_forwarding_utils.py
+++ b/google_compute_engine/ip_forwarding/ip_forwarding_utils.py
@@ -19,6 +19,7 @@ import re
import subprocess
IP_REGEX = re.compile(r'\A(\d{1,3}\.){3}\d{1,3}\Z')
+IP_ALIAS_REGEX = re.compile(r'\A(\d{1,3}\.){3}\d{1,3}/\d{1,2}\Z')
class IpForwardingUtils(object):
@@ -92,7 +93,7 @@ class IpForwardingUtils(object):
addresses = []
forwarded_ips = forwarded_ips or []
for ip in forwarded_ips:
- if ip and IP_REGEX.match(ip):
+ if ip and (IP_REGEX.match(ip) or IP_ALIAS_REGEX.match(ip)):
addresses.append(ip)
else:
self.logger.warning('Could not parse IP address: "%s".', ip)
@@ -119,7 +120,8 @@ class IpForwardingUtils(object):
address: string, the IP address to configure.
interface: string, the output device to use.
"""
- args = ['add', 'to', 'local', '%s/32' % address]
+ address = address if IP_ALIAS_REGEX.match(address) else '%s/32' % address
+ args = ['add', 'to', 'local', address]
options = self._CreateRouteOptions(dev=interface)
self._RunIpRoute(args=args, options=options)
@@ -130,6 +132,7 @@ class IpForwardingUtils(object):
address: string, the IP address to configure.
interface: string, the output device to use.
"""
- args = ['delete', 'to', 'local', '%s/32' % address]
+ address = address if IP_ALIAS_REGEX.match(address) else '%s/32' % address
+ args = ['delete', 'to', 'local', address]
options = self._CreateRouteOptions(dev=interface)
self._RunIpRoute(args=args, options=options)
diff --git a/google_compute_engine/ip_forwarding/tests/ip_forwarding_daemon_test.py b/google_compute_engine/ip_forwarding/tests/ip_forwarding_daemon_test.py
index 71dee68..4c0f32f 100644
--- a/google_compute_engine/ip_forwarding/tests/ip_forwarding_daemon_test.py
+++ b/google_compute_engine/ip_forwarding/tests/ip_forwarding_daemon_test.py
@@ -66,7 +66,8 @@ class IpForwardingDaemonTest(unittest.TestCase):
mock.call.lock.LockFile().__enter__(),
mock.call.logger.Logger().info(mock.ANY),
mock.call.watcher.MetadataWatcher().WatchMetadata(
- mock_handle, metadata_key=metadata_key, recursive=True),
+ mock_handle, metadata_key=metadata_key, recursive=True,
+ timeout=mock.ANY),
mock.call.lock.LockFile().__exit__(None, None, None),
]
self.assertEqual(mocks.mock_calls, expected_calls)
@@ -179,23 +180,26 @@ class IpForwardingDaemonTest(unittest.TestCase):
mocks.attach_mock(self.mock_network_utils, 'network')
mocks.attach_mock(self.mock_setup, 'setup')
self.mock_network_utils.GetNetworkInterface.side_effect = [
- 'eth0', 'eth1', 'eth2', None]
+ 'eth0', 'eth1', 'eth2', 'eth3', None]
result = [
- {'mac': '1', 'forwardedIps': 'a'},
- {'mac': '2', 'forwardedIps': 'b'},
- {'mac': '3'},
- {'forwardedIps': 'c'},
+ {'mac': '1', 'forwardedIps': ['a']},
+ {'mac': '2', 'forwardedIps': ['b'], 'ipAliases': ['banana']},
+ {'mac': '3', 'ipAliases': ['cherry']},
+ {'mac': '4'},
+ {'forwardedIps': ['d'], 'ipAliases': ['date']},
]
ip_forwarding_daemon.IpForwardingDaemon.HandleNetworkInterfaces(
self.mock_setup, result)
expected_calls = [
mock.call.network.GetNetworkInterface('1'),
- mock.call.setup._HandleForwardedIps('a', 'eth0'),
+ mock.call.setup._HandleForwardedIps(['a'], 'eth0'),
mock.call.network.GetNetworkInterface('2'),
- mock.call.setup._HandleForwardedIps('b', 'eth1'),
+ mock.call.setup._HandleForwardedIps(['b', 'banana'], 'eth1'),
mock.call.network.GetNetworkInterface('3'),
- mock.call.setup._HandleForwardedIps(None, 'eth2'),
+ mock.call.setup._HandleForwardedIps(['cherry'], 'eth2'),
+ mock.call.network.GetNetworkInterface('4'),
+ mock.call.setup._HandleForwardedIps([], 'eth3'),
mock.call.network.GetNetworkInterface(None),
mock.call.setup.logger.warning(mock.ANY, None),
]
diff --git a/google_compute_engine/ip_forwarding/tests/ip_forwarding_utils_test.py b/google_compute_engine/ip_forwarding/tests/ip_forwarding_utils_test.py
index 18774a6..5543d13 100644
--- a/google_compute_engine/ip_forwarding/tests/ip_forwarding_utils_test.py
+++ b/google_compute_engine/ip_forwarding/tests/ip_forwarding_utils_test.py
@@ -137,6 +137,12 @@ class IpForwardingUtilsTest(unittest.TestCase):
'1.1.1.a': False,
None: False,
'1.0.0.0': True,
+ '1.1.1.1/1': True,
+ '1.1.1.1/11': True,
+ '123.123.123.123/1': True,
+ '123.123.123.123/123': False,
+ '123.123.123.123/a': False,
+ '123.123.123.123/': False,
}
input_ips = forwarded_ips.keys()
valid_ips = [ip for ip, valid in forwarded_ips.items() if valid]
@@ -175,6 +181,18 @@ class IpForwardingUtilsTest(unittest.TestCase):
mock_run.assert_called_once_with(
args=['add', 'to', 'local', '1.1.1.1/32'], options=self.options)
+ def testAddIpAlias(self):
+ mock_options = mock.Mock()
+ mock_options.return_value = self.options
+ mock_run = mock.Mock()
+ self.mock_utils._CreateRouteOptions = mock_options
+ self.mock_utils._RunIpRoute = mock_run
+
+ self.mock_utils.AddForwardedIp('1.1.1.1/24', 'interface')
+ mock_options.assert_called_once_with(dev='interface')
+ mock_run.assert_called_once_with(
+ args=['add', 'to', 'local', '1.1.1.1/24'], options=self.options)
+
def testRemoveForwardedIp(self):
mock_options = mock.Mock()
mock_options.return_value = self.options
@@ -187,6 +205,18 @@ class IpForwardingUtilsTest(unittest.TestCase):
mock_run.assert_called_once_with(
args=['delete', 'to', 'local', '1.1.1.1/32'], options=self.options)
+ def testRemoveAliasIp(self):
+ mock_options = mock.Mock()
+ mock_options.return_value = self.options
+ mock_run = mock.Mock()
+ self.mock_utils._CreateRouteOptions = mock_options
+ self.mock_utils._RunIpRoute = mock_run
+
+ self.mock_utils.RemoveForwardedIp('1.1.1.1/24', 'interface')
+ mock_options.assert_called_once_with(dev='interface')
+ mock_run.assert_called_once_with(
+ args=['delete', 'to', 'local', '1.1.1.1/24'], options=self.options)
+
if __name__ == '__main__':
unittest.main()
diff --git a/google_compute_engine/metadata_scripts/script_retriever.py b/google_compute_engine/metadata_scripts/script_retriever.py
index a5c77a5..2568c8b 100644
--- a/google_compute_engine/metadata_scripts/script_retriever.py
+++ b/google_compute_engine/metadata_scripts/script_retriever.py
@@ -50,6 +50,14 @@ class ScriptRetriever(object):
Returns:
string, the path to the file storing the metadata script.
"""
+ try:
+ subprocess.check_call(
+ ['which', 'gsutil'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ except subprocess.CalledProcessError:
+ self.logger.warning(
+ 'gsutil is not installed, cannot download items from Google Storage.')
+ return None
+
dest_file = tempfile.NamedTemporaryFile(dir=dest_dir, delete=False)
dest_file.close()
dest = dest_file.name
diff --git a/google_compute_engine/metadata_scripts/tests/script_retriever_test.py b/google_compute_engine/metadata_scripts/tests/script_retriever_test.py
index 9fa1e79..60701d4 100644
--- a/google_compute_engine/metadata_scripts/tests/script_retriever_test.py
+++ b/google_compute_engine/metadata_scripts/tests/script_retriever_test.py
@@ -34,6 +34,15 @@ class ScriptRetrieverTest(unittest.TestCase):
self.mock_logger, self.script_type)
@mock.patch('google_compute_engine.metadata_scripts.script_retriever.subprocess.check_call')
+ def testDownloadGsNoExec(self, mock_call):
+ mock_call.side_effect = subprocess.CalledProcessError('foo', 'bar')
+ gs_url = 'gs://fake/url'
+ self.assertIsNone(self.retriever._DownloadGsUrl(gs_url, self.dest_dir))
+ mock_call.assert_called_once_with(
+ ['which', 'gsutil'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ self.mock_logger.warning.assert_called_once_with(mock.ANY)
+
+ @mock.patch('google_compute_engine.metadata_scripts.script_retriever.subprocess.check_call')
@mock.patch('google_compute_engine.metadata_scripts.script_retriever.tempfile.NamedTemporaryFile')
def testDownloadGsUrl(self, mock_tempfile, mock_call):
gs_url = 'gs://fake/url'
@@ -44,7 +53,7 @@ class ScriptRetrieverTest(unittest.TestCase):
mock_tempfile.assert_called_once_with(dir=self.dest_dir, delete=False)
mock_tempfile.close.assert_called_once_with()
self.mock_logger.info.assert_called_once_with(mock.ANY, gs_url, self.dest)
- mock_call.assert_called_once_with(['gsutil', 'cp', gs_url, self.dest])
+ mock_call.assert_called_with(['gsutil', 'cp', gs_url, self.dest])
self.mock_logger.warning.assert_not_called()
@mock.patch('google_compute_engine.metadata_scripts.script_retriever.subprocess.check_call')
@@ -53,7 +62,7 @@ class ScriptRetrieverTest(unittest.TestCase):
gs_url = 'gs://fake/url'
mock_tempfile.return_value = mock_tempfile
mock_tempfile.name = self.dest
- mock_call.side_effect = subprocess.CalledProcessError(1, 'Test')
+ mock_call.side_effect = [0, subprocess.CalledProcessError(1, 'Test')]
self.assertIsNone(self.retriever._DownloadGsUrl(gs_url, self.dest_dir))
self.assertEqual(self.mock_logger.warning.call_count, 1)
@@ -63,7 +72,7 @@ class ScriptRetrieverTest(unittest.TestCase):
gs_url = 'gs://fake/url'
mock_tempfile.return_value = mock_tempfile
mock_tempfile.name = self.dest
- mock_call.side_effect = Exception('Error.')
+ mock_call.side_effect = [0, Exception('Error.')]
self.assertIsNone(self.retriever._DownloadGsUrl(gs_url, self.dest_dir))
self.assertEqual(self.mock_logger.warning.call_count, 1)
diff --git a/google_compute_engine/metadata_watcher.py b/google_compute_engine/metadata_watcher.py
index e7da49b..3ff648d 100644
--- a/google_compute_engine/metadata_watcher.py
+++ b/google_compute_engine/metadata_watcher.py
@@ -76,12 +76,13 @@ class MetadataWatcher(object):
self.timeout = timeout
@RetryOnUnavailable
- def _GetMetadataRequest(self, metadata_url, params=None):
+ def _GetMetadataRequest(self, metadata_url, params=None, timeout=None):
"""Performs a GET request with the metadata headers.
Args:
metadata_url: string, the URL to perform a GET request on.
params: dictionary, the query parameters in the GET request.
+ timeout: int, timeout in seconds for metadata requests.
Returns:
HTTP response from the GET request.
@@ -94,7 +95,8 @@ class MetadataWatcher(object):
url = '%s?%s' % (metadata_url, params)
request = urlrequest.Request(url, headers=headers)
request_opener = urlrequest.build_opener(urlrequest.ProxyHandler({}))
- return request_opener.open(request, timeout=self.timeout*1.1)
+ timeout = timeout or self.timeout
+ return request_opener.open(request, timeout=timeout*1.1)
def _UpdateEtag(self, response):
"""Update the etag from an API response.
@@ -110,13 +112,15 @@ class MetadataWatcher(object):
self.etag = etag
return etag_updated
- def _GetMetadataUpdate(self, metadata_key='', recursive=True, wait=True):
+ def _GetMetadataUpdate(
+ self, metadata_key='', recursive=True, wait=True, timeout=None):
"""Request the contents of metadata server and deserialize the response.
Args:
metadata_key: string, the metadata key to watch for changes.
recursive: bool, True if we should recursively watch for metadata changes.
wait: bool, True if we should wait for a metadata change.
+ timeout: int, timeout in seconds for returning metadata output.
Returns:
json, the deserialized contents of the metadata server.
@@ -127,27 +131,33 @@ class MetadataWatcher(object):
'alt': 'json',
'last_etag': self.etag,
'recursive': recursive,
- 'timeout_sec': self.timeout,
+ 'timeout_sec': timeout or self.timeout,
'wait_for_change': wait,
}
while True:
- response = self._GetMetadataRequest(metadata_url, params=params)
+ response = self._GetMetadataRequest(
+ metadata_url, params=params, timeout=timeout)
etag_updated = self._UpdateEtag(response)
- if wait and not etag_updated:
+ if wait and not etag_updated and not timeout:
# Retry until the etag is updated.
continue
else:
- # Waiting for change is not required or the etag is updated.
+ # One of the following are true:
+ # - Waiting for change is not required.
+ # - The etag is updated.
+ # - The user specified a request timeout.
break
return json.loads(response.read().decode('utf-8'))
- def _HandleMetadataUpdate(self, metadata_key='', recursive=True, wait=True):
+ def _HandleMetadataUpdate(
+ self, metadata_key='', recursive=True, wait=True, timeout=None):
"""Wait for a successful metadata response.
Args:
metadata_key: string, the metadata key to watch for changes.
recursive: bool, True if we should recursively watch for metadata changes.
wait: bool, True if we should wait for a metadata change.
+ timeout: int, timeout in seconds for returning metadata output.
Returns:
json, the deserialized contents of the metadata server.
@@ -156,7 +166,8 @@ class MetadataWatcher(object):
while True:
try:
return self._GetMetadataUpdate(
- metadata_key=metadata_key, recursive=recursive, wait=wait)
+ metadata_key=metadata_key, recursive=recursive, wait=wait,
+ timeout=timeout)
except (httpclient.HTTPException, socket.error, urlerror.URLError) as e:
if isinstance(e, type(exception)):
continue
@@ -164,31 +175,36 @@ class MetadataWatcher(object):
exception = e
self.logger.exception('GET request error retrieving metadata.')
- def WatchMetadata(self, handler, metadata_key='', recursive=True):
+ def WatchMetadata(
+ self, handler, metadata_key='', recursive=True, timeout=None):
"""Watch for changes to the contents of the metadata server.
Args:
handler: callable, a function to call with the updated metadata contents.
metadata_key: string, the metadata key to watch for changes.
recursive: bool, True if we should recursively watch for metadata changes.
+ timeout: int, timeout in seconds for returning metadata output.
"""
while True:
response = self._HandleMetadataUpdate(
- metadata_key=metadata_key, recursive=recursive, wait=True)
+ metadata_key=metadata_key, recursive=recursive, wait=True,
+ timeout=timeout)
try:
handler(response)
except Exception as e:
self.logger.exception('Exception calling the response handler. %s.', e)
- def GetMetadata(self, metadata_key='', recursive=True):
+ def GetMetadata(self, metadata_key='', recursive=True, timeout=None):
"""Retrieve the contents of metadata server for a metadata key.
Args:
metadata_key: string, the metadata key to watch for changes.
recursive: bool, True if we should recursively watch for metadata changes.
+ timeout: int, timeout in seconds for returning metadata output.
Returns:
json, the deserialized contents of the metadata server or None if error.
"""
return self._HandleMetadataUpdate(
- metadata_key=metadata_key, recursive=recursive, wait=False)
+ metadata_key=metadata_key, recursive=recursive, wait=False,
+ timeout=timeout)
diff --git a/google_compute_engine/tests/metadata_watcher_test.py b/google_compute_engine/tests/metadata_watcher_test.py
index a1e5240..2245711 100644
--- a/google_compute_engine/tests/metadata_watcher_test.py
+++ b/google_compute_engine/tests/metadata_watcher_test.py
@@ -191,7 +191,8 @@ class MetadataWatcherTest(unittest.TestCase):
self.assertEqual(self.mock_watcher._GetMetadataUpdate(), {})
self.assertEqual(self.mock_watcher.etag, 1)
- mock_response.assert_called_once_with(request_url, params=self.params)
+ mock_response.assert_called_once_with(
+ request_url, params=self.params, timeout=None)
def testGetMetadataUpdateArgs(self):
mock_response = mock.Mock()
@@ -205,9 +206,10 @@ class MetadataWatcherTest(unittest.TestCase):
request_url = os.path.join(self.url, metadata_key)
self.mock_watcher._GetMetadataUpdate(
- metadata_key=metadata_key, recursive=False, wait=False)
+ metadata_key=metadata_key, recursive=False, wait=False, timeout=60)
self.assertEqual(self.mock_watcher.etag, 0)
- mock_response.assert_called_once_with(request_url, params=self.params)
+ mock_response.assert_called_once_with(
+ request_url, params=self.params, timeout=60)
def testGetMetadataUpdateWait(self):
self.params['last_etag'] = 1
@@ -225,7 +227,9 @@ class MetadataWatcherTest(unittest.TestCase):
self.mock_watcher._GetMetadataUpdate()
self.assertEqual(self.mock_watcher.etag, 2)
- expected_calls = [mock.call(request_url, params=self.params)] * 3
+ expected_calls = [
+ mock.call(request_url, params=self.params, timeout=None),
+ ] * 3
self.assertEqual(mock_response.mock_calls, expected_calls)
def testHandleMetadataUpdate(self):
@@ -235,7 +239,7 @@ class MetadataWatcherTest(unittest.TestCase):
self.assertEqual(self.mock_watcher.GetMetadata(), {})
mock_response.assert_called_once_with(
- metadata_key='', recursive=True, wait=False)
+ metadata_key='', recursive=True, wait=False, timeout=None)
self.mock_watcher.logger.exception.assert_not_called()
def testHandleMetadataUpdateException(self):
@@ -250,10 +254,13 @@ class MetadataWatcherTest(unittest.TestCase):
self.assertEqual(
self.mock_watcher._HandleMetadataUpdate(
- metadata_key=metadata_key, recursive=recursive, wait=wait),
+ metadata_key=metadata_key, recursive=recursive, wait=wait,
+ timeout=None),
{})
expected_calls = [
- mock.call(metadata_key=metadata_key, recursive=recursive, wait=wait),
+ mock.call(
+ metadata_key=metadata_key, recursive=recursive, wait=wait,
+ timeout=None),
] * 4
self.assertEqual(mock_response.mock_calls, expected_calls)
expected_calls = [mock.call.exception(mock.ANY)] * 2
@@ -272,7 +279,7 @@ class MetadataWatcherTest(unittest.TestCase):
self.mock_watcher.WatchMetadata(mock_handler, recursive=recursive)
mock_handler.assert_called_once_with({})
mock_response.assert_called_once_with(
- metadata_key='', recursive=recursive, wait=True)
+ metadata_key='', recursive=recursive, wait=True, timeout=None)
def testWatchMetadataException(self):
mock_response = mock.Mock()
@@ -286,7 +293,7 @@ class MetadataWatcherTest(unittest.TestCase):
self.mock_watcher.WatchMetadata(
None, metadata_key=metadata_key, recursive=recursive)
mock_response.assert_called_once_with(
- metadata_key=metadata_key, recursive=recursive, wait=True)
+ metadata_key=metadata_key, recursive=recursive, wait=True, timeout=None)
def testGetMetadata(self):
mock_response = mock.Mock()
@@ -295,7 +302,7 @@ class MetadataWatcherTest(unittest.TestCase):
self.assertEqual(self.mock_watcher.GetMetadata(), {})
mock_response.assert_called_once_with(
- metadata_key='', recursive=True, wait=False)
+ metadata_key='', recursive=True, wait=False, timeout=None)
self.mock_watcher.logger.exception.assert_not_called()
def testGetMetadataArgs(self):
@@ -306,10 +313,10 @@ class MetadataWatcherTest(unittest.TestCase):
recursive = False
response = self.mock_watcher.GetMetadata(
- metadata_key=metadata_key, recursive=recursive)
+ metadata_key=metadata_key, recursive=recursive, timeout=60)
self.assertEqual(response, {})
mock_response.assert_called_once_with(
- metadata_key=metadata_key, recursive=False, wait=False)
+ metadata_key=metadata_key, recursive=False, wait=False, timeout=60)
self.mock_watcher.logger.exception.assert_not_called()
diff --git a/setup.py b/setup.py
index c9adf14..26636a5 100755
--- a/setup.py
+++ b/setup.py
@@ -25,7 +25,7 @@ setuptools.setup(
author_email='gc-team@google.com',
description='Google Compute Engine',
include_package_data=True,
- install_requires=['boto'],
+ install_requires=['boto', 'setuptools'],
license='Apache Software License',
long_description='Google Compute Engine guest environment.',
name='google-compute-engine',