Skip to content

Commit

Permalink
Merge pull request #747 from tseaver/741-hoist_gae_gcd_detection
Browse files Browse the repository at this point in the history
#741: hoist GAE/GCD detection into `gcloud._helpers`
  • Loading branch information
tseaver committed Mar 20, 2015
2 parents 8948a49 + 14a7415 commit 9c09ced
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 174 deletions.
54 changes: 54 additions & 0 deletions gcloud/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,21 @@
This module is not part of the public API surface of `gcloud`.
"""
import socket

try:
from threading import local as Local
except ImportError: # pragma: NO COVER (who doesn't have it?)
class Local(object):
"""Placeholder for non-threaded applications."""

from six.moves.http_client import HTTPConnection # pylint: disable=F0401

try:
from google.appengine.api import app_identity
except ImportError:
app_identity = None


class _LocalStack(Local):
"""Manage a thread-local LIFO stack of resources.
Expand Down Expand Up @@ -102,3 +110,49 @@ def _lazy_property_deco(deferred_callable):
# For Python2.7+ deferred_callable.__func__ would suffice.
deferred_callable = deferred_callable.__get__(True)
return _LazyProperty(deferred_callable.__name__, deferred_callable)


def _app_engine_id():
"""Gets the App Engine application ID if it can be inferred.
:rtype: string or ``NoneType``
:returns: App Engine application ID if running in App Engine,
else ``None``.
"""
if app_identity is None:
return None

return app_identity.get_application_id()


def _compute_engine_id():
"""Gets the Compute Engine project ID if it can be inferred.
Uses 169.254.169.254 for the metadata server to avoid request
latency from DNS lookup.
See https://cloud.google.com/compute/docs/metadata#metadataserver
for information about this IP address. (This IP is also used for
Amazon EC2 instances, so the metadata flavor is crucial.)
See https://github.com/google/oauth2client/issues/93 for context about
DNS latency.
:rtype: string or ``NoneType``
:returns: Compute Engine project ID if the metadata service is available,
else ``None``.
"""
host = '169.254.169.254'
uri_path = '/computeMetadata/v1/project/project-id'
headers = {'Metadata-Flavor': 'Google'}
connection = HTTPConnection(host, timeout=0.1)

try:
connection.request('GET', uri_path, headers=headers)
response = connection.getresponse()
if response.status == 200:
return response.read()
except socket.error: # socket.timeout or socket.error(64, 'Host is down')
pass
finally:
connection.close()
60 changes: 4 additions & 56 deletions gcloud/datastore/_implicit_environ.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,9 @@
"""

import os
import socket

from six.moves.http_client import HTTPConnection # pylint: disable=F0401

try:
from google.appengine.api import app_identity
except ImportError:
app_identity = None

from gcloud._helpers import _app_engine_id
from gcloud._helpers import _compute_engine_id
from gcloud._helpers import _lazy_property_deco
from gcloud import credentials
from gcloud.datastore.connection import Connection
Expand All @@ -41,52 +35,6 @@
_GCD_DATASET_ENV_VAR_NAME = 'DATASTORE_DATASET'


def app_engine_id():
"""Gets the App Engine application ID if it can be inferred.
:rtype: string or ``NoneType``
:returns: App Engine application ID if running in App Engine,
else ``None``.
"""
if app_identity is None:
return None

return app_identity.get_application_id()


def compute_engine_id():
"""Gets the Compute Engine project ID if it can be inferred.
Uses 169.254.169.254 for the metadata server to avoid request
latency from DNS lookup.
See https://cloud.google.com/compute/docs/metadata#metadataserver
for information about this IP address. (This IP is also used for
Amazon EC2 instances, so the metadata flavor is crucial.)
See https://github.com/google/oauth2client/issues/93 for context about
DNS latency.
:rtype: string or ``NoneType``
:returns: Compute Engine project ID if the metadata service is available,
else ``None``.
"""
host = '169.254.169.254'
uri_path = '/computeMetadata/v1/project/project-id'
headers = {'Metadata-Flavor': 'Google'}
connection = HTTPConnection(host, timeout=0.1)

try:
connection.request('GET', uri_path, headers=headers)
response = connection.getresponse()
if response.status == 200:
return response.read()
except socket.error: # socket.timeout or socket.error(64, 'Host is down')
pass
finally:
connection.close()


def _get_production_dataset_id():
"""Gets the production application ID if it can be inferred."""
return os.getenv(_DATASET_ENV_VAR_NAME)
Expand Down Expand Up @@ -121,10 +69,10 @@ def _determine_default_dataset_id(dataset_id=None):
dataset_id = _get_gcd_dataset_id()

if dataset_id is None:
dataset_id = app_engine_id()
dataset_id = _app_engine_id()

if dataset_id is None:
dataset_id = compute_engine_id()
dataset_id = _compute_engine_id()

return dataset_id

Expand Down
120 changes: 2 additions & 118 deletions gcloud/datastore/test__implicit_environ.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,68 +121,6 @@ def test_value_set(self):
self.assertEqual(dataset_id, MOCK_DATASET_ID)


class Test_app_engine_id(unittest2.TestCase):

def _callFUT(self):
from gcloud.datastore import _implicit_environ
return _implicit_environ.app_engine_id()

def test_no_value(self):
from gcloud._testing import _Monkey
from gcloud.datastore import _implicit_environ

with _Monkey(_implicit_environ, app_identity=None):
dataset_id = self._callFUT()
self.assertEqual(dataset_id, None)

def test_value_set(self):
from gcloud._testing import _Monkey
from gcloud.datastore import _implicit_environ

APP_ENGINE_ID = object()
APP_IDENTITY = _AppIdentity(APP_ENGINE_ID)
with _Monkey(_implicit_environ, app_identity=APP_IDENTITY):
dataset_id = self._callFUT()
self.assertEqual(dataset_id, APP_ENGINE_ID)


class Test_compute_engine_id(unittest2.TestCase):

def _callFUT(self):
from gcloud.datastore import _implicit_environ
return _implicit_environ.compute_engine_id()

def _monkeyConnection(self, connection):
from gcloud._testing import _Monkey
from gcloud.datastore import _implicit_environ

def _factory(host, timeout):
connection.host = host
connection.timeout = timeout
return connection

return _Monkey(_implicit_environ, HTTPConnection=_factory)

def test_bad_status(self):
connection = _HTTPConnection(404, None)
with self._monkeyConnection(connection):
dataset_id = self._callFUT()
self.assertEqual(dataset_id, None)

def test_success(self):
COMPUTE_ENGINE_ID = object()
connection = _HTTPConnection(200, COMPUTE_ENGINE_ID)
with self._monkeyConnection(connection):
dataset_id = self._callFUT()
self.assertEqual(dataset_id, COMPUTE_ENGINE_ID)

def test_socket_raises(self):
connection = _TimeoutHTTPConnection()
with self._monkeyConnection(connection):
dataset_id = self._callFUT()
self.assertEqual(dataset_id, None)


class Test__determine_default_dataset_id(unittest2.TestCase):

def _callFUT(self, dataset_id=None):
Expand Down Expand Up @@ -216,8 +154,8 @@ def gce_mock():
patched_methods = {
'_get_production_dataset_id': prod_mock,
'_get_gcd_dataset_id': gcd_mock,
'app_engine_id': gae_mock,
'compute_engine_id': gce_mock,
'_app_engine_id': gae_mock,
'_compute_engine_id': gce_mock,
}

with _Monkey(_implicit_environ, **patched_methods):
Expand Down Expand Up @@ -412,57 +350,3 @@ def test_set_implicit(self):
self._callFUT()

self.assertEqual(_implicit_environ.get_default_connection(), fake_cnxn)


class _AppIdentity(object):

def __init__(self, app_id):
self.app_id = app_id

def get_application_id(self):
return self.app_id


class _HTTPResponse(object):

def __init__(self, status, data):
self.status = status
self.data = data

def read(self):
return self.data


class _BaseHTTPConnection(object):

host = timeout = None

def __init__(self):
self._close_count = 0
self._called_args = []
self._called_kwargs = []

def request(self, method, uri, **kwargs):
self._called_args.append((method, uri))
self._called_kwargs.append(kwargs)

def close(self):
self._close_count += 1


class _HTTPConnection(_BaseHTTPConnection):

def __init__(self, status, project_id):
super(_HTTPConnection, self).__init__()
self.status = status
self.project_id = project_id

def getresponse(self):
return _HTTPResponse(self.status, self.project_id)


class _TimeoutHTTPConnection(_BaseHTTPConnection):

def getresponse(self):
import socket
raise socket.timeout('timed out')
Loading

0 comments on commit 9c09ced

Please sign in to comment.