summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMitch Garnaat <mitch@cloudright.com>2010-09-16 16:17:12 -0400
committerMitch Garnaat <mitch@cloudright.com>2010-09-16 16:17:12 -0400
commit65c503d0ca275b4510520fb8886fcf7d063b4c2d (patch)
tree1369a81c9174450ca8b3e783ce56b18fff3d8bc1
parentb914cb013c614833476727fac274b80ed8a366c5 (diff)
parentfe9986bd192c3d6bf5be23a1472987e96f6449f3 (diff)
downloadboto-65c503d0ca275b4510520fb8886fcf7d063b4c2d.tar.gz
Merge branch 'master' into turnkey
-rwxr-xr-x[-rw-r--r--]bin/cfadmin14
-rwxr-xr-xbin/cq (renamed from cq.py)2
-rwxr-xr-xbin/elbadmin62
-rw-r--r--boto/__init__.py72
-rw-r--r--boto/cloudfront/__init__.py23
-rw-r--r--boto/cloudfront/invalidation.py97
-rw-r--r--boto/ec2/elb/__init__.py11
-rw-r--r--boto/ecs/__init__.py104
-rw-r--r--boto/ecs/item.py151
-rw-r--r--boto/exception.py76
-rw-r--r--[-rwxr-xr-x]boto/file/bucket.py21
-rw-r--r--[-rwxr-xr-x]boto/gs/bucket.py12
-rw-r--r--boto/iam/__init__.py824
-rw-r--r--boto/iam/response.py147
-rw-r--r--boto/provider.py36
-rw-r--r--boto/s3/bucket.py59
-rw-r--r--boto/s3/connection.py15
-rw-r--r--boto/s3/key.py24
-rw-r--r--boto/sdb/db/manager/sdbmanager.py15
-rw-r--r--boto/sdb/db/property.py5
-rw-r--r--boto/sdb/item.py8
-rwxr-xr-xboto/storage_uri.py6
-rw-r--r--boto/tests/test_gsconnection.py1
-rw-r--r--setup.py4
24 files changed, 1674 insertions, 115 deletions
diff --git a/bin/cfadmin b/bin/cfadmin
index d44e7405..c3db35f8 100644..100755
--- a/bin/cfadmin
+++ b/bin/cfadmin
@@ -46,6 +46,20 @@ def ls(cf):
print "Streaming Distributions"
_print_distributions(cf.get_all_streaming_distributions())
+def invalidate(cf, origin_or_id, *paths):
+ """Create a cloudfront invalidation request"""
+ if not paths:
+ print "Usage: cfadmin invalidate distribution_origin_or_id [path] [path2]..."
+ sys.exit(1)
+ dist = None
+ for d in cf.get_all_distributions():
+ if d.id == origin_or_id or d.origin == origin_or_id:
+ dist = d
+ break
+ if not dist:
+ print "Distribution not found: %s" % origin_or_id
+ sys.exit(1)
+ cf.create_invalidation_request(dist.id, paths)
if __name__ == "__main__":
import boto
diff --git a/cq.py b/bin/cq
index 24151569..258002d5 100755
--- a/cq.py
+++ b/bin/cq
@@ -25,7 +25,7 @@ from boto.sqs.connection import SQSConnection
from boto.exception import SQSError
def usage():
- print 'cq.py [-c] [-q queue_name] [-o output_file] [-t timeout]'
+ print 'cq [-c] [-q queue_name] [-o output_file] [-t timeout]'
def main():
try:
diff --git a/bin/elbadmin b/bin/elbadmin
index 5c139eec..be185bb2 100755
--- a/bin/elbadmin
+++ b/bin/elbadmin
@@ -44,38 +44,39 @@ def list(elb):
def get(elb, name):
"""Get details about ELB <name>"""
- b = elb.get_all_load_balancers(name)
- if len(b) < 1:
+ elbs = elb.get_all_load_balancers(name)
+ if len(elbs) < 1:
print "No load balancer by the name of %s found" % name
return
- b = b[0]
-
- print "Name: %s" % b.name
- print "DNS Name: %s" % b.dns_name
+ for b in elbs:
+ if name in b.name:
+ print "="*80
+ print "Name: %s" % b.name
+ print "DNS Name: %s" % b.dns_name
- print
+ print
- print "Listeners"
- print "---------"
- print "%-8s %-8s %s" % ("IN", "OUT", "PROTO")
- for l in b.listeners:
- print "%-8s %-8s %s" % (l[0], l[1], l[2])
+ print "Listeners"
+ print "---------"
+ print "%-8s %-8s %s" % ("IN", "OUT", "PROTO")
+ for l in b.listeners:
+ print "%-8s %-8s %s" % (l[0], l[1], l[2])
- print
+ print
- print " Zones "
- print "---------"
- for z in b.availability_zones:
- print z
+ print " Zones "
+ print "---------"
+ for z in b.availability_zones:
+ print z
- print
+ print
- print "Instances"
- print "---------"
- for i in b.instances:
- print i.id
+ print "Instances"
+ print "---------"
+ for i in b.instances:
+ print i.id
- print
+ print
def create(elb, name, zones, listeners):
"""Create an ELB named <name>"""
@@ -93,8 +94,9 @@ def delete(elb, name):
if len(b) < 1:
print "No load balancer by the name of %s found" % name
return
- b = b[0]
- b.delete()
+ for i in b:
+ if name in i.name:
+ i.delete()
print "Load Balancer %s deleted" % name
def add_instance(elb, name, instance):
@@ -103,8 +105,9 @@ def add_instance(elb, name, instance):
if len(b) < 1:
print "No load balancer by the name of %s found" % name
return
- b = b[0]
- b.register_instances([instance])
+ for i in b:
+ if name in i.name:
+ i.register_instances([instance])
return get(elb, name)
@@ -114,8 +117,9 @@ def remove_instance(elb, name, instance):
if len(b) < 1:
print "No load balancer by the name of %s found" % name
return
- b = b[0]
- b.deregister_instances([instance])
+ for i in b:
+ if name in i.name:
+ i.deregister_instances([instance])
return get(elb, name)
def enable_zone(elb, name, zone):
diff --git a/boto/__init__.py b/boto/__init__.py
index 533d0455..83cd2e47 100644
--- a/boto/__init__.py
+++ b/boto/__init__.py
@@ -287,6 +287,72 @@ def connect_sns(aws_access_key_id=None, aws_secret_access_key=None, **kwargs):
return SNSConnection(aws_access_key_id, aws_secret_access_key, **kwargs)
+def connect_iam(aws_access_key_id=None, aws_secret_access_key=None, **kwargs):
+ """
+ :type aws_access_key_id: string
+ :param aws_access_key_id: Your AWS Access Key ID
+
+ :type aws_secret_access_key: string
+ :param aws_secret_access_key: Your AWS Secret Access Key
+
+ :rtype: :class:`boto.iam.IAMConnection`
+ :return: A connection to Amazon's IAM
+ """
+ from boto.iam import IAMConnection
+ return IAMConnection(aws_access_key_id, aws_secret_access_key, **kwargs)
+
+def connect_euca(host, aws_access_key_id=None, aws_secret_access_key=None,
+ port=8773, path='/services/Eucalyptus', is_secure=False,
+ **kwargs):
+ """
+ Connect to a Eucalyptus service.
+
+ :type host: string
+ :param host: the host name or ip address of the Eucalyptus server
+
+ :type aws_access_key_id: string
+ :param aws_access_key_id: Your AWS Access Key ID
+
+ :type aws_secret_access_key: string
+ :param aws_secret_access_key: Your AWS Secret Access Key
+
+ :rtype: :class:`boto.ec2.connection.EC2Connection`
+ :return: A connection to Eucalyptus server
+ """
+ from boto.ec2 import EC2Connection
+ from boto.ec2.regioninfo import RegionInfo
+
+ reg = RegionInfo(name='eucalyptus', endpoint=host)
+ return EC2Connection(aws_access_key_id, aws_secret_access_key,
+ region=reg, port=port, path=path,
+ is_secure=is_secure, **kwargs)
+
+def connect_walrus(host, aws_access_key_id=None, aws_secret_access_key=None,
+ port=8773, path='/services/Walrus', is_secure=False,
+ **kwargs):
+ """
+ Connect to a Walrus service.
+
+ :type host: string
+ :param host: the host name or ip address of the Walrus server
+
+ :type aws_access_key_id: string
+ :param aws_access_key_id: Your AWS Access Key ID
+
+ :type aws_secret_access_key: string
+ :param aws_secret_access_key: Your AWS Secret Access Key
+
+ :rtype: :class:`boto.s3.connection.S3Connection`
+ :return: A connection to Walrus
+ """
+ from boto.s3.connection import S3Connection
+ from boto.s3.connection import OrdinaryCallingFormat
+
+ return S3Connection(aws_access_key_id, aws_secret_access_key,
+ host=host, port=port, path=path,
+ calling_format=OrdinaryCallingFormat(),
+ is_secure=is_secure, **kwargs)
+
def check_extensions(module_name, module_path):
"""
This function checks for extensions to boto modules. It should be called in the
@@ -324,15 +390,15 @@ def lookup(service, name):
_aws_cache['.'.join((service,name))] = obj
return obj
-def storage_uri(uri_str, default_scheme='file', debug=False, validate=True):
+def storage_uri(uri_str, default_scheme='file', debug=0, validate=True):
"""Instantiate a StorageUri from a URI string.
:type uri_str: string
:param uri_str: URI naming bucket + optional object.
:type default_scheme: string
:param default_scheme: default scheme for scheme-less URIs.
- :type debug: bool
- :param debug: whether to enable connection-level debugging.
+ :type debug: int
+ :param debug: debug level to pass in to boto connection (range 0..2).
:type validate: bool
:param validate: whether to check for bucket name validity.
diff --git a/boto/cloudfront/__init__.py b/boto/cloudfront/__init__.py
index 1e872ecd..8996c507 100644
--- a/boto/cloudfront/__init__.py
+++ b/boto/cloudfront/__init__.py
@@ -30,13 +30,14 @@ from boto.cloudfront.distribution import StreamingDistribution, StreamingDistrib
from boto.cloudfront.identity import OriginAccessIdentity
from boto.cloudfront.identity import OriginAccessIdentitySummary
from boto.cloudfront.identity import OriginAccessIdentityConfig
+from boto.cloudfront.invalidation import InvalidationBatch
from boto.resultset import ResultSet
from boto.cloudfront.exception import CloudFrontServerError
class CloudFrontConnection(AWSAuthConnection):
DefaultHost = 'cloudfront.amazonaws.com'
- Version = '2010-07-15'
+ Version = '2010-08-01'
def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
port=None, proxy=None, proxy_port=None,
@@ -220,4 +221,24 @@ class CloudFrontConnection(AWSAuthConnection):
return self._delete_object(access_id, etag,
'origin-access-identity/cloudfront')
+ # Object Invalidation
+
+ def create_invalidation_request(self, distribution_id, paths, caller_reference=None):
+ """Creates a new invalidation request
+ :see: http://docs.amazonwebservices.com/AmazonCloudFront/2010-08-01/APIReference/index.html?CreateInvalidation.html
+ """
+ # We allow you to pass in either an array or
+ # an InvalidationBatch object
+ if not isinstance(paths, InvalidationBatch):
+ paths = InvalidationBatch(paths)
+ paths.connection = self
+ response = self.make_request('POST', '/%s/distribution/%s/invalidation' % (self.Version, distribution_id),
+ {'Content-Type' : 'text/xml'}, data=paths.to_xml())
+ body = response.read()
+ if response.status == 201:
+ h = handler.XmlHandler(paths, self)
+ xml.sax.parseString(body, h)
+ return paths
+ else:
+ raise CloudFrontServerError(response.status, response.reason, body)
diff --git a/boto/cloudfront/invalidation.py b/boto/cloudfront/invalidation.py
new file mode 100644
index 00000000..4a0f2059
--- /dev/null
+++ b/boto/cloudfront/invalidation.py
@@ -0,0 +1,97 @@
+# Copyright (c) 2006-2010 Chris Moyer http://coredumped.org/
+#
+# 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 uuid
+import urllib
+
+class InvalidationBatch(object):
+ """A simple invalidation request.
+ :see: http://docs.amazonwebservices.com/AmazonCloudFront/2010-08-01/APIReference/index.html?InvalidationBatchDatatype.html
+ """
+
+ def __init__(self, paths=[], connection=None, distribution=None, caller_reference=''):
+ """Create a new invalidation request:
+ :paths: An array of paths to invalidate
+ """
+ self.paths = paths
+ self.distribution = distribution
+ self.caller_reference = caller_reference
+ if not self.caller_reference:
+ self.caller_reference = str(uuid.uuid4())
+
+ # If we passed in a distribution,
+ # then we use that as the connection object
+ if distribution:
+ self.connection = connection
+ else:
+ self.connection = connection
+
+ def add(self, path):
+ """Add another path to this invalidation request"""
+ return self.paths.append(path)
+
+ def remove(self, path):
+ """Remove a path from this invalidation request"""
+ return self.paths.remove(path)
+
+ def __iter__(self):
+ return iter(self.paths)
+
+ def __getitem__(self, i):
+ return self.paths[i]
+
+ def __setitem__(self, k, v):
+ self.paths[k] = v
+
+ def escape(self, p):
+ """Escape a path, make sure it begins with a slash and contains no invalid characters"""
+ if not p[0] == "/":
+ p = "/%s" % p
+ return urllib.quote(p)
+
+ def to_xml(self):
+ """Get this batch as XML"""
+ assert self.connection != None
+ s = '<?xml version="1.0" encoding="UTF-8"?>\n'
+ s += '<InvalidationBatch xmlns="http://cloudfront.amazonaws.com/doc/%s/">\n' % self.connection.Version
+ for p in self.paths:
+ s += ' <Path>%s</Path>\n' % self.escape(p)
+ s += ' <CallerReference>%s</CallerReference>\n' % self.caller_reference
+ s += '</InvalidationBatch>\n'
+ return s
+
+ def startElement(self, name, attrs, connection):
+ if name == "InvalidationBatch":
+ self.paths = []
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'Path':
+ self.paths.append(value)
+ elif name == "Status":
+ self.status = value
+ elif name == "Id":
+ self.id = id
+ elif name == "CreateTime":
+ self.create_time = value
+ elif name == "CallerReference":
+ self.caller_reference = value
+ return None
diff --git a/boto/ec2/elb/__init__.py b/boto/ec2/elb/__init__.py
index 55e846f0..65403c42 100644
--- a/boto/ec2/elb/__init__.py
+++ b/boto/ec2/elb/__init__.py
@@ -56,20 +56,19 @@ class ELBConnection(AWSQueryConnection):
for i in range(1, len(items)+1):
params[label % i] = items[i-1]
- def get_all_load_balancers(self, load_balancer_name=None):
+ def get_all_load_balancers(self, load_balancer_names=None):
"""
Retrieve all load balancers associated with your account.
- :type load_balancer_names: str
- :param load_balancer_names: An optional filter string to get only one ELB
+ :type load_balancer_names: list
+ :param load_balancer_names: An optional list of load balancer names
:rtype: list
:return: A list of :class:`boto.ec2.elb.loadbalancer.LoadBalancer`
"""
params = {}
- if load_balancer_name:
- #self.build_list_params(params, load_balancer_names, 'LoadBalancerName.%d')
- params['LoadBalancerName'] = load_balancer_name
+ if load_balancer_names:
+ self.build_list_params(params, load_balancer_names, 'LoadBalancerNames.member.%d')
return self.get_list('DescribeLoadBalancers', params, [('member', LoadBalancer)])
diff --git a/boto/ecs/__init__.py b/boto/ecs/__init__.py
new file mode 100644
index 00000000..9139db51
--- /dev/null
+++ b/boto/ecs/__init__.py
@@ -0,0 +1,104 @@
+# Copyright (c) 2010 Chris Moyer http://coredumped.org/
+#
+# 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 boto
+from boto.connection import AWSQueryConnection, AWSAuthConnection
+import time
+import urllib
+import xml.sax
+from boto.ecs.item import ItemSet
+from boto import handler
+
+class ECSConnection(AWSQueryConnection):
+ """ECommerse Connection"""
+
+ APIVersion = '2010-09-01'
+ SignatureVersion = '2'
+
+ def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
+ is_secure=True, port=None, proxy=None, proxy_port=None,
+ proxy_user=None, proxy_pass=None, host='ecs.amazonaws.com',
+ debug=0, https_connection_factory=None, path='/'):
+ AWSQueryConnection.__init__(self, aws_access_key_id, aws_secret_access_key,
+ is_secure, port, proxy, proxy_port, proxy_user, proxy_pass,
+ host, debug, https_connection_factory, path)
+
+ def make_request(self, action, params=None, path='/', verb='GET'):
+ """Overriden because we don't do the "Action" setting here"""
+ headers = {}
+ if params == None:
+ params = {}
+ params['Version'] = self.APIVersion
+ params['AWSAccessKeyId'] = self.aws_access_key_id
+ params['SignatureVersion'] = self.SignatureVersion
+ params['Timestamp'] = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime())
+ qs, signature = self.get_signature(params, verb, self.get_path(path))
+ if verb == 'POST':
+ headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
+ request_body = qs + '&Signature=' + urllib.quote(signature)
+ qs = path
+ else:
+ request_body = ''
+ qs = path + '?' + qs + '&Signature=' + urllib.quote(signature)
+ return AWSAuthConnection.make_request(self, verb, qs,
+ data=request_body,
+ headers=headers)
+
+
+ def get_response(self, action, params, page=0, itemSet=None):
+ """
+ Utility method to handle calls to ECS and parsing of responses.
+ """
+ params['Service'] = "AWSECommerceService"
+ params['Operation'] = action
+ if page:
+ params['ItemPage'] = page
+ response = self.make_request("GET", params, "/onca/xml")
+ body = response.read()
+ boto.log.debug(body)
+
+ if response.status != 200:
+ boto.log.error('%s %s' % (response.status, response.reason))
+ boto.log.error('%s' % body)
+ raise self.ResponseError(response.status, response.reason, body)
+
+ if itemSet == None:
+ rs = ItemSet(self, action, params, page)
+ else:
+ rs = itemSet
+ h = handler.XmlHandler(rs, self)
+ xml.sax.parseString(body, h)
+ return rs
+
+ #
+ # Group methods
+ #
+
+ def item_search(self, search_index, **params):
+ """
+ Returns items that satisfy the search criteria, including one or more search
+ indices.
+
+ For a full list of search terms,
+ :see: http://docs.amazonwebservices.com/AWSECommerceService/2010-09-01/DG/index.html?ItemSearch.html
+ """
+ params['SearchIndex'] = search_index
+ return self.get_response('ItemSearch', params)
diff --git a/boto/ecs/item.py b/boto/ecs/item.py
new file mode 100644
index 00000000..526928f9
--- /dev/null
+++ b/boto/ecs/item.py
@@ -0,0 +1,151 @@
+# Copyright (c) 2010 Chris Moyer http://coredumped.org/
+#
+# 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 xml.sax
+import cgi
+from StringIO import StringIO
+
+class ResponseGroup(xml.sax.ContentHandler):
+ """A Generic "Response Group", which can
+ be anything from the entire list of Items to
+ specific response elements within an item"""
+
+ def __init__(self, connection=None, nodename=None):
+ """Initialize this Item"""
+ self._connection = connection
+ self._nodename = nodename
+ self._nodepath = []
+ self._curobj = None
+ self._xml = StringIO()
+
+ def __repr__(self):
+ return '<%s: %s>' % (self.__class__.__name__, self.__dict__)
+
+ #
+ # Attribute Functions
+ #
+ def get(self, name):
+ return self.__dict__.get(name)
+
+ def set(self, name, value):
+ self.__dict__[name] = value
+
+ def to_xml(self):
+ return "<%s>%s</%s>" % (self._nodename, self._xml.getvalue(), self._nodename)
+
+ #
+ # XML Parser functions
+ #
+ def startElement(self, name, attrs, connection):
+ self._xml.write("<%s>" % name)
+ self._nodepath.append(name)
+ if len(self._nodepath) == 1:
+ obj = ResponseGroup(self._connection)
+ self.set(name, obj)
+ self._curobj = obj
+ elif self._curobj:
+ self._curobj.startElement(name, attrs, connection)
+ return None
+
+ def endElement(self, name, value, connection):
+ self._xml.write("%s</%s>" % (cgi.escape(value).replace("&amp;amp;", "&amp;"), name))
+ if len(self._nodepath) == 0:
+ return
+ obj = None
+ curval = self.get(name)
+ if len(self._nodepath) == 1:
+ if value or not curval:
+ self.set(name, value)
+ if self._curobj:
+ self._curobj = None
+ #elif len(self._nodepath) == 2:
+ #self._curobj = None
+ elif self._curobj:
+ self._curobj.endElement(name, value, connection)
+ self._nodepath.pop()
+ return None
+
+
+class Item(ResponseGroup):
+ """A single Item"""
+
+ def __init__(self, connection=None):
+ """Initialize this Item"""
+ ResponseGroup.__init__(self, connection, "Item")
+
+class ItemSet(ResponseGroup):
+ """A special ResponseGroup that has built-in paging, and
+ only creates new Items on the "Item" tag"""
+
+ def __init__(self, connection, action, params, page=0):
+ ResponseGroup.__init__(self, connection, "Items")
+ self.objs = []
+ self.iter = None
+ self.page = page
+ self.action = action
+ self.params = params
+ self.curItem = None
+
+ def startElement(self, name, attrs, connection):
+ if name == "Item":
+ self.curItem = Item(self._connection)
+ elif self.curItem != None:
+ self.curItem.startElement(name, attrs, connection)
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'TotalResults':
+ self.total_results = value
+ elif name == 'TotalPages':
+ self.total_pages = value
+ elif name == "Item":
+ self.objs.append(self.curItem)
+ self._xml.write(self.curItem.to_xml())
+ self.curItem = None
+ elif self.curItem != None:
+ self.curItem.endElement(name, value, connection)
+ return None
+
+ def next(self):
+ """Special paging functionality"""
+ if self.iter == None:
+ self.iter = iter(self.objs)
+ try:
+ return self.iter.next()
+ except StopIteration:
+ self.iter = None
+ self.objs = []
+ if int(self.page) < int(self.total_pages):
+ self.page += 1
+ self._connection.get_response(self.action, self.params, self.page, self)
+ return self.next()
+ else:
+ raise
+
+ def __iter__(self):
+ return self
+
+ def to_xml(self):
+ """Override to first fetch everything"""
+ for item in self:
+ pass
+ return ResponseGroup.to_xml(self)
diff --git a/boto/exception.py b/boto/exception.py
index 595eac64..31ad206e 100644
--- a/boto/exception.py
+++ b/boto/exception.py
@@ -49,12 +49,24 @@ class SDBPersistenceError(StandardError):
pass
-class S3PermissionsError(BotoClientError):
+class StoragePermissionsError(BotoClientError):
+ """
+ Permissions error when accessing a bucket or key on a storage service.
+ """
+ pass
+
+class S3PermissionsError(StoragePermissionsError):
"""
Permissions error when accessing a bucket or key on S3.
"""
pass
+class GSPermissionsError(StoragePermissionsError):
+ """
+ Permissions error when accessing a bucket or key on GS.
+ """
+ pass
+
class BotoServerError(StandardError):
def __init__(self, status, reason, body=None):
@@ -135,9 +147,9 @@ class ConsoleOutput:
else:
setattr(self, name, value)
-class S3CreateError(BotoServerError):
+class StorageCreateError(BotoServerError):
"""
- Error creating a bucket or key on S3.
+ Error creating a bucket or key on a storage service.
"""
def __init__(self, status, reason, body=None):
self.bucket = None
@@ -149,12 +161,36 @@ class S3CreateError(BotoServerError):
else:
return BotoServerError.endElement(self, name, value, connection)
-class S3CopyError(BotoServerError):
+class S3CreateError(StorageCreateError):
+ """
+ Error creating a bucket or key on S3.
+ """
+ pass
+
+class GSCreateError(StorageCreateError):
+ """
+ Error creating a bucket or key on GS.
+ """
+ pass
+
+class StorageCopyError(BotoServerError):
+ """
+ Error copying a key on a storage service.
+ """
+ pass
+
+class S3CopyError(StorageCopyError):
"""
Error copying a key on S3.
"""
pass
+class GSCopyError(StorageCopyError):
+ """
+ Error copying a key on GS.
+ """
+ pass
+
class SQSError(BotoServerError):
"""
General Error on Simple Queue Service.
@@ -193,10 +229,10 @@ class SQSDecodeError(BotoClientError):
def __str__(self):
return 'SQSDecodeError: %s' % self.reason
-
-class S3ResponseError(BotoServerError):
+
+class StorageResponseError(BotoServerError):
"""
- Error in response from S3.
+ Error in response from a storage service.
"""
def __init__(self, status, reason, body=None):
self.resource = None
@@ -216,6 +252,18 @@ class S3ResponseError(BotoServerError):
for p in ('resource'):
setattr(self, p, None)
+class S3ResponseError(StorageResponseError):
+ """
+ Error in response from S3.
+ """
+ pass
+
+class GSResponseError(StorageResponseError):
+ """
+ Error in response from GS.
+ """
+ pass
+
class EC2ResponseError(BotoServerError):
"""
Error in response from EC2.
@@ -285,12 +333,24 @@ class AWSConnectionError(BotoClientError):
"""
pass
-class S3DataError(BotoClientError):
+class StorageDataError(BotoClientError):
+ """
+ Error receiving data from a storage service.
+ """
+ pass
+
+class S3DataError(StorageDataError):
"""
Error receiving data from S3.
"""
pass
+class GSDataError(StorageDataError):
+ """
+ Error receiving data from GS.
+ """
+ pass
+
class FPSResponseError(BotoServerError):
pass
diff --git a/boto/file/bucket.py b/boto/file/bucket.py
index f1055884..be01cff6 100755..100644
--- a/boto/file/bucket.py
+++ b/boto/file/bucket.py
@@ -24,7 +24,7 @@
import os
from key import Key
from boto.file.simpleresultset import SimpleResultSet
-from boto.exception import S3ResponseError
+from boto.provider import Provider
from boto.s3.bucketlistresultset import BucketListResultSet
class Bucket(object):
@@ -54,10 +54,7 @@ class Bucket(object):
:type mfa_token: tuple or list of strings
:param mfa_token: Unused in this subclass.
"""
- try:
- os.remove(key_name)
- except OSError, e:
- raise S3ResponseError(409, e.strerror)
+ os.remove(key_name)
def get_all_keys(self, headers=None, **params):
"""
@@ -85,11 +82,8 @@ class Bucket(object):
:rtype: :class:`boto.file.key.Key`
:returns: A Key object from this bucket.
"""
- try:
- fp = open(key_name, 'rb')
- return Key(self.name, key_name, fp)
- except OSError, e:
- raise S3ResponseError(409, e.strerror)
+ fp = open(key_name, 'rb')
+ return Key(self.name, key_name, fp)
def new_key(self, key_name=None):
"""
@@ -104,8 +98,5 @@ class Bucket(object):
dir_name = os.path.dirname(key_name)
if dir_name and not os.path.exists(dir_name):
os.makedirs(dir_name)
- try:
- fp = open(key_name, 'wb')
- return Key(self.name, key_name, fp)
- except OSError, e:
- raise S3ResponseError(409, e.strerror)
+ fp = open(key_name, 'wb')
+ return Key(self.name, key_name, fp)
diff --git a/boto/gs/bucket.py b/boto/gs/bucket.py
index df9165f2..fd3aa1ff 100755..100644
--- a/boto/gs/bucket.py
+++ b/boto/gs/bucket.py
@@ -21,7 +21,8 @@
import boto
from boto import handler
-from boto.exception import InvalidAclError, S3ResponseError, S3PermissionsError
+from boto.exception import InvalidAclError
+from boto.provider import Provider
from boto.gs.acl import ACL
from boto.gs.acl import SupportedPermissions as GSPermissions
from boto.gs.key import Key as GSKey
@@ -52,7 +53,8 @@ class Bucket(S3Bucket):
xml.sax.parseString(body, h)
return acl
else:
- raise S3ResponseError(response.status, response.reason, body)
+ raise self.connection.provider.storage_response_error(
+ response.status, response.reason, body)
# Method with same signature as boto.s3.bucket.Bucket.add_email_grant(),
# to allow polymorphic treatment at application layer.
@@ -82,7 +84,8 @@ class Bucket(S3Bucket):
a long time!
"""
if permission not in GSPermissions:
- raise S3PermissionsError('Unknown Permission: %s' % permission)
+ raise self.connection.provider.storage_permissions_error(
+ 'Unknown Permission: %s' % permission)
acl = self.get_acl(headers=headers)
acl.add_email_grant(permission, email_address)
self.set_acl(acl, headers=headers)
@@ -116,7 +119,8 @@ class Bucket(S3Bucket):
a long time!
"""
if permission not in GSPermissions:
- raise S3PermissionsError('Unknown Permission: %s' % permission)
+ raise self.connection.provider.storage_permissions_error(
+ 'Unknown Permission: %s' % permission)
acl = self.get_acl(headers=headers)
acl.add_user_grant(permission, user_id)
self.set_acl(acl, headers=headers)
diff --git a/boto/iam/__init__.py b/boto/iam/__init__.py
new file mode 100644
index 00000000..d4bfff20
--- /dev/null
+++ b/boto/iam/__init__.py
@@ -0,0 +1,824 @@
+# Copyright (c) 2010 Mitch Garnaat http://garnaat.org/
+#
+# 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 boto
+import boto.iam.response
+from boto.connection import AWSQueryConnection
+
+boto.set_stream_logger('iam')
+
+class IAMConnection(AWSQueryConnection):
+
+ APIVersion = '2010-05-08'
+ SignatureVersion = '2'
+
+ def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
+ is_secure=True, port=None, proxy=None, proxy_port=None,
+ proxy_user=None, proxy_pass=None, host='iam.amazonaws.com',
+ debug=2, https_connection_factory=None, path='/'):
+ AWSQueryConnection.__init__(self, aws_access_key_id, aws_secret_access_key,
+ is_secure, port, proxy, proxy_port, proxy_user, proxy_pass,
+ host, debug, https_connection_factory, path)
+
+ def get_response(self, action, params, path='/', parent=None,
+ verb='GET', list_marker='Set'):
+ """
+ Utility method to handle calls to IAM and parsing of responses.
+ """
+ if not parent:
+ parent = self
+ response = self.make_request(action, params, path, verb)
+ body = response.read()
+ boto.log.debug(body)
+ if response.status == 200:
+ e = boto.iam.response.Element(list_marker=list_marker)
+ h = boto.iam.response.XmlHandler(e, parent)
+ h.parse(body)
+ return e
+ else:
+ boto.log.error('%s %s' % (response.status, response.reason))
+ boto.log.error('%s' % body)
+ raise self.ResponseError(response.status, response.reason, body)
+
+ #
+ # Group methods
+ #
+
+ def get_all_groups(self, path_prefix='/', marker=None, max_items=None):
+ """
+ List the groups that have the specified path prefix.
+
+ :type path_prefix: string
+ :param path_prefix: If provided, only groups whose paths match
+ the provided prefix will be returned.
+
+ :type marker: string
+ :param marker: Use this only when paginating results and only in
+ follow-up request after you've received a response
+ where the results are truncated. Set this to the
+ value of the Marker element in the response you
+ just received.
+
+ :type max_items: int
+ :param max_items: Use this only when paginating results to indicate
+ the maximum number of groups you want in the
+ response.
+ """
+ params = {}
+ if path_prefix:
+ params['PathPrefix'] = path_prefix
+ if marker:
+ params['Marker'] = marker
+ if max_items:
+ params['MaxItems'] = max_items
+ return self.get_response('ListGroups', params, list_marker='Groups')
+
+ def get_group(self, group_name, marker=None, max_items=None):
+ """
+ Return a list of users that are in the specified group.
+
+ :type group_name: string
+ :param group_name: The name of the group whose information should
+ be returned.
+ :type marker: string
+ :param marker: Use this only when paginating results and only in
+ follow-up request after you've received a response
+ where the results are truncated. Set this to the
+ value of the Marker element in the response you
+ just received.
+
+ :type max_items: int
+ :param max_items: Use this only when paginating results to indicate
+ the maximum number of groups you want in the
+ response.
+ """
+ params = {'GroupName' : group_name}
+ if marker:
+ params['Marker'] = marker
+ if max_items:
+ params['MaxItems'] = max_items
+ return self.get_response('GetGroup', params, list_marker='Users')
+
+ def create_group(self, group_name, path='/'):
+ """
+ Create a group.
+
+ :type group_name: string
+ :param group_name: The name of the new group
+
+ :type path: string
+ :param path: The path to the group (Optional). Defaults to /.
+
+ """
+ params = {'GroupName' : group_name,
+ 'Path' : path}
+ return self.get_response('CreateGroup', params)
+
+ def delete_group(self, group_name):
+ """
+ Delete a group. The group must not contain any Users or
+ have any attached policies
+
+ :type group_name: string
+ :param group_name: The name of the group to delete.
+
+ """
+ params = {'GroupName' : group_name}
+ return self.get_response('DeleteGroup', params)
+
+ def update_group(self, group_name, new_group_name=None, new_path=None):
+ """
+ Update a group by adding or removing a user to/from it.
+
+ :type group_name: string
+ :param group_name: The name of the new group
+
+ :type new_group_name: string
+ :param new_group_name: If provided, the name of the group will be
+ changed to this name.
+
+ :type new_path: string
+ :param new_path: If provided, the path of the group will be
+ changed to this path.
+
+ """
+ params = {'GroupName' : group_name}
+ if new_group_name:
+ params['NewGroupName'] = new_group_name
+ if new_path:
+ params['NewPath'] = new_path
+ return self.get_response('UpdateGroup', params)
+
+ def add_user_to_group(self, group_name, user_name):
+ """
+ Add a user to a group
+
+ :type group_name: string
+ :param group_name: The name of the new group
+
+ :type user_name: string
+ :param user_name: The to be added to the group.
+
+ """
+ params = {'GroupName' : group_name,
+ 'UserName' : user_name}
+ return self.get_response('AddUserToGroup', params)
+
+ def remove_user_from_group(self, group_name, user_name):
+ """
+ Remove a user from a group.
+
+ :type group_name: string
+ :param group_name: The name of the new group
+
+ :type user_name: string
+ :param user_name: The user to remove from the group.
+
+ """
+ params = {'GroupName' : group_name,
+ 'UserName' : user_name}
+ return self.get_response('RemoveUserFromGroup', params)
+
+ def put_group_policy(self, group_name, policy_name, policy_json):
+ """
+ Adds or updates the specified policy document for the specified group.
+
+ :type group_name: string
+ :param group_name: The name of the group the policy is associated with.
+
+ :type policy_name: string
+ :param policy_name: The policy document to get.
+
+ :type policy_json: string
+ :param policy_json: The policy document.
+
+ """
+ params = {'GroupName' : group_name,
+ 'PolicyName' : policy_name,
+ 'PolicyDocument' : policy_json}
+ return self.get_response('PutGroupPolicy', params, verb='POST')
+
+ def get_all_group_policies(self, group_name, marker=None, max_items=None):
+ """
+ List the names of the policies associated with the specified group.
+
+ :type group_name: string
+ :param group_name: The name of the group the policy is associated with.
+
+ :type marker: string
+ :param marker: Use this only when paginating results and only in
+ follow-up request after you've received a response
+ where the results are truncated. Set this to the
+ value of the Marker element in the response you
+ just received.
+
+ :type max_items: int
+ :param max_items: Use this only when paginating results to indicate
+ the maximum number of groups you want in the
+ response.
+ """
+ params = {'GroupName' : group_name}
+ if marker:
+ params['Marker'] = marker
+ if max_items:
+ params['MaxItems'] = max_items
+ return self.get_response('ListGroupPolicies', params,
+ list_marker='PolicyNames')
+
+ def get_group_policy(self, group_name, policy_name):
+ """
+ Retrieves the specified policy document for the specified group.
+
+ :type group_name: string
+ :param group_name: The name of the group the policy is associated with.
+
+ :type policy_name: string
+ :param policy_name: The policy document to get.
+
+ """
+ params = {'GroupName' : group_name,
+ 'PolicyName' : policy_name}
+ return self.get_response('GetGroupPolicy', params, verb='POST')
+
+ def delete_group_policy(self, group_name, policy_name):
+ """
+ Deletes the specified policy document for the specified group.
+
+ :type group_name: string
+ :param group_name: The name of the group the policy is associated with.
+
+ :type policy_name: string
+ :param policy_name: The policy document to delete.
+
+ """
+ params = {'GroupName' : group_name,
+ 'PolicyName' : policy_name}
+ return self.get_response('DeleteGroupPolicy', params, verb='POST')
+
+ def get_all_users(self, path_prefix='/', marker=None, max_items=None):
+ """
+ List the users that have the specified path prefix.
+
+ :type path_prefix: string
+ :param path_prefix: If provided, only users whose paths match
+ the provided prefix will be returned.
+
+ :type marker: string
+ :param marker: Use this only when paginating results and only in
+ follow-up request after you've received a response
+ where the results are truncated. Set this to the
+ value of the Marker element in the response you
+ just received.
+
+ :type max_items: int
+ :param max_items: Use this only when paginating results to indicate
+ the maximum number of groups you want in the
+ response.
+ """
+ params = {'PathPrefix' : path_prefix}
+ if marker:
+ params['Marker'] = marker
+ if max_items:
+ params['MaxItems'] = max_items
+ return self.get_response('ListUsers', params, list_marker='Users')
+
+ #
+ # User methods
+ #
+
+ def create_user(self, user_name, path='/'):
+ """
+ Create a user.
+
+ :type user_name: string
+ :param user_name: The name of the new user
+
+ :type path: string
+ :param path: The path in which the user will be created.
+ Defaults to /.
+
+ """
+ params = {'UserName' : user_name,
+ 'Path' : path}
+ return self.get_response('CreateUser', params)
+
+ def delete_user(self, user_name):
+ """
+ Delete a user including the user's path, GUID and ARN.
+
+ If the user_name is not specified, the user_name is determined
+ implicitly based on the AWS Access Key ID used to sign the request.
+
+ :type user_name: string
+ :param user_name: The name of the user to delete.
+
+ """
+ params = {'UserName' : user_name}
+ return self.get_response('DeleteUser', params)
+
+ def get_user(self, user_name=None):
+ """
+ Retrieve information about the specified user.
+
+ If the user_name is not specified, the user_name is determined
+ implicitly based on the AWS Access Key ID used to sign the request.
+
+ :type user_name: string
+ :param user_name: The name of the user to delete.
+ If not specified, defaults to user making
+ request.
+
+ """
+ params = {}
+ if user_name:
+ params['UserName'] = user_name
+ return self.get_response('GetUser', params)
+
+ def update_user(self, user_name, new_user_name=None, new_path=None):
+ """
+ Updates name and/or path of the specified user.
+
+ :type user_name: string
+ :param user_name: The name of the user
+
+ :type new_user_name: string
+ :param new_user_name: If provided, the username of the user will be
+ changed to this username.
+
+ :type new_path: string
+ :param new_path: If provided, the path of the user will be
+ changed to this path.
+
+ """
+ params = {'UserName' : user_name}
+ if new_user_name:
+ params['NewUserName'] = new_user_name
+ if new_path:
+ params['NewPath'] = new_path
+ return self.get_response('UpdateUser', params)
+
+ def get_all_user_policies(self, user_name, marker=None, max_items=None):
+ """
+ List the names of the policies associated with the specified user.
+
+ :type user_name: string
+ :param user_name: The name of the user the policy is associated with.
+
+ :type marker: string
+ :param marker: Use this only when paginating results and only in
+ follow-up request after you've received a response
+ where the results are truncated. Set this to the
+ value of the Marker element in the response you
+ just received.
+
+ :type max_items: int
+ :param max_items: Use this only when paginating results to indicate
+ the maximum number of groups you want in the
+ response.
+ """
+ params = {'UserName' : user_name}
+ if marker:
+ params['Marker'] = marker
+ if max_items:
+ params['MaxItems'] = max_items
+ return self.get_response('ListUserPolicies', params,
+ list_marker='PolicyNames')
+
+ def put_user_policy(self, user_name, policy_name, policy_json):
+ """
+ Adds or updates the specified policy document for the specified user.
+
+ :type user_name: string
+ :param user_name: The name of the user the policy is associated with.
+
+ :type policy_name: string
+ :param policy_name: The policy document to get.
+
+ :type policy_json: string
+ :param policy_json: The policy document.
+
+ """
+ params = {'UserName' : user_name,
+ 'PolicyName' : policy_name,
+ 'PolicyDocument' : policy_json}
+ return self.get_response('PutUserPolicy', params, verb='POST')
+
+ def get_user_policy(self, user_name, policy_name):
+ """
+ Retrieves the specified policy document for the specified user.
+
+ :type user_name: string
+ :param user_name: The name of the user the policy is associated with.
+
+ :type policy_name: string
+ :param policy_name: The policy document to get.
+
+ """
+ params = {'UserName' : user_name,
+ 'PolicyName' : policy_name}
+ return self.get_response('GetUserPolicy', params, verb='POST')
+
+ def delete_user_policy(self, user_name, policy_name):
+ """
+ Deletes the specified policy document for the specified user.
+
+ :type user_name: string
+ :param user_name: The name of the user the policy is associated with.
+
+ :type policy_name: string
+ :param policy_name: The policy document to delete.
+
+ """
+ params = {'UserName' : user,
+ 'PolicyName' : policy_name}
+ return self.get_response('DeleteUserPolicy', params, verb='POST')
+
+ def get_groups_for_user(self, user_name, marker=None, max_items=None):
+ """
+ List the groups that a specified user belongs to.
+
+ :type user_name: string
+ :param user_name: The name of the user to list groups for.
+
+ :type marker: string
+ :param marker: Use this only when paginating results and only in
+ follow-up request after you've received a response
+ where the results are truncated. Set this to the
+ value of the Marker element in the response you
+ just received.
+
+ :type max_items: int
+ :param max_items: Use this only when paginating results to indicate
+ the maximum number of groups you want in the
+ response.
+ """
+ params = {'UserName' : user_name}
+ if marker:
+ params['Marker'] = marker
+ if max_items:
+ params['MaxItems'] = max_items
+ return self.get_response('ListUsers', params, list_marker='Groups')
+
+ #
+ # Access Keys
+ #
+
+ def get_all_access_keys(self, user_name, marker=None, max_items=None):
+ """
+ Get all access keys associated with an account.
+
+ :type user_name: string
+ :param user_name: The username of the new user
+
+ :type marker: string
+ :param marker: Use this only when paginating results and only in
+ follow-up request after you've received a response
+ where the results are truncated. Set this to the
+ value of the Marker element in the response you
+ just received.
+
+ :type max_items: int
+ :param max_items: Use this only when paginating results to indicate
+ the maximum number of groups you want in the
+ response.
+ """
+ params = {'UserName' : user_name}
+ if marker:
+ params['Marker'] = marker
+ if max_items:
+ params['MaxItems'] = max_items
+ return self.get_response('ListAccessKeys', params, list_marker='AccessKeys')
+
+ def create_access_key(self, user_name=None):
+ """
+ Create a new AWS Secret Access Key and corresponding AWS Access Key ID
+ for the specified user. The default status for new keys is Active
+
+ If the user_name is not specified, the user_name is determined
+ implicitly based on the AWS Access Key ID used to sign the request.
+
+ :type user_name: string
+ :param user_name: The username of the new user
+
+ """
+ params = {'UserName' : user_name}
+ return self.get_response('CreateAccessKey', params)
+
+ def update_access_key(self, access_key_id, status, user_name=None):
+ """
+ Changes the status of the specified access key from Active to Inactive
+ or vice versa. This action can be used to disable a user's key as
+ part of a key rotation workflow.
+
+ If the user_name is not specified, the user_name is determined
+ implicitly based on the AWS Access Key ID used to sign the request.
+
+ :type access_key_id: string
+ :param access_key_id: The ID of the access key.
+
+ :type status: string
+ :param status: Either Active or Inactive.
+
+ :type user_name: string
+ :param user_name: The username of user (optional).
+
+ """
+ params = {'AccessKeyId' : access_key_id,
+ 'Status' : status}
+ if user_name:
+ params['UserName'] = user_name
+ return self.get_response('UpdateAccessKey', params)
+
+ def delete_access_key(self, access_key_id, user_name=None):
+ """
+ Delete an access key associated with a user.
+
+ If the user_name is not specified, it is determined implicitly based
+ on the AWS Access Key ID used to sign the request.
+
+ :type access_key_id: string
+ :param access_key_id: The ID of the access key to be deleted.
+
+ :type user_name: string
+ :param user_name: The username of the new user
+
+ """
+ params = {'AccessKeyId' : access_key_id}
+ if user_name:
+ params['UserName'] = user_name
+ return self.get_response('DeleteAccessKey', params)
+
+ #
+ # Signing Certificates
+ #
+
+ def get_all_signing_certs(self, marker=None, max_items=None,
+ user_name=None):
+ """
+ Get all signing certificates associated with an account.
+
+ If the user_name is not specified, it is determined implicitly based
+ on the AWS Access Key ID used to sign the request.
+
+ :type marker: string
+ :param marker: Use this only when paginating results and only in
+ follow-up request after you've received a response
+ where the results are truncated. Set this to the
+ value of the Marker element in the response you
+ just received.
+
+ :type max_items: int
+ :param max_items: Use this only when paginating results to indicate
+ the maximum number of groups you want in the
+ response.
+
+ :type user_name: string
+ :param user_name: The username of the user
+
+ """
+ params = {}
+ if marker:
+ params['Marker'] = marker
+ if max_items:
+ params['MaxItems'] = max_items
+ if user_name:
+ params['UserName'] = user_name
+ return self.get_response('ListSigningCertificates',
+ params, list_marker='Certificates')
+
+ def update_signing_cert(self, cert_id, status, user_name=None):
+ """
+ Change the status of the specified signing certificate from
+ Active to Inactive or vice versa.
+
+ If the user_name is not specified, it is determined implicitly based
+ on the AWS Access Key ID used to sign the request.
+
+ :type cert_id: string
+ :param cert_id: The ID of the signing certificate
+
+ :type status: string
+ :param status: Either Active or Inactive.
+
+ :type user_name: string
+ :param user_name: The username of the user
+ """
+ params = {'CertificateId' : cert_id,
+ 'Status' : status}
+ if user_name:
+ params['UserName'] = user_name
+ return self.get_response('UpdateSigningCertificate', params)
+
+ def upload_signing_cert(self, cert_body, user_name=None):
+ """
+ Uploads an X.509 signing certificate and associates it with
+ the specified user.
+
+ If the user_name is not specified, it is determined implicitly based
+ on the AWS Access Key ID used to sign the request.
+
+ :type cert_body: string
+ :param cert_body: The body of the signing certificate.
+
+ :type user_name: string
+ :param user_name: The username of the new user
+
+ """
+ params = {'CertificateBody' : cert_body}
+ if user_name:
+ params['UserName'] = user_name
+ return self.get_response('UploadSigningCertificate', params,
+ verb='POST')
+
+ def delete_signing_cert(self, cert_id, user_name=None):
+ """
+ Delete a signing certificate associated with a user.
+
+ If the user_name is not specified, it is determined implicitly based
+ on the AWS Access Key ID used to sign the request.
+
+ :type user_name: string
+ :param user_name: The username of the new user
+
+ :type cert_id: string
+ :param cert_id: The ID of the certificate.
+
+ """
+ params = {'CertificateId' : cert_id}
+ if user_name:
+ params['UserName'] = user_name
+ return self.get_response('DeleteSigningCertificate', params)
+
+ #
+ # MFA Devices
+ #
+
+ def get_all_mfa_devices(self, marker=None, max_items=None,
+ user_name=None):
+ """
+ Get all MFA devices associated with an account.
+
+ If the user_name is not specified, it is determined implicitly based
+ on the AWS Access Key ID used to sign the request.
+
+ :type marker: string
+ :param marker: Use this only when paginating results and only in
+ follow-up request after you've received a response
+ where the results are truncated. Set this to the
+ value of the Marker element in the response you
+ just received.
+
+ :type max_items: int
+ :param max_items: Use this only when paginating results to indicate
+ the maximum number of groups you want in the
+ response.
+
+ :type user_name: string
+ :param user_name: The username of the user
+
+ """
+ params = {}
+ if marker:
+ params['Marker'] = marker
+ if max_items:
+ params['MaxItems'] = max_items
+ if user_name:
+ params['UserName'] = user_name
+ return self.get_response('ListMFADevices',
+ params, list_marker='MFADevices')
+
+ def enable_mfa_device(self, user_name, serial_number,
+ auth_code_1, auth_code_2):
+ """
+ Enables the specified MFA device and associates it with the
+ specified user.
+
+ :type user_name: string
+ :param user_name: The username of the user
+
+ :type serial_number: string
+ :param seriasl_number: The serial number which uniquely identifies
+ the MFA device.
+
+ :type auth_code_1: string
+ :param auth_code_1: An authentication code emitted by the device.
+
+ :type auth_code_2: string
+ :param auth_code_2: A subsequent authentication code emitted
+ by the device.
+
+ """
+ params = {'UserName' : user_name,
+ 'SerialNumber' : serial_number,
+ 'AuthenticationCode1' : auth_code_1,
+ 'AuthenticationCode2' : auth_code_2}
+ return self.get_response('EnableMFADevice', params)
+
+ def deactivate_mfa_device(self, user_name, serial_number):
+ """
+ Deactivates the specified MFA device and removes it from
+ association with the user.
+
+ :type user_name: string
+ :param user_name: The username of the user
+
+ :type serial_number: string
+ :param seriasl_number: The serial number which uniquely identifies
+ the MFA device.
+
+ """
+ params = {'UserName' : user_name,
+ 'SerialNumber' : serial_number}
+ return self.get_response('DeactivateMFADevice', params)
+
+ def resync_mfa_device(self, user_name, serial_number,
+ auth_code_1, auth_code_2):
+ """
+ Syncronizes the specified MFA device with the AWS servers.
+
+ :type user_name: string
+ :param user_name: The username of the user
+
+ :type serial_number: string
+ :param seriasl_number: The serial number which uniquely identifies
+ the MFA device.
+
+ :type auth_code_1: string
+ :param auth_code_1: An authentication code emitted by the device.
+
+ :type auth_code_2: string
+ :param auth_code_2: A subsequent authentication code emitted
+ by the device.
+
+ """
+ params = {'UserName' : user_name,
+ 'SerialNumber' : serial_number,
+ 'AuthenticationCode1' : auth_code_1,
+ 'AuthenticationCode2' : auth_code_2}
+ return self.get_response('ResyncMFADevice', params)
+
+ #
+ # Login Profiles
+ #
+
+ def create_login_profile(self, user_name, password):
+ """
+ Creates a login profile for the specified user, give the user the
+ ability to access AWS services and the AWS Management Console.
+
+ :type user_name: string
+ :param user_name: The name of the new user
+
+ :type password: string
+ :param password: The new password for the user
+
+ """
+ params = {'UserName' : user_name,
+ 'Password' : password}
+ return self.get_response('CreateLoginProfile', params)
+
+ def delete_login_profile(self, user_name):
+ """
+ Deletes the login profile associated with the specified user.
+
+ :type user_name: string
+ :param user_name: The name of the user to delete.
+
+ """
+ params = {'UserName' : user_name}
+ return self.get_response('DeleteLoginProfile', params)
+
+ def update_login_profile(self, user_name, password):
+ """
+ Resets the password associated with the user's login profile.
+
+ :type user_name: string
+ :param user_name: The name of the user
+
+ :type password: string
+ :param password: The new password for the user
+
+ """
+ params = {'UserName' : user_name,
+ 'Password' : password}
+ return self.get_response('UpdateLoginProfile', params)
+
+
diff --git a/boto/iam/response.py b/boto/iam/response.py
new file mode 100644
index 00000000..aa01c147
--- /dev/null
+++ b/boto/iam/response.py
@@ -0,0 +1,147 @@
+# Copyright (c) 2010 Mitch Garnaat http://garnaat.org/
+#
+# 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.
+
+from boto import log
+import xml.sax
+
+def pythonize_name(name):
+ s = ''
+ if name[0].isupper:
+ s = name[0].lower()
+ for c in name[1:]:
+ if c.isupper():
+ s += '_' + c.lower()
+ else:
+ s += c
+ return s
+
+class XmlHandler(xml.sax.ContentHandler):
+
+ def __init__(self, root_node, connection):
+ self.connection = connection
+ self.nodes = [('root', root_node)]
+ self.current_text = ''
+
+ def startElement(self, name, attrs):
+ self.current_text = ''
+ t = self.nodes[-1][1].startElement(name, attrs, self.connection)
+ if t != None:
+ if isinstance(t, tuple):
+ self.nodes.append(t)
+ else:
+ self.nodes.append((name, t))
+
+ def endElement(self, name):
+ self.nodes[-1][1].endElement(name, self.current_text, self.connection)
+ if self.nodes[-1][0] == name:
+ self.nodes.pop()
+ self.current_text = ''
+
+ def characters(self, content):
+ self.current_text += content
+
+ def parse(self, s):
+ xml.sax.parseString(s, self)
+
+class Element(dict):
+
+ def __init__(self, connection=None, element_name=None,
+ stack=None, parent=None, list_marker='Set'):
+ dict.__init__(self)
+ self.connection = connection
+ self.element_name = element_name
+ self.list_marker = list_marker
+ if stack is None:
+ self.stack = []
+ else:
+ self.stack = stack
+ self.parent = parent
+
+ def __getattr__(self, key):
+ if key in self:
+ return self[key]
+ for k in self:
+ e = self[k]
+ if isinstance(e, Element):
+ try:
+ return getattr(e, key)
+ except AttributeError:
+ pass
+ raise AttributeError
+
+ def startElement(self, name, attrs, connection):
+ self.stack.append(name)
+ if name.endswith(self.list_marker):
+ l = ListElement(self.connection, name, self.list_marker)
+ self[pythonize_name(name)] = l
+ return l
+ if len(self.stack) > 0:
+ element_name = self.stack[-1]
+ e = Element(self.connection, element_name, self.stack, self, self.list_marker)
+ self[pythonize_name(element_name)] = e
+ return (element_name, e)
+ else:
+ return None
+
+ def endElement(self, name, value, connection):
+ if len(self.stack) > 0:
+ self.stack.pop()
+ value = value.strip()
+ if value:
+ if isinstance(self.parent, Element):
+ self.parent[pythonize_name(name)] = value
+ elif isinstance(self.parent, ListElement):
+ self.parent.append(value)
+
+class ListElement(list):
+
+ def __init__(self, connection=None, element_name=None,
+ list_marker='Set', item_marker='member'):
+ list.__init__(self)
+ self.connection = connection
+ self.element_name = element_name
+ self.list_marker = list_marker
+ self.item_marker = item_marker
+
+ def startElement(self, name, attrs, connection):
+ if name.endswith(self.list_marker):
+ l = ListElement(self.connection, name)
+ setattr(self, pythonize_name(name), l)
+ return l
+ elif name == self.item_marker:
+ e = Element(self.connection, name, parent=self)
+ self.append(e)
+ return e
+ else:
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == self.element_name:
+ if len(self) > 0:
+ empty = []
+ for e in self:
+ if isinstance(e, Element):
+ if len(e) == 0:
+ empty.append(e)
+ for e in empty:
+ self.remove(e)
+ else:
+ setattr(self, pythonize_name(name), value)
diff --git a/boto/provider.py b/boto/provider.py
index 026cb6df..84893bd9 100644
--- a/boto/provider.py
+++ b/boto/provider.py
@@ -51,6 +51,13 @@ STORAGE_CLASS_HEADER_KEY = 'storage-class'
MFA_HEADER_KEY = 'mfa-header'
VERSION_ID_HEADER_KEY = 'version-id-header'
+STORAGE_COPY_ERROR = 'StorageCopyError'
+STORAGE_CREATE_ERROR = 'StorageCreateError'
+STORAGE_DATA_ERROR = 'StorageDataError'
+STORAGE_PERMISSIONS_ERROR = 'StoragePermissionsError'
+STORAGE_RESPONSE_ERROR = 'StorageResponseError'
+
+
class Provider(object):
CredentialMap = {
@@ -109,6 +116,23 @@ class Provider(object):
MFA_HEADER_KEY : None,
}
}
+
+ ErrorMap = {
+ 'aws' : {
+ STORAGE_COPY_ERROR : boto.exception.S3CopyError,
+ STORAGE_CREATE_ERROR : boto.exception.S3CreateError,
+ STORAGE_DATA_ERROR : boto.exception.S3DataError,
+ STORAGE_PERMISSIONS_ERROR : boto.exception.S3PermissionsError,
+ STORAGE_RESPONSE_ERROR : boto.exception.S3ResponseError,
+ },
+ 'google' : {
+ STORAGE_COPY_ERROR : boto.exception.GSCopyError,
+ STORAGE_CREATE_ERROR : boto.exception.GSCreateError,
+ STORAGE_DATA_ERROR : boto.exception.GSDataError,
+ STORAGE_PERMISSIONS_ERROR : boto.exception.GSPermissionsError,
+ STORAGE_RESPONSE_ERROR : boto.exception.GSResponseError,
+ }
+ }
def __init__(self, name, access_key=None, secret_key=None):
self.host = None
@@ -119,6 +143,7 @@ class Provider(object):
self.canned_acls = self.CannedAclsMap[self.name]
self.get_credentials(access_key, secret_key)
self.configure_headers()
+ self.configure_errors()
# allow config file to override default host
host_opt_name = '%s_host' % self.HostKeyMap[self.name]
if (config.has_option('Credentials', host_opt_name)):
@@ -147,7 +172,8 @@ class Provider(object):
self.acl_header = header_info_map[ACL_HEADER_KEY]
self.auth_header = header_info_map[AUTH_HEADER_KEY]
self.copy_source_header = header_info_map[COPY_SOURCE_HEADER_KEY]
- self.copy_source_version_id = header_info_map[COPY_SOURCE_VERSION_ID_HEADER_KEY]
+ self.copy_source_version_id = header_info_map[
+ COPY_SOURCE_VERSION_ID_HEADER_KEY]
self.date_header = header_info_map[DATE_HEADER_KEY]
self.delete_marker = header_info_map[DELETE_MARKER_HEADER_KEY]
self.metadata_directive_header = (
@@ -157,6 +183,14 @@ class Provider(object):
self.version_id = header_info_map[VERSION_ID_HEADER_KEY]
self.mfa_header = header_info_map[MFA_HEADER_KEY]
+ def configure_errors(self):
+ error_map = self.ErrorMap[self.name]
+ self.storage_copy_error = error_map[STORAGE_COPY_ERROR]
+ self.storage_create_error = error_map[STORAGE_CREATE_ERROR]
+ self.storage_data_error = error_map[STORAGE_DATA_ERROR]
+ self.storage_permissions_error = error_map[STORAGE_PERMISSIONS_ERROR]
+ self.storage_response_error = error_map[STORAGE_RESPONSE_ERROR]
+
# Static utility method for getting default Provider.
def get_default():
return Provider('aws')
diff --git a/boto/s3/bucket.py b/boto/s3/bucket.py
index b5d7a0e6..0acfdbb3 100644
--- a/boto/s3/bucket.py
+++ b/boto/s3/bucket.py
@@ -23,13 +23,13 @@
import boto
from boto import handler
+from boto.provider import Provider
from boto.resultset import ResultSet
from boto.s3.acl import ACL, Policy, CannedACLStrings, Grant
from boto.s3.key import Key
from boto.s3.prefix import Prefix
from boto.s3.deletemarker import DeleteMarker
from boto.s3.user import User
-from boto.exception import S3ResponseError, S3PermissionsError, S3CopyError
from boto.s3.bucketlistresultset import BucketListResultSet
from boto.s3.bucketlistresultset import VersionedBucketListResultSet
import boto.utils
@@ -159,7 +159,8 @@ class Bucket(object):
response.read()
return None
else:
- raise S3ResponseError(response.status, response.reason, '')
+ raise self.connection.provider.storage_response_error(
+ response.status, response.reason, '')
def list(self, prefix='', delimiter='', marker='', headers=None):
"""
@@ -248,7 +249,8 @@ class Bucket(object):
xml.sax.parseString(body, h)
return rs
else:
- raise S3ResponseError(response.status, response.reason, body)
+ raise self.connection.provider.storage_response_error(
+ response.status, response.reason, body)
def get_all_keys(self, headers=None, **params):
"""
@@ -360,6 +362,7 @@ class Bucket(object):
deleting versioned objects from a bucket
that has the MFADelete option on the bucket.
"""
+ provider = self.connection.provider
if version_id:
query_args = 'versionId=%s' % version_id
else:
@@ -367,14 +370,14 @@ class Bucket(object):
if mfa_token:
if not headers:
headers = {}
- provider = self.connection.provider
headers[provider.mfa_header] = ' '.join(mfa_token)
response = self.connection.make_request('DELETE', self.name, key_name,
headers=headers,
query_args=query_args)
body = response.read()
if response.status != 204:
- raise S3ResponseError(response.status, response.reason, body)
+ raise provider.storage_response_error(response.status,
+ response.reason, body)
def copy_key(self, new_key_name, src_bucket_name,
src_key_name, metadata=None, src_version_id=None,
@@ -448,13 +451,13 @@ class Bucket(object):
h = handler.XmlHandler(key, self)
xml.sax.parseString(body, h)
if hasattr(key, 'Error'):
- raise S3CopyError(key.Code, key.Message, body)
+ raise provider.storage_copy_error(key.Code, key.Message, body)
key.handle_version_headers(response)
if preserve_acl:
self.set_xml_acl(acl, new_key_name)
return key
else:
- raise S3ResponseError(response.status, response.reason, body)
+ raise provider.storage_response_error(response.status, response.reason, body)
def set_canned_acl(self, acl_str, key_name='', headers=None,
version_id=None):
@@ -472,7 +475,8 @@ class Bucket(object):
headers=headers, query_args=query_args)
body = response.read()
if response.status != 200:
- raise S3ResponseError(response.status, response.reason, body)
+ raise self.connection.provider.storage_response_error(
+ response.status, response.reason, body)
def get_xml_acl(self, key_name='', headers=None, version_id=None):
query_args = 'acl'
@@ -483,7 +487,8 @@ class Bucket(object):
headers=headers)
body = response.read()
if response.status != 200:
- raise S3ResponseError(response.status, response.reason, body)
+ raise self.connection.provider.storage_response_error(
+ response.status, response.reason, body)
return body
def set_xml_acl(self, acl_str, key_name='', headers=None, version_id=None):
@@ -496,7 +501,8 @@ class Bucket(object):
headers=headers)
body = response.read()
if response.status != 200:
- raise S3ResponseError(response.status, response.reason, body)
+ raise self.connection.provider.storage_response_error(
+ response.status, response.reason, body)
def set_acl(self, acl_or_str, key_name='', headers=None, version_id=None):
if isinstance(acl_or_str, Policy):
@@ -520,7 +526,8 @@ class Bucket(object):
xml.sax.parseString(body, h)
return policy
else:
- raise S3ResponseError(response.status, response.reason, body)
+ raise self.connection.provider.storage_response_error(
+ response.status, response.reason, body)
def make_public(self, recursive=False, headers=None):
self.set_canned_acl('public-read', headers=headers)
@@ -554,7 +561,8 @@ class Bucket(object):
a long time!
"""
if permission not in S3Permissions:
- raise S3PermissionsError('Unknown Permission: %s' % permission)
+ raise self.connection.provider.storage_permissions_error(
+ 'Unknown Permission: %s' % permission)
policy = self.get_acl(headers=headers)
policy.acl.add_email_grant(permission, email_address)
self.set_acl(policy, headers=headers)
@@ -586,7 +594,8 @@ class Bucket(object):
a long time!
"""
if permission not in S3Permissions:
- raise S3PermissionsError('Unknown Permission: %s' % permission)
+ raise self.connection.provider.storage_permissions_error(
+ 'Unknown Permission: %s' % permission)
policy = self.get_acl(headers=headers)
policy.acl.add_user_grant(permission, user_id)
self.set_acl(policy, headers=headers)
@@ -615,7 +624,8 @@ class Bucket(object):
xml.sax.parseString(body, h)
return rs.LocationConstraint
else:
- raise S3ResponseError(response.status, response.reason, body)
+ raise self.connection.provider.storage_response_error(
+ response.status, response.reason, body)
def enable_logging(self, target_bucket, target_prefix='', headers=None):
if isinstance(target_bucket, Bucket):
@@ -627,7 +637,8 @@ class Bucket(object):
if response.status == 200:
return True
else:
- raise S3ResponseError(response.status, response.reason, body)
+ raise self.connection.provider.storage_response_error(
+ response.status, response.reason, body)
def disable_logging(self, headers=None):
body = self.EmptyBucketLoggingBody
@@ -637,7 +648,8 @@ class Bucket(object):
if response.status == 200:
return True
else:
- raise S3ResponseError(response.status, response.reason, body)
+ raise self.connection.provider.storage_response_error(
+ response.status, response.reason, body)
def get_logging_status(self, headers=None):
response = self.connection.make_request('GET', self.name,
@@ -646,7 +658,8 @@ class Bucket(object):
if response.status == 200:
return body
else:
- raise S3ResponseError(response.status, response.reason, body)
+ raise self.connection.provider.storage_response_error(
+ response.status, response.reason, body)
def set_as_logging_target(self, headers=None):
policy = self.get_acl(headers=headers)
@@ -663,7 +676,8 @@ class Bucket(object):
if response.status == 200:
return body
else:
- raise S3ResponseError(response.status, response.reason, body)
+ raise self.connection.provider.storage_response_error(
+ response.status, response.reason, body)
def set_request_payment(self, payer='BucketOwner', headers=None):
body = self.BucketPaymentBody % payer
@@ -673,7 +687,8 @@ class Bucket(object):
if response.status == 200:
return True
else:
- raise S3ResponseError(response.status, response.reason, body)
+ raise self.connection.provider.storage_response_error(
+ response.status, response.reason, body)
def configure_versioning(self, versioning, mfa_delete=False,
mfa_token=None, headers=None):
@@ -722,7 +737,8 @@ class Bucket(object):
if response.status == 200:
return True
else:
- raise S3ResponseError(response.status, response.reason, body)
+ raise self.connection.provider.storage_response_error(
+ response.status, response.reason, body)
def get_versioning_status(self, headers=None):
"""
@@ -750,7 +766,8 @@ class Bucket(object):
d['MfaDelete'] = mfa.group(1)
return d
else:
- raise S3ResponseError(response.status, response.reason, body)
+ raise self.connection.provider.storage_response_error(
+ response.status, response.reason, body)
def delete(self, headers=None):
return self.connection.delete_bucket(self.name, headers=headers)
diff --git a/boto/s3/connection.py b/boto/s3/connection.py
index 5fa11f0b..d37a9b79 100644
--- a/boto/s3/connection.py
+++ b/boto/s3/connection.py
@@ -27,10 +27,11 @@ import time
import boto.utils
from boto.connection import AWSAuthConnection
from boto import handler
+from boto.provider import Provider
from boto.s3.bucket import Bucket
from boto.s3.key import Key
from boto.resultset import ResultSet
-from boto.exception import S3ResponseError, S3CreateError, BotoClientError
+from boto.exception import BotoClientError
def check_lowercase_bucketname(n):
"""
@@ -297,7 +298,8 @@ class S3Connection(AWSAuthConnection):
response = self.make_request('GET')
body = response.read()
if response.status > 300:
- raise S3ResponseError(response.status, response.reason, body)
+ raise self.provider.storage_response_error(
+ response.status, response.reason, body)
rs = ResultSet([('Bucket', self.bucket_class)])
h = handler.XmlHandler(rs, self)
xml.sax.parseString(body, h)
@@ -365,17 +367,20 @@ class S3Connection(AWSAuthConnection):
data=data)
body = response.read()
if response.status == 409:
- raise S3CreateError(response.status, response.reason, body)
+ raise self.provider.storage_create_error(
+ response.status, response.reason, body)
if response.status == 200:
return self.bucket_class(self, bucket_name)
else:
- raise S3ResponseError(response.status, response.reason, body)
+ raise self.provider.storage_response_error(
+ response.status, response.reason, body)
def delete_bucket(self, bucket, headers=None):
response = self.make_request('DELETE', bucket, headers=headers)
body = response.read()
if response.status != 204:
- raise S3ResponseError(response.status, response.reason, body)
+ raise self.provider.storage_response_error(
+ response.status, response.reason, body)
def make_request(self, method, bucket='', key='', headers=None, data='',
query_args=None, sender=None):
diff --git a/boto/s3/key.py b/boto/s3/key.py
index 38939e18..f80ff67f 100644
--- a/boto/s3/key.py
+++ b/boto/s3/key.py
@@ -25,7 +25,8 @@ import rfc822
import StringIO
import base64
import boto.utils
-from boto.exception import S3ResponseError, S3DataError, BotoClientError
+from boto.exception import BotoClientError
+from boto.provider import Provider
from boto.s3.user import User
from boto import UserAgent
try:
@@ -117,15 +118,16 @@ class Key(object):
if self.resp == None:
self.mode = 'r'
+ provider = self.bucket.connection.provider
self.resp = self.bucket.connection.make_request('GET',
self.bucket.name,
self.name, headers,
query_args=query_args)
if self.resp.status < 199 or self.resp.status > 299:
body = self.resp.read()
- raise S3ResponseError(self.resp.status, self.resp.reason, body)
+ raise provider.storage_response_error(self.resp.status,
+ self.resp.reason, body)
response_headers = self.resp.msg
- provider = self.bucket.connection.provider
self.metadata = boto.utils.get_aws_metadata(response_headers,
provider)
for name,value in response_headers.items():
@@ -404,6 +406,8 @@ class Key(object):
your callback to be called with each buffer read.
"""
+ provider = self.bucket.connection.provider
+
def sender(http_conn, method, path, data, headers):
http_conn.putrequest(method, path)
for key in headers:
@@ -444,10 +448,12 @@ class Key(object):
elif response.status >= 200 and response.status <= 299:
self.etag = response.getheader('etag')
if self.etag != '"%s"' % self.md5:
- raise S3DataError('ETag from S3 did not match computed MD5')
+ raise provider.storage_data_error(
+ 'ETag from S3 did not match computed MD5')
return response
else:
- raise S3ResponseError(response.status, response.reason, body)
+ raise provider.storage_response_error(
+ response.status, response.reason, body)
if not headers:
headers = {}
@@ -456,12 +462,11 @@ class Key(object):
headers['User-Agent'] = UserAgent
headers['Content-MD5'] = self.base64md5
if self.storage_class != 'STANDARD':
- provider = self.bucket.connection.provider
headers[provider.storage_class_header] = self.storage_class
- if headers.has_key('Content-Type'):
- self.content_type = headers['Content-Type']
if headers.has_key('Content-Encoding'):
self.content_encoding = headers['Content-Encoding']
+ if headers.has_key('Content-Type'):
+ self.content_type = headers['Content-Type']
elif self.path:
self.content_type = mimetypes.guess_type(self.path)[0]
if self.content_type == None:
@@ -471,8 +476,7 @@ class Key(object):
headers['Content-Type'] = self.content_type
headers['Content-Length'] = str(self.size)
headers['Expect'] = '100-Continue'
- headers = boto.utils.merge_meta(headers, self.metadata,
- self.bucket.connection.provider)
+ headers = boto.utils.merge_meta(headers, self.metadata, provider)
resp = self.bucket.connection.make_request('PUT', self.bucket.name,
self.name, headers,
sender=sender)
diff --git a/boto/sdb/db/manager/sdbmanager.py b/boto/sdb/db/manager/sdbmanager.py
index 7221dfe2..8aca5ad7 100644
--- a/boto/sdb/db/manager/sdbmanager.py
+++ b/boto/sdb/db/manager/sdbmanager.py
@@ -103,7 +103,7 @@ class SDBConverter:
if Model in item_type.mro():
item_type = Model
encoded_value = self.encode(item_type, value[key])
- if encoded_value != None and encoded_value != "None":
+ if encoded_value != None:
new_value.append('%s:%s' % (key, encoded_value))
return new_value
@@ -122,7 +122,7 @@ class SDBConverter:
item_type = getattr(prop, "item_type")
dec_val = {}
for val in value:
- if val != "None" and val != None:
+ if val != None:
k,v = self.decode_map_element(item_type, val)
try:
k = int(k)
@@ -272,7 +272,7 @@ class SDBConverter:
def encode_reference(self, value):
if value in (None, 'None', '', ' '):
- return 'None'
+ return None
if isinstance(value, str) or isinstance(value, unicode):
return value
else:
@@ -462,7 +462,7 @@ class SDBManager(object):
def _build_filter(self, property, name, op, val):
if val == None:
if op in ('is','='):
- return "`%s` is null" % name
+ return "`%(name)s` is null" % {"name": name}
elif op in ('is not', '!='):
return "`%s` is not null" % name
else:
@@ -557,12 +557,16 @@ class SDBManager(object):
attrs = {'__type__' : obj.__class__.__name__,
'__module__' : obj.__class__.__module__,
'__lineage__' : obj.get_lineage()}
+ del_attrs = []
for property in obj.properties(hidden=False):
value = property.get_value_for_datastore(obj)
if value is not None:
value = self.encode_value(property, value)
if value == []:
value = None
+ if value == None:
+ del_attrs.append(property.name)
+ continue
attrs[property.name] = value
if property.unique:
try:
@@ -573,6 +577,9 @@ class SDBManager(object):
except(StopIteration):
pass
self.domain.put_attributes(obj.id, attrs, replace=True)
+ if len(del_attrs) > 0:
+ self.domain.delete_attributes(obj.id, del_attrs)
+ return obj
def delete_object(self, obj):
self.domain.delete_attributes(obj.id)
diff --git a/boto/sdb/db/property.py b/boto/sdb/db/property.py
index ead30fec..6fe2f023 100644
--- a/boto/sdb/db/property.py
+++ b/boto/sdb/db/property.py
@@ -43,7 +43,10 @@ class Property(object):
self.required = required
self.validator = validator
self.choices = choices
- self.slot_name = '_'
+ if self.name:
+ self.slot_name = '_' + self.name
+ else:
+ self.slot_name = '_'
self.unique = unique
def __get__(self, obj, objtype):
diff --git a/boto/sdb/item.py b/boto/sdb/item.py
index d6a56a95..7edce7bb 100644
--- a/boto/sdb/item.py
+++ b/boto/sdb/item.py
@@ -88,6 +88,14 @@ class Item(dict):
def save(self, replace=True):
self.domain.put_attributes(self.name, self, replace)
+ # Delete any attributes set to "None"
+ if replace:
+ del_attrs = []
+ for name in self:
+ if self[name] == None:
+ del_attrs.append(name)
+ if len(del_attrs) > 0:
+ self.domain.delete_attributes(self.name, del_attrs)
def add_value(self, key, value):
if key in self:
diff --git a/boto/storage_uri.py b/boto/storage_uri.py
index 29114fab..66ad572f 100755
--- a/boto/storage_uri.py
+++ b/boto/storage_uri.py
@@ -155,7 +155,7 @@ class BucketStorageUri(StorageUri):
"""
def __init__(self, scheme, bucket_name=None, object_name=None,
- debug=False):
+ debug=0):
"""Instantiate a BucketStorageUri from scheme,bucket,object tuple.
@type scheme: string
@@ -164,8 +164,8 @@ class BucketStorageUri(StorageUri):
@param bucket_name: bucket name
@type object_name: string
@param object_name: object name
- @type debug: bool
- @param debug: whether to turn on debugging on calls to this class
+ @type debug: int
+ @param debug: debug level to pass in to connection (range 0..2)
After instantiation the components are available in the following
fields: uri, scheme, bucket_name, object_name.
diff --git a/boto/tests/test_gsconnection.py b/boto/tests/test_gsconnection.py
index 34679a83..c6c90716 100644
--- a/boto/tests/test_gsconnection.py
+++ b/boto/tests/test_gsconnection.py
@@ -32,7 +32,6 @@ import time
import os
import urllib
from boto.gs.connection import GSConnection
-from boto.exception import S3PermissionsError
class GSConnectionTest (unittest.TestCase):
diff --git a/setup.py b/setup.py
index fd691389..9f0b4661 100644
--- a/setup.py
+++ b/setup.py
@@ -39,14 +39,14 @@ setup(name = "boto",
scripts = ["bin/sdbadmin", "bin/elbadmin", "bin/cfadmin",
"bin/s3put", "bin/fetch_file", "bin/launch_instance",
"bin/list_instances", "bin/taskadmin", "bin/kill_instance",
- "bin/bundle_image", "bin/pyami_sendmail", "bin/lss3"],
+ "bin/bundle_image", "bin/pyami_sendmail", "bin/lss3", "bin/cq"],
url = "http://code.google.com/p/boto/",
packages = [ 'boto', 'boto.sqs', 'boto.s3', 'boto.gs', 'boto.file',
'boto.ec2', 'boto.ec2.cloudwatch', 'boto.ec2.autoscale', 'boto.ec2.elb',
'boto.sdb', 'boto.sdb.persist', 'boto.sdb.db', 'boto.sdb.db.manager',
'boto.mturk', 'boto.pyami', 'boto.mashups', 'boto.contrib', 'boto.manage',
'boto.services', 'boto.tests', 'boto.cloudfront', 'boto.rds', 'boto.vpc',
- 'boto.fps', 'boto.emr', 'boto.sns'],
+ 'boto.fps', 'boto.emr', 'boto.sns', 'boto.ecs'],
license = 'MIT',
platforms = 'Posix; MacOS X; Windows',
classifiers = [ 'Development Status :: 5 - Production/Stable',