Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add compute engine metadata client #11

Merged
merged 7 commits into from
Oct 7, 2016
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
44 changes: 44 additions & 0 deletions google/auth/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import datetime

import six
from six.moves import urllib


def utcnow():
Expand Down Expand Up @@ -88,3 +89,46 @@ def from_bytes(value):
else:
raise ValueError(
'{0!r} could not be converted to unicode'.format(value))


def update_query(url, params):
"""Updates a URL's query parameters.

Replaces any current values if they are already present in the URL.

This comment was marked as spam.

This comment was marked as spam.


Args:
url (str): The URL to update.
params (Mapping[str, Optional[str]]): A mapping of query parameter
keys to values. If the value of a parameter is `None`, the query
parameter will be removed from the query string.

Returns:
str: The URL with updated query parameters.

Examples:

>>> url = 'http://example.com?a=1'
>>> update_query(url, {'a': '2'})
http://example.com?a=2
>>> update_query(url, {'b': '3'})
http://example.com?a=1&b=3
>> update_query(url, {'a': None, 'b': '3'})

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

http://example.com?b=3

"""
# Split the URL into parts.
parts = urllib.parse.urlparse(url)
# Parse the query string.
query_params = urllib.parse.parse_qs(parts.query)
# Update the query parameters with the new parameters.
query_params.update(params)

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

# Remove any None values.
query_params = {
key: value for key, value
in six.iteritems(query_params)
if value is not None}

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

# Re-encoded the query string.
new_query = urllib.parse.urlencode(query_params, doseq=True)
# Unsplit the url.
new_parts = parts._replace(query=new_query)
return urllib.parse.urlunparse(new_parts)
13 changes: 13 additions & 0 deletions google/auth/compute_engine/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2016 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
173 changes: 173 additions & 0 deletions google/auth/compute_engine/_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Copyright 2016 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Provides helper methods for talking to the Compute Engine metadata server.

See https://cloud.google.com/compute/docs/metadata for more details.
"""

import datetime
import json
import os

from six.moves import http_client
from six.moves.urllib import parse as urlparse

from google.auth import _helpers
from google.auth import exceptions

_METADATA_ROOT = 'http://metadata.google.internal/computeMetadata/v1/'

# This is used to ping the metadata server, it avoids the cost of a DNS
# lookup.
_METADATA_IP_ROOT = 'http://169.254.169.254'
_METADATA_FLAVOR_HEADER = 'metdata-flavor'
_METADATA_FLAVOR_VALUE = 'Google'
_METADATA_HEADERS = {_METADATA_FLAVOR_HEADER: _METADATA_FLAVOR_VALUE}

# Timeout in seconds to wait for the GCE metadata server when detecting the
# GCE environment.
try:
_METADATA_DEFAULT_TIMEOUT = int(os.getenv('GCE_METADATA_TIMEOUT', 3))
except ValueError: # pragma: NO COVER
_METADATA_DEFAULT_TIMEOUT = 3


def ping(request, timeout=_METADATA_DEFAULT_TIMEOUT):
"""Checks to see if the metadata server is available.

Args:
request (google.auth.transport.Request): A callable used to make
HTTP requests.
timeout (int): How long to wait for the metadata server to respond.

Returns:
bool: True if the metadata server is reachable, False otherwise.
"""
# NOTE: The explicit ``timeout`` is a workaround. The underlying
# issue is that resolving an unknown host on some networks will take
# 20-30 seconds; making this timeout short fixes the issue, but
# could lead to false negatives in the event that we are on GCE, but
# the metadata resolution was particularly slow. The latter case is
# "unlikely".
try:
response = request(
url=_METADATA_IP_ROOT, method='GET', headers=_METADATA_HEADERS,
timeout=timeout)

metadata_flavor = response.headers.get(_METADATA_FLAVOR_HEADER)
return (response.status == http_client.OK and
metadata_flavor == _METADATA_FLAVOR_VALUE)

except exceptions.TransportError:
return False


def get(request, path, root=_METADATA_ROOT, recursive=None):
"""Fetch a resource from the metadata server.

Args:
request (google.auth.transport.Request): A callable used to make
HTTP requests.
path (str): The resource to retrieve. For example,
``'instance/service-accounts/defualt'``.
root (str): The full path to the metadata server root.
recursive (bool): Whether to do a recursive query of metadata. See
https://cloud.google.com/compute/docs/metadata#aggcontents for more
details.

Returns:
Union[Mapping, str]: If the metadata server returns JSON, a mapping of
the decoded JSON is return. Otherwise, the response content is
returned as a string.

Raises:
google.auth.exceptions.TransportError: if an error occurred while
retrieving metadata.
"""
url = urlparse.urljoin(root, path)
url = _helpers.update_query(url, {'recursive': recursive})

response = request(url=url, method='GET', headers=_METADATA_HEADERS)

if response.status == http_client.OK:
content = _helpers.from_bytes(response.data)
if response.headers['content-type'] == 'application/json':
try:
return json.loads(content)
except ValueError:
raise exceptions.TransportError(
'Received invalid JSON from the Google Compute Engine'
'metadata service: {:.20}'.format(content))
else:
return content
else:
raise exceptions.TransportError(
'Failed to retrieve {} from the Google Compute Engine'
'metadata service. Status: {} Response:\n{}'.format(
url, response.status, response.data), response)


def get_service_account_info(request, service_account='default'):
"""Get information about a service account from the metadata server.

Args:
request (google.auth.transport.Request): A callable used to make
HTTP requests.
service_account (str): The string 'default' or a service account email
address. The determines which service account for which to acquire
information.

Returns:
Mapping: The service account's information, for example::

{
'email': '...',
'scopes': ['scope', ...],
'aliases': ['default', '...']
}

Raises:
google.auth.exceptions.TransportError: if an error occurred while
retrieving metadata.
"""
return get(
request,
'instance/service-accounts/{0}/'.format(service_account),
recursive=True)


def get_service_account_token(request, service_account='default'):
"""Get the OAuth 2.0 access token for a service account.

Args:
request (google.auth.transport.Request): A callable used to make
HTTP requests.
service_account (str): The string 'default' or a service account email
address. The determines which service account for which to acquire
an access token.

Returns:
Union[str, datetime]: The access token and its expiration.

Raises:
google.auth.exceptions.TransportError: if an error occurred while
retrieving metadata.
"""
token_json = get(
request,
'instance/service-accounts/{0}/token'.format(service_account))
token_expiry = _helpers.utcnow() + datetime.timedelta(
seconds=token_json['expires_in'])
return token_json['access_token'], token_expiry
2 changes: 1 addition & 1 deletion pylintrc.tests
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ reports=no
# DEFAULT: good-names=i,j,k,ex,Run,_
# RATIONALE: 'fh' is a well-known file handle variable name.
good-names = i, j, k, ex, Run, _,
fh,
fh

# Regular expression matching correct method names
# DEFAULT: method-rgx=[a-z_][a-z0-9_]{2,30}$
Expand Down
Empty file.