Permalink
Browse files

Adding a new module, core, which contains the start of a request-base…

…d Python 3.x compatible core for boto.
  • Loading branch information...
1 parent a77085e commit 7e865218c9fe0092bc4bf645effe71d1e8a40901 @garnaat garnaat committed Jul 5, 2012
Showing with 558 additions and 0 deletions.
  1. +58 −0 boto/core/README
  2. +23 −0 boto/core/__init__.py
  3. +78 −0 boto/core/auth.py
  4. +154 −0 boto/core/credentials.py
  5. +178 −0 boto/core/dictresponse.py
  6. +67 −0 boto/core/service.py
View
@@ -0,0 +1,58 @@
+What's This All About?
+======================
+
+This directory contains the beginnings of what is hoped will be the
+new core of boto. We want to move from using httplib to using
+requests. We also want to offer full support for Python 2.6, 2.7, and
+3.x. This is a pretty big change and will require some time to roll
+out but this module provides a starting point.
+
+What you will find in this module:
+
+* auth.py provides a SigV2 authentication packages as a args hook for requests.
+* credentials.py provides a way of finding AWS credentials (see below).
+* dictresponse.py provides a generic response handler that parses XML responses
+ and returns them as nested Python data structures.
+* service.py provides a simple example of a service that actually makes an EC2
+ request and returns a response.
+
+Credentials
+===========
+
+Credentials are being handled a bit differently here. The following
+describes the order of search for credentials:
+
+1. If your local environment for has ACCESS_KEY and SECRET_KEY variables
+ defined, these will be used.
+
+2. If your local environment has AWS_CREDENTIAL_FILE defined, it is assumed
+ that it will be a config file with entries like this:
+
+ [default]
+ access_key = xxxxxxxxxxxxxxxx
+ sercret_key = xxxxxxxxxxxxxxxxxx
+
+ [test]
+ access_key = yyyyyyyyyyyyyy
+ secret_key = yyyyyyyyyyyyyyyyyy
+
+ Each section in the config file is called a persona and you can reference
+ a particular persona by name when instantiating a Service class.
+
+3. If a standard boto config file is found that contains credentials, those
+ will be used.
+
+4. If temporary credentials for an IAM Role are found in the instance
+ metadata of an EC2 instance, these credentials will be used.
+
+Trying Things Out
+=================
+To try this code out, cd to the directory containing the core module.
+
+ >>> import core.service
+ >>> s = core.service.Service()
+ >>> s.describe_instances()
+
+This code should return a Python data structure containing information
+about your currently running EC2 instances. This example should run in
+Python 2.6.x, 2.7.x and Python 3.x.
View
@@ -0,0 +1,23 @@
+# Copyright (c) 2012 Mitch Garnaat http://garnaat.org/
+# Copyright (c) 2012 Amazon.com, Inc. or its affiliates.
+# All Rights Reserved
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish, dis-
+# tribute, sublicense, and/or sell copies of the Software, and to permit
+# persons to whom the Software is furnished to do so, subject to the fol-
+# lowing conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
+# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+#
View
@@ -0,0 +1,78 @@
+# Copyright (c) 2012 Mitch Garnaat http://garnaat.org/
+# Copyright (c) 2012 Amazon.com, Inc. or its affiliates.
+# All Rights Reserved
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish, dis-
+# tribute, sublicense, and/or sell copies of the Software, and to permit
+# persons to whom the Software is furnished to do so, subject to the fol-
+# lowing conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
+# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+#
+import requests.packages.urllib3
+import hmac
+import base64
+from hashlib import sha256
+import sys
+import datetime
+
+try:
+ from urllib.parse import quote
+except ImportError:
+ from urllib import quote
+
+
+class SigV2Auth(object):
+ """
+ Sign an Query Signature V2 request.
+ """
+ def __init__(self, credentials, api_version=''):
+ self.credentials = credentials
+ self.api_version = api_version
+ self.hmac = hmac.new(self.credentials.secret_key.encode('utf-8'),
+ digestmod=sha256)
+
+ def calc_signature(self, args):
+ scheme, host, port = requests.packages.urllib3.get_host(args['url'])
+ string_to_sign = '%s\n%s\n%s\n' % (args['method'], host, '/')
+ hmac = self.hmac.copy()
+ args['params']['SignatureMethod'] = 'HmacSHA256'
+ if self.credentials.token:
+ args['params']['SecurityToken'] = self.credentials.token
+ sorted_params = sorted(args['params'])
+ pairs = []
+ for key in sorted_params:
+ value = args['params'][key]
+ pairs.append(quote(key, safe='') + '=' +
+ quote(value, safe='-_~'))
+ qs = '&'.join(pairs)
+ string_to_sign += qs
+ print('string_to_sign')
+ print(string_to_sign)
+ hmac.update(string_to_sign.encode('utf-8'))
+ b64 = base64.b64encode(hmac.digest()).strip().decode('utf-8')
+ return (qs, b64)
+
+ def add_auth(self, args):
+ args['params']['Action'] = 'DescribeInstances'
+ args['params']['AWSAccessKeyId'] = self.credentials.access_key
+ args['params']['SignatureVersion'] = '2'
+ args['params']['Timestamp'] = datetime.datetime.utcnow().isoformat()
+ args['params']['Version'] = self.api_version
+ qs, signature = self.calc_signature(args)
+ args['params']['Signature'] = signature
+ if args['method'] == 'POST':
+ args['data'] = args['params']
+ args['params'] = {}
View
@@ -0,0 +1,154 @@
+# Copyright (c) 2012 Mitch Garnaat http://garnaat.org/
+# Copyright (c) 2012 Amazon.com, Inc. or its affiliates.
+# All Rights Reserved
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish, dis-
+# tribute, sublicense, and/or sell copies of the Software, and to permit
+# persons to whom the Software is furnished to do so, subject to the fol-
+# lowing conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
+# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+#
+import os
+from six.moves import configparser
+import requests
+import json
+
+
+class Credentials(object):
+ """
+ Holds the credentials needed to authenticate requests. In addition
+ the Credential object knows how to search for credentials and how
+ to choose the right credentials when multiple credentials are found.
+ """
+
+ def __init__(self, access_key=None, secret_key=None, token=None):
+ self.access_key = access_key
+ self.secret_key = secret_key
+ self.token = token
+
+
+def _search_md(url='http://169.254.169.254/latest/meta-data/iam/'):
+ d = {}
+ try:
+ r = requests.get(url, timeout=.1)
+ if r.content:
+ fields = r.content.split('\n')
+ for field in fields:
+ if field.endswith('/'):
+ d[field[0:-1]] = get_iam_role(url + field)
+ else:
+ val = requests.get(url + field).content
+ if val[0] == '{':
+ val = json.loads(val)
+ else:
+ p = val.find('\n')
+ if p > 0:
+ val = r.content.split('\n')
+ d[field] = val
+ except (requests.Timeout, requests.ConnectionError):
+ pass
+ return d
+
+
+def search_metadata(**kwargs):
+ credentials = None
+ metadata = _search_md()
+ # Assuming there's only one role on the instance profile.
+ if metadata:
+ metadata = metadata['iam']['security-credentials'].values()[0]
+ credentials = Credentials(metadata['AccessKeyId'],
+ metadata['SecretAccessKey'],
+ metadata['Token'])
+ return credentials
+
+
+def search_environment(**kwargs):
+ """
+ Search for credentials in explicit environment variables.
+ """
+ credentials = None
+ access_key = os.environ.get(kwargs['access_key_name'].upper(), None)
+ secret_key = os.environ.get(kwargs['secret_key_name'].upper(), None)
+ if access_key and secret_key:
+ credentials = Credentials(access_key, secret_key)
+ return credentials
+
+
+def search_file(**kwargs):
+ """
+ If the 'AWS_CREDENTIAL_FILE' environment variable exists, parse that
+ file for credentials.
+ """
+ credentials = None
+ if 'AWS_CREDENTIAL_FILE' in os.environ:
+ persona = kwargs.get('persona', 'default')
+ access_key_name = kwargs['access_key_name']
+ secret_key_name = kwargs['secret_key_name']
+ access_key = secret_key = None
+ path = os.getenv('AWS_CREDENTIAL_FILE')
+ path = os.path.expandvars(path)
+ path = os.path.expanduser(path)
+ cp = configparser.RawConfigParser()
+ cp.read(path)
+ if not cp.has_section(persona):
+ raise ValueError('Persona: %s not found' % persona)
+ if cp.has_option(persona, access_key_name):
+ access_key = cp.get(persona, access_key_name)
+ else:
+ access_key = None
+ if cp.has_option(persona, secret_key_name):
+ secret_key = cp.get(persona, secret_key_name)
+ else:
+ secret_key = None
+ if access_key and secret_key:
+ credentials = Credentials(access_key, secret_key)
+ return credentials
+
+
+def search_boto_config(**kwargs):
+ """
+ Look for credentials in boto config file.
+ """
+ credentials = access_key = secret_key = None
+ if 'BOTO_CONFIG' in os.environ:
+ paths = [os.environ['BOTO_CONFIG']]
+ else:
+ paths = ['/etc/boto.cfg', '~/.boto']
+ paths = [os.path.expandvars(p) for p in paths]
+ paths = [os.path.expanduser(p) for p in paths]
+ cp = configparser.RawConfigParser()
+ cp.read(paths)
+ if cp.has_section('Credentials'):
+ access_key = cp.get('Credentials', 'aws_access_key_id')
+ secret_key = cp.get('Credentials', 'aws_secret_access_key')
+ if access_key and secret_key:
+ credentials = Credentials(access_key, secret_key)
+ return credentials
+
+AllCredentialFunctions = [search_environment,
+ search_file,
+ search_boto_config,
+ search_metadata]
+
+
+def get_credentials(persona='default'):
+ for cred_fn in AllCredentialFunctions:
+ credentials = cred_fn(persona=persona,
+ access_key_name='access_key',
+ secret_key_name='secret_key')
+ if credentials:
+ break
+ return credentials
Oops, something went wrong.

0 comments on commit 7e86521

Please sign in to comment.