Import http://svn.github.com/boto/boto.git at revision 2005

git-svn-id: svn://svn.chromium.org/boto@1 4f2e627c-b00b-48dd-b1fb-2c643665b734
diff --git a/README b/README
new file mode 100644
index 0000000..35b7232
--- /dev/null
+++ b/README
@@ -0,0 +1,65 @@
+boto 2.0b4
+13-Feb-2011
+
+Copyright (c) 2006-2011 Mitch Garnaat <mitch@garnaat.org>
+Copyright (c) 2010-2011, Eucalyptus Systems, Inc.
+All rights reserved.
+
+http://code.google.com/p/boto
+
+Boto is a Python package that provides interfaces to Amazon Web Services.
+At the moment, boto supports:
+
+ * Simple Storage Service (S3)
+ * SimpleQueue Service (SQS)
+ * Elastic Compute Cloud (EC2)
+ * Mechanical Turk
+ * SimpleDB
+ * CloudFront
+ * CloudWatch
+ * AutoScale
+ * Elastic Load Balancer (ELB)
+ * Virtual Private Cloud (VPC)
+ * Elastic Map Reduce (EMR)
+ * Relational Data Service (RDS) 
+ * Simple Notification Server (SNS)
+ * Google Storage
+ * Identity and Access Management (IAM)
+ * Route53 DNS Service (route53)
+ * Simple Email Service (SES)
+
+The intent is to support additional services in the future.
+
+The goal of boto is to provide a very simple, easy to use, lightweight
+wrapper around the Amazon services.  Not all features supported by the
+Amazon Web Services will be supported in boto.  Basically, those
+features I need to do what I want to do are supported first.  Other
+features and requests are welcome and will be accomodated to the best
+of my ability.  Patches and contributions are welcome!
+
+Boto was written using Python 2.6.5 on Mac OSX.  It has also been tested
+on Linux Ubuntu using Python 2.6.5.  Boto requires no additional
+libraries or packages other than those that are distributed with Python 2.6.5.
+Efforts are made to keep boto compatible with Python 2.4.x but no
+guarantees are made.
+
+Documentation for boto can be found at:
+
+http://boto.cloudhackers.com/
+
+Join our `IRC channel`_ (#boto on FreeNode).
+    IRC channel: http://webchat.freenode.net/?channels=boto
+ 
+Your credentials can be passed into the methods that create 
+connections.  Alternatively, boto will check for the existance of the
+following environment variables to ascertain your credentials:
+
+AWS_ACCESS_KEY_ID - Your AWS Access Key ID
+AWS_SECRET_ACCESS_KEY - Your AWS Secret Access Key
+
+Credentials and other boto-related settings can also be stored in a boto config
+file.  See:
+
+http://code.google.com/p/boto/wiki/BotoConfig
+
+for details.
\ No newline at end of file
diff --git a/bin/bundle_image b/bin/bundle_image
new file mode 100755
index 0000000..7096979
--- /dev/null
+++ b/bin/bundle_image
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+from boto.manage.server import Server
+if __name__ == "__main__":
+    from optparse import OptionParser
+    parser = OptionParser(version="%prog 1.0", usage="Usage: %prog [options] instance-id [instance-id-2]")
+
+    # Commands
+    parser.add_option("-b", "--bucket", help="Destination Bucket", dest="bucket", default=None)
+    parser.add_option("-p", "--prefix", help="AMI Prefix", dest="prefix", default=None)
+    parser.add_option("-k", "--key", help="Private Key File", dest="key_file", default=None)
+    parser.add_option("-c", "--cert", help="Public Certificate File", dest="cert_file", default=None)
+    parser.add_option("-s", "--size", help="AMI Size", dest="size", default=None)
+    parser.add_option("-i", "--ssh-key", help="SSH Keyfile", dest="ssh_key", default=None)
+    parser.add_option("-u", "--user-name", help="SSH Username", dest="uname", default="root")
+    parser.add_option("-n", "--name", help="Name of Image", dest="name")
+    (options, args) = parser.parse_args()
+
+    for instance_id in args:
+        try:
+            s = Server.find(instance_id=instance_id).next()
+            print "Found old server object"
+        except StopIteration:
+            print "New Server Object Created"
+            s = Server.create_from_instance_id(instance_id, options.name)
+        assert(s.hostname is not None)
+        b = s.get_bundler(uname=options.uname)
+        b.bundle(bucket=options.bucket,prefix=options.prefix,key_file=options.key_file,cert_file=options.cert_file,size=int(options.size),ssh_key=options.ssh_key)
diff --git a/bin/cfadmin b/bin/cfadmin
new file mode 100755
index 0000000..97726c1
--- /dev/null
+++ b/bin/cfadmin
@@ -0,0 +1,84 @@
+#!/usr/bin/env python
+# Author: Chris Moyer
+#
+# cfadmin is similar to sdbadmin for CloudFront, it's a simple
+# console utility to perform the most frequent tasks with CloudFront
+#
+def _print_distributions(dists):
+	"""Internal function to print out all the distributions provided"""
+	print "%-12s %-50s %s" % ("Status", "Domain Name", "Origin")
+	print "-"*80
+	for d in dists:
+		print "%-12s %-50s %-30s" % (d.status, d.domain_name, d.origin)
+		for cname in d.cnames:
+			print " "*12, "CNAME => %s" % cname
+	print ""
+
+def help(cf, fnc=None):
+	"""Print help message, optionally about a specific function"""
+	import inspect
+	self = sys.modules['__main__']
+	if fnc:
+		try:
+			cmd = getattr(self, fnc)
+		except:
+			cmd = None
+		if not inspect.isfunction(cmd):
+			print "No function named: %s found" % fnc
+			sys.exit(2)
+		(args, varargs, varkw, defaults) = inspect.getargspec(cmd)
+		print cmd.__doc__
+		print "Usage: %s %s" % (fnc, " ".join([ "[%s]" % a for a in args[1:]]))
+	else:
+		print "Usage: cfadmin [command]"
+		for cname in dir(self):
+			if not cname.startswith("_"):
+				cmd = getattr(self, cname)
+				if inspect.isfunction(cmd):
+					doc = cmd.__doc__
+					print "\t%s - %s" % (cname, doc)
+	sys.exit(1)
+
+def ls(cf):
+	"""List all distributions and streaming distributions"""
+	print "Standard Distributions"
+	_print_distributions(cf.get_all_distributions())
+	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.dns_name == 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
+	import sys
+	cf = boto.connect_cloudfront()
+	self = sys.modules['__main__']
+	if len(sys.argv) >= 2:
+		try:
+			cmd = getattr(self, sys.argv[1])
+		except:
+			cmd = None
+		args = sys.argv[2:]
+	else:
+		cmd = help
+		args = []
+	if not cmd:
+		cmd = help
+	try:
+		cmd(cf, *args)
+	except TypeError, e:
+		print e
+		help(cf, cmd.__name__)
diff --git a/bin/cq b/bin/cq
new file mode 100755
index 0000000..258002d
--- /dev/null
+++ b/bin/cq
@@ -0,0 +1,82 @@
+#!/usr/bin/env python
+# Copyright (c) 2006,2007 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 getopt, sys
+from boto.sqs.connection import SQSConnection
+from boto.exception import SQSError
+
+def usage():
+    print 'cq [-c] [-q queue_name] [-o output_file] [-t timeout]'
+  
+def main():
+    try:
+        opts, args = getopt.getopt(sys.argv[1:], 'hcq:o:t:',
+                                   ['help', 'clear', 'queue',
+                                    'output', 'timeout'])
+    except:
+        usage()
+        sys.exit(2)
+    queue_name = ''
+    output_file = ''
+    timeout = 30
+    clear = False
+    for o, a in opts:
+        if o in ('-h', '--help'):
+            usage()
+            sys.exit()
+        if o in ('-q', '--queue'):
+            queue_name = a
+        if o in ('-o', '--output'):
+            output_file = a
+        if o in ('-c', '--clear'):
+            clear = True
+        if o in ('-t', '--timeout'):
+            timeout = int(a)
+    c = SQSConnection()
+    if queue_name:
+        try:
+            rs = [c.create_queue(queue_name)]
+        except SQSError, e:
+            print 'An Error Occurred:'
+            print '%s: %s' % (e.status, e.reason)
+            print e.body
+            sys.exit()
+    else:
+        try:
+            rs = c.get_all_queues()
+        except SQSError, e:
+            print 'An Error Occurred:'
+            print '%s: %s' % (e.status, e.reason)
+            print e.body
+            sys.exit()
+    for q in rs:
+        if clear:
+            n = q.clear()
+            print 'clearing %d messages from %s' % (n, q.id)
+        elif output_file:
+            q.dump(output_file)
+        else:
+            print q.id, q.count(vtimeout=timeout)
+
+if __name__ == "__main__":
+    main()
+        
diff --git a/bin/elbadmin b/bin/elbadmin
new file mode 100755
index 0000000..a5ec6bb
--- /dev/null
+++ b/bin/elbadmin
@@ -0,0 +1,219 @@
+#!/usr/bin/env python
+# Copyright (c) 2009 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
+
+#
+# Elastic Load Balancer Tool
+#
+VERSION="0.1"
+usage = """%prog [options] [command]
+Commands:
+    list|ls                           List all Elastic Load Balancers
+    delete    <name>                  Delete ELB <name>
+    get       <name>                  Get all instances associated with <name>
+    create    <name>                  Create an ELB
+    add       <name> <instance>       Add <instance> in ELB <name>
+    remove|rm <name> <instance>       Remove <instance> from ELB <name>
+    enable|en <name> <zone>           Enable Zone <zone> for ELB <name>
+    disable   <name> <zone>           Disable Zone <zone> for ELB <name>
+    addl      <name>                  Add listeners (specified by -l) to the ELB <name>
+    rml       <name> <port>           Remove Listener(s) specified by the port on the ELB
+"""
+
+def list(elb):
+    """List all ELBs"""
+    print "%-20s %s" %  ("Name", "DNS Name")
+    print "-"*80
+    for b in elb.get_all_load_balancers():
+        print "%-20s %s" % (b.name, b.dns_name)
+
+def get(elb, name):
+    """Get details about ELB <name>"""
+    elbs = elb.get_all_load_balancers(name)
+    if len(elbs) < 1:
+        print "No load balancer by the name of %s found" % name
+        return
+    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 "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 "  Zones  "
+            print "---------"
+            for z in b.availability_zones:
+                print z
+
+            print
+
+            print "Instances"
+            print "---------"
+            for i in b.instances:
+                print i.id
+
+            print
+
+def create(elb, name, zones, listeners):
+    """Create an ELB named <name>"""
+    l_list = []
+    for l in listeners:
+        l = l.split(",")
+        if l[2]=='HTTPS':
+            l_list.append((int(l[0]), int(l[1]), l[2], l[3]))
+        else : l_list.append((int(l[0]), int(l[1]), l[2]))
+    
+    b = elb.create_load_balancer(name, zones, l_list)
+    return get(elb, name)
+
+def delete(elb, name):
+    """Delete this ELB"""
+    b = elb.get_all_load_balancers(name)
+    if len(b) < 1:
+        print "No load balancer by the name of %s found" % name
+        return
+    for i in b:
+        if name in i.name:
+            i.delete()
+    print "Load Balancer %s deleted" % name
+
+def add_instance(elb, name, instance):
+    """Add <instance> to ELB <name>"""
+    b = elb.get_all_load_balancers(name)
+    if len(b) < 1:
+        print "No load balancer by the name of %s found" % name
+        return
+    for i in b:
+        if name in i.name:
+            i.register_instances([instance])
+    return get(elb, name)
+
+
+def remove_instance(elb, name, instance):
+    """Remove instance from elb <name>"""
+    b = elb.get_all_load_balancers(name)
+    if len(b) < 1:
+        print "No load balancer by the name of %s found" % name
+        return
+    for i in b:
+        if name in i.name:
+            i.deregister_instances([instance])
+    return get(elb, name)
+
+def enable_zone(elb, name, zone):
+    """Enable <zone> for elb"""
+    b = elb.get_all_load_balancers(name)
+    if len(b) < 1:
+        print "No load balancer by the name of %s found" % name
+        return
+    b = b[0]
+    b.enable_zones([zone])
+    return get(elb, name)
+
+def disable_zone(elb, name, zone):
+    """Disable <zone> for elb"""
+    b = elb.get_all_load_balancers(name)
+    if len(b) < 1:
+        print "No load balancer by the name of %s found" % name
+        return
+    b = b[0]
+    b.disable_zones([zone])
+    return get(elb, name)
+
+def add_listener(elb, name, listeners):
+    """Add listeners to a given load balancer"""
+    l_list = []
+    for l in listeners:
+        l = l.split(",")
+        l_list.append((int(l[0]), int(l[1]), l[2]))
+    b = elb.get_all_load_balancers(name)
+    if len(b) < 1:
+        print "No load balancer by the name of %s found" % name
+        return
+    b = b[0]
+    b.create_listeners(l_list)
+    return get(elb, name)
+
+def rm_listener(elb, name, ports):
+    """Remove listeners from a given load balancer"""
+    b = elb.get_all_load_balancers(name)
+    if len(b) < 1:
+        print "No load balancer by the name of %s found" % name
+        return
+    b = b[0]
+    b.delete_listeners(ports)
+    return get(elb, name)
+
+
+
+
+if __name__ == "__main__":
+    try:
+        import readline
+    except ImportError:
+        pass
+    import boto
+    import sys
+    from optparse import OptionParser
+    from boto.mashups.iobject import IObject
+    parser = OptionParser(version=VERSION, usage=usage)
+    parser.add_option("-z", "--zone", help="Operate on zone", action="append", default=[], dest="zones")
+    parser.add_option("-l", "--listener", help="Specify Listener in,out,proto", action="append", default=[], dest="listeners")
+
+    (options, args) = parser.parse_args()
+
+    if len(args) < 1:
+        parser.print_help()
+        sys.exit(1)
+
+    elb = boto.connect_elb()
+
+    print "%s" % (elb.region.endpoint)
+
+    command = args[0].lower()
+    if command in ("ls", "list"):
+        list(elb)
+    elif command == "get":
+        get(elb, args[1])
+    elif command == "create":
+        create(elb, args[1], options.zones, options.listeners)
+    elif command == "delete":
+        delete(elb, args[1])
+    elif command in ("add", "put"):
+        add_instance(elb, args[1], args[2])
+    elif command in ("rm", "remove"):
+        remove_instance(elb, args[1], args[2])
+    elif command in ("en", "enable"):
+        enable_zone(elb, args[1], args[2])
+    elif command == "disable":
+        disable_zone(elb, args[1], args[2])
+    elif command == "addl":
+        add_listener(elb, args[1], options.listeners)
+    elif command == "rml":
+        rm_listener(elb, args[1], args[2:])
diff --git a/bin/fetch_file b/bin/fetch_file
new file mode 100755
index 0000000..6b8c4da
--- /dev/null
+++ b/bin/fetch_file
@@ -0,0 +1,37 @@
+#!/usr/bin/env python
+# Copyright (c) 2009 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.
+#
+if __name__ == "__main__":
+	from optparse import OptionParser
+	parser = OptionParser(version="0.1", usage="Usage: %prog [options] url")
+	parser.add_option("-o", "--out-file", help="Output file", dest="outfile")
+
+	(options, args) = parser.parse_args()
+	if len(args) < 1:
+		parser.print_help()
+		exit(1)
+	from boto.utils import fetch_file
+	f = fetch_file(args[0])
+	if options.outfile:
+		open(options.outfile, "w").write(f.read())
+	else:
+		print f.read()
diff --git a/bin/kill_instance b/bin/kill_instance
new file mode 100755
index 0000000..0c63741
--- /dev/null
+++ b/bin/kill_instance
@@ -0,0 +1,35 @@
+#!/usr/bin/env python
+
+import sys
+from optparse import OptionParser
+
+import boto
+from boto.ec2 import regions
+
+
+
+def kill_instance(region, ids):
+    """Kill an instances given it's instance IDs"""
+    # Connect the region
+    ec2 = boto.connect_ec2(region=region)
+    for instance_id in ids:
+        print "Stopping instance: %s" % instance_id
+        ec2.terminate_instances([instance_id])
+
+
+if __name__ == "__main__":
+    parser = OptionParser(usage="kill_instance [-r] id [id ...]")
+    parser.add_option("-r", "--region", help="Region (default us-east-1)", dest="region", default="us-east-1")
+    (options, args) = parser.parse_args()
+    if not args:
+        parser.print_help()
+        sys.exit(1)
+    for r in regions():
+        if r.name == options.region:
+            region = r
+            break
+    else:
+        print "Region %s not found." % options.region
+        sys.exit(1)
+
+    kill_instance(region, args)
diff --git a/bin/launch_instance b/bin/launch_instance
new file mode 100755
index 0000000..cb857d9
--- /dev/null
+++ b/bin/launch_instance
@@ -0,0 +1,197 @@
+#!/usr/bin/env python
+# Copyright (c) 2009 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
+
+#
+# Utility to launch an EC2 Instance
+#
+VERSION="0.2"
+
+
+CLOUD_INIT_SCRIPT = """#!/usr/bin/env python
+f = open("/etc/boto.cfg", "w")
+f.write(\"\"\"%s\"\"\")
+f.close()
+"""
+import boto.pyami.config
+from boto.utils import fetch_file
+import re, os
+import ConfigParser
+
+class Config(boto.pyami.config.Config):
+    """A special config class that also adds import abilities
+    Directly in the config file. To have a config file import
+    another config file, simply use "#import <path>" where <path>
+    is either a relative path or a full URL to another config
+    """
+
+    def __init__(self):
+        ConfigParser.SafeConfigParser.__init__(self, {'working_dir' : '/mnt/pyami', 'debug' : '0'})
+
+    def add_config(self, file_url):
+        """Add a config file to this configuration
+        :param file_url: URL for the file to add, or a local path
+        :type file_url: str
+        """
+        if not re.match("^([a-zA-Z0-9]*:\/\/)(.*)", file_url):
+            if not file_url.startswith("/"):
+                file_url = os.path.join(os.getcwd(), file_url)
+            file_url = "file://%s" % file_url
+        (base_url, file_name) = file_url.rsplit("/", 1)
+        base_config = fetch_file(file_url)
+        base_config.seek(0)
+        for line in base_config.readlines():
+            match = re.match("^#import[\s\t]*([^\s^\t]*)[\s\t]*$", line)
+            if match:
+                self.add_config("%s/%s" % (base_url, match.group(1)))
+        base_config.seek(0)
+        self.readfp(base_config)
+
+    def add_creds(self, ec2):
+        """Add the credentials to this config if they don't already exist"""
+        if not self.has_section('Credentials'):
+            self.add_section('Credentials')
+            self.set('Credentials', 'aws_access_key_id', ec2.aws_access_key_id)
+            self.set('Credentials', 'aws_secret_access_key', ec2.aws_secret_access_key)
+
+    
+    def __str__(self):
+        """Get config as string"""
+        from StringIO import StringIO
+        s = StringIO()
+        self.write(s)
+        return s.getvalue()
+
+
+if __name__ == "__main__":
+    try:
+        import readline
+    except ImportError:
+        pass
+    import sys
+    import time
+    import boto
+    from boto.ec2 import regions
+    from optparse import OptionParser
+    from boto.mashups.iobject import IObject
+    parser = OptionParser(version=VERSION, usage="%prog [options] config_url")
+    parser.add_option("-c", "--max-count", help="Maximum number of this type of instance to launch", dest="max_count", default="1")
+    parser.add_option("--min-count", help="Minimum number of this type of instance to launch", dest="min_count", default="1")
+    parser.add_option("--cloud-init", help="Indicates that this is an instance that uses 'CloudInit', Ubuntu's cloud bootstrap process. This wraps the config in a shell script command instead of just passing it in directly", dest="cloud_init", default=False, action="store_true")
+    parser.add_option("-g", "--groups", help="Security Groups to add this instance to",  action="append", dest="groups")
+    parser.add_option("-a", "--ami", help="AMI to launch", dest="ami_id")
+    parser.add_option("-t", "--type", help="Type of Instance (default m1.small)", dest="type", default="m1.small")
+    parser.add_option("-k", "--key", help="Keypair", dest="key_name")
+    parser.add_option("-z", "--zone", help="Zone (default us-east-1a)", dest="zone", default="us-east-1a")
+    parser.add_option("-r", "--region", help="Region (default us-east-1)", dest="region", default="us-east-1")
+    parser.add_option("-i", "--ip", help="Elastic IP", dest="elastic_ip")
+    parser.add_option("-n", "--no-add-cred", help="Don't add a credentials section", default=False, action="store_true", dest="nocred")
+    parser.add_option("--save-ebs", help="Save the EBS volume on shutdown, instead of deleting it", default=False, action="store_true", dest="save_ebs")
+    parser.add_option("-w", "--wait", help="Wait until instance is running", default=False, action="store_true", dest="wait")
+    parser.add_option("-d", "--dns", help="Returns public and private DNS (implicates --wait)", default=False, action="store_true", dest="dns")
+    parser.add_option("-T", "--tag", help="Set tag", default=None, action="append", dest="tags", metavar="key:value")
+
+    (options, args) = parser.parse_args()
+
+    if len(args) < 1:
+        parser.print_help()
+        sys.exit(1)
+    file_url = os.path.expanduser(args[0])
+
+    cfg = Config()
+    cfg.add_config(file_url)
+
+    for r in regions():
+        if r.name == options.region:
+            region = r
+            break
+    else:
+        print "Region %s not found." % options.region
+        sys.exit(1)
+    ec2 = boto.connect_ec2(region=region)
+    if not options.nocred:
+        cfg.add_creds(ec2)
+
+    iobj = IObject()
+    if options.ami_id:
+        ami = ec2.get_image(options.ami_id)
+    else:
+        ami_id = options.ami_id
+        l = [(a, a.id, a.location) for a in ec2.get_all_images()]
+        ami = iobj.choose_from_list(l, prompt='Choose AMI')
+
+    if options.key_name:
+        key_name = options.key_name
+    else:
+        l = [(k, k.name, '') for k in ec2.get_all_key_pairs()]
+        key_name = iobj.choose_from_list(l, prompt='Choose Keypair').name
+
+    if options.groups:
+        groups = options.groups
+    else:
+        groups = []
+        l = [(g, g.name, g.description) for g in ec2.get_all_security_groups()]
+        g = iobj.choose_from_list(l, prompt='Choose Primary Security Group')
+        while g != None:
+            groups.append(g)
+            l.remove((g, g.name, g.description))
+            g = iobj.choose_from_list(l, prompt='Choose Additional Security Group (0 to quit)')
+
+    user_data = str(cfg)
+    # If it's a cloud init AMI,
+    # then we need to wrap the config in our
+    # little wrapper shell script
+    if options.cloud_init:
+        user_data = CLOUD_INIT_SCRIPT % user_data
+    shutdown_proc = "terminate"
+    if options.save_ebs:
+        shutdown_proc = "save"
+
+    r = ami.run(min_count=int(options.min_count), max_count=int(options.max_count),
+            key_name=key_name, user_data=user_data,
+            security_groups=groups, instance_type=options.type,
+            placement=options.zone, instance_initiated_shutdown_behavior=shutdown_proc)
+
+    instance = r.instances[0]
+
+    if options.tags:
+        for tag_pair in options.tags:
+            name = tag_pair
+            value = ''
+            if ':' in tag_pair:
+                name, value = tag_pair.split(':', 1)
+            instance.add_tag(name, value)
+
+    if options.dns:
+        options.wait = True
+
+    if not options.wait:
+        sys.exit(0)
+
+    while True:
+        instance.update()
+        if instance.state == 'running':
+            break
+        time.sleep(3)
+
+    if options.dns:
+        print "Public DNS name: %s" % instance.public_dns_name
+        print "Private DNS name: %s" % instance.private_dns_name
+
diff --git a/bin/list_instances b/bin/list_instances
new file mode 100755
index 0000000..5abe9b6
--- /dev/null
+++ b/bin/list_instances
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+
+import sys
+from operator import attrgetter
+from optparse import OptionParser
+
+import boto
+from boto.ec2 import regions
+
+
+HEADERS = {
+    'ID': {'get': attrgetter('id'), 'length':15},
+    'Zone': {'get': attrgetter('placement'), 'length':15},
+    'Groups': {'get': attrgetter('groups'), 'length':30},
+    'Hostname': {'get': attrgetter('public_dns_name'), 'length':50},
+    'State': {'get': attrgetter('state'), 'length':15},
+    'Image': {'get': attrgetter('image_id'), 'length':15},
+    'Type': {'get': attrgetter('instance_type'), 'length':15},
+    'IP': {'get': attrgetter('ip_address'), 'length':16},
+    'PrivateIP': {'get': attrgetter('private_ip_address'), 'length':16},
+    'Key': {'get': attrgetter('key_name'), 'length':25},
+    'T:': {'length': 30},
+}
+
+def get_column(name, instance=None):
+    if name.startswith('T:'):
+        _, tag = name.split(':', 1)
+        return instance.tags.get(tag, '')
+    return HEADERS[name]['get'](instance)
+
+
+def main():
+    parser = OptionParser()
+    parser.add_option("-r", "--region", help="Region (default us-east-1)", dest="region", default="us-east-1")
+    parser.add_option("-H", "--headers", help="Set headers (use 'T:tagname' for including tags)", default=None, action="store", dest="headers", metavar="ID,Zone,Groups,Hostname,State,T:Name")
+    (options, args) = parser.parse_args()
+
+    # Connect the region
+    for r in regions():
+        if r.name == options.region:
+            region = r
+            break
+    else:
+        print "Region %s not found." % options.region
+        sys.exit(1)
+    ec2 = boto.connect_ec2(region=region)
+
+    # Read headers
+    if options.headers:
+        headers = tuple(options.headers.split(','))
+    else:
+        headers = ("ID", 'Zone', "Groups", "Hostname")
+
+    # Create format string
+    format_string = ""
+    for h in headers:
+        if h.startswith('T:'):
+            format_string += "%%-%ds" % HEADERS['T:']['length']
+        else:
+            format_string += "%%-%ds" % HEADERS[h]['length']
+
+
+    # List and print
+    print format_string % headers
+    print "-" * len(format_string % headers)
+    for r in ec2.get_all_instances():
+        groups = [g.id for g in r.groups]
+        for i in r.instances:
+            i.groups = ','.join(groups)
+            print format_string % tuple(get_column(h, i) for h in headers)
+
+if __name__ == "__main__":
+    main()
diff --git a/bin/lss3 b/bin/lss3
new file mode 100755
index 0000000..1fba89d
--- /dev/null
+++ b/bin/lss3
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+import boto
+
+def sizeof_fmt(num):
+    for x in ['b ','KB','MB','GB','TB', 'XB']:
+        if num < 1024.0:
+            return "%3.1f %s" % (num, x)
+        num /= 1024.0
+    return "%3.1f %s" % (num, x)
+
+def list_bucket(b):
+    """List everything in a bucket"""
+    total = 0
+    for k in b:
+        mode = "-rwx---"
+        for g in k.get_acl().acl.grants:
+            if g.id == None:
+                if g.permission == "READ":
+                    mode = "-rwxr--"
+                elif g.permission == "FULL_CONTROL":
+                    mode = "-rwxrwx"
+        print "%s\t%010s\t%s" % (mode, sizeof_fmt(k.size), k.name)
+        total += k.size
+    print "="*60
+    print "TOTAL: \t%010s" % sizeof_fmt(total)
+
+def list_buckets(s3):
+    """List all the buckets"""
+    for b in s3.get_all_buckets():
+        print b.name
+
+if __name__ == "__main__":
+    import sys
+    s3 = boto.connect_s3()
+    if len(sys.argv) < 2:
+        list_buckets(s3)
+    else:
+        for name in sys.argv[1:]:
+            list_bucket(s3.get_bucket(name))
diff --git a/bin/pyami_sendmail b/bin/pyami_sendmail
new file mode 100755
index 0000000..78e3003
--- /dev/null
+++ b/bin/pyami_sendmail
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+# 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
+
+#
+# Send Mail from a PYAMI instance, or anything that has a boto.cfg
+# properly set up
+#
+VERSION="0.1"
+usage = """%prog [options]
+Sends whatever is on stdin to the recipient specified by your boto.cfg
+or whoevery you specify in the options here.
+"""
+
+if __name__ == "__main__":
+    from boto.utils import notify
+    import sys
+    from optparse import OptionParser
+    parser = OptionParser(version=VERSION, usage=usage)
+    parser.add_option("-t", "--to", help="Optional to address to send to (default from your boto.cfg)", action="store", default=None, dest="to")
+    parser.add_option("-s", "--subject", help="Optional Subject to send this report as", action="store", default="Report", dest="subject")
+    parser.add_option("-f", "--file", help="Optionally, read from a file instead of STDIN", action="store", default=None, dest="file")
+
+    (options, args) = parser.parse_args()
+    if options.file:
+        body = open(options.file, 'r').read()
+    else:
+        body = sys.stdin.read()
+
+    notify(options.subject, body=body, to_string=options.to)
diff --git a/bin/route53 b/bin/route53
new file mode 100755
index 0000000..55f86a5
--- /dev/null
+++ b/bin/route53
@@ -0,0 +1,105 @@
+#!/usr/bin/env python
+# Author: Chris Moyer
+#
+# route53 is similar to sdbadmin for Route53, it's a simple
+# console utility to perform the most frequent tasks with Route53
+
+def _print_zone_info(zoneinfo):
+    print "="*80
+    print "| ID:   %s" % zoneinfo['Id'].split("/")[-1]
+    print "| Name: %s" % zoneinfo['Name']
+    print "| Ref:  %s" % zoneinfo['CallerReference']
+    print "="*80
+    print zoneinfo['Config']
+    print
+    
+
+def create(conn, hostname, caller_reference=None, comment=''):
+    """Create a hosted zone, returning the nameservers"""
+    response = conn.create_hosted_zone(hostname, caller_reference, comment)
+    print "Pending, please add the following Name Servers:"
+    for ns in response.NameServers:
+        print "\t", ns
+
+def delete_zone(conn, hosted_zone_id):
+    """Delete a hosted zone by ID"""
+    response = conn.delete_hosted_zone(hosted_zone_id)
+    print response
+
+def ls(conn):
+    """List all hosted zones"""
+    response = conn.get_all_hosted_zones()
+    for zoneinfo in response['ListHostedZonesResponse']['HostedZones']:
+        _print_zone_info(zoneinfo)
+
+def get(conn, hosted_zone_id, type=None, name=None, maxitems=None):
+    """Get all the records for a single zone"""
+    response = conn.get_all_rrsets(hosted_zone_id, type, name, maxitems=maxitems)
+    print '%-20s %-20s %-20s %s' % ("Name", "Type", "TTL", "Value(s)")
+    for record in response:
+        print '%-20s %-20s %-20s %s' % (record.name, record.type, record.ttl, ",".join(record.resource_records))
+
+
+def add_record(conn, hosted_zone_id, name, type, value, ttl=600, comment=""):
+    """Add a new record to a zone"""
+    from boto.route53.record import ResourceRecordSets
+    changes = ResourceRecordSets(conn, hosted_zone_id, comment)
+    change = changes.add_change("CREATE", name, type, ttl)
+    change.add_value(value)
+    print changes.commit()
+
+def del_record(conn, hosted_zone_id, name, type, value, ttl=600, comment=""):
+    """Delete a record from a zone"""
+    from boto.route53.record import ResourceRecordSets
+    changes = ResourceRecordSets(conn, hosted_zone_id, comment)
+    change = changes.add_change("DELETE", name, type, ttl)
+    change.add_value(value)
+    print changes.commit()
+
+def help(conn, fnc=None):
+    """Prints this help message"""
+    import inspect
+    self = sys.modules['__main__']
+    if fnc:
+        try:
+            cmd = getattr(self, fnc)
+        except:
+            cmd = None
+        if not inspect.isfunction(cmd):
+            print "No function named: %s found" % fnc
+            sys.exit(2)
+        (args, varargs, varkw, defaults) = inspect.getargspec(cmd)
+        print cmd.__doc__
+        print "Usage: %s %s" % (fnc, " ".join([ "[%s]" % a for a in args[1:]]))
+    else:
+        print "Usage: route53 [command]"
+        for cname in dir(self):
+            if not cname.startswith("_"):
+                cmd = getattr(self, cname)
+                if inspect.isfunction(cmd):
+                    doc = cmd.__doc__
+                    print "\t%s - %s" % (cname, doc)
+    sys.exit(1)
+
+
+if __name__ == "__main__":
+    import boto
+    import sys
+    conn = boto.connect_route53()
+    self = sys.modules['__main__']
+    if len(sys.argv) >= 2:
+        try:
+            cmd = getattr(self, sys.argv[1])
+        except:
+            cmd = None
+        args = sys.argv[2:]
+    else:
+        cmd = help
+        args = []
+    if not cmd:
+        cmd = help
+    try:
+        cmd(conn, *args)
+    except TypeError, e:
+        print e
+        help(conn, cmd.__name__)
diff --git a/bin/s3put b/bin/s3put
new file mode 100755
index 0000000..b5467d9
--- /dev/null
+++ b/bin/s3put
@@ -0,0 +1,196 @@
+#!/usr/bin/env python
+# Copyright (c) 2006,2007,2008 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 getopt, sys, os
+import boto
+from boto.exception import S3ResponseError
+
+usage_string = """
+SYNOPSIS
+    s3put [-a/--access_key <access_key>] [-s/--secret_key <secret_key>]
+          -b/--bucket <bucket_name> [-c/--callback <num_cb>]
+          [-d/--debug <debug_level>] [-i/--ignore <ignore_dirs>]
+          [-n/--no_op] [-p/--prefix <prefix>] [-q/--quiet]
+          [-g/--grant grant] [-w/--no_overwrite] path
+
+    Where
+        access_key - Your AWS Access Key ID.  If not supplied, boto will
+                     use the value of the environment variable
+                     AWS_ACCESS_KEY_ID
+        secret_key - Your AWS Secret Access Key.  If not supplied, boto
+                     will use the value of the environment variable
+                     AWS_SECRET_ACCESS_KEY
+        bucket_name - The name of the S3 bucket the file(s) should be
+                      copied to.
+        path - A path to a directory or file that represents the items
+               to be uploaded.  If the path points to an individual file,
+               that file will be uploaded to the specified bucket.  If the
+               path points to a directory, s3_it will recursively traverse
+               the directory and upload all files to the specified bucket.
+        debug_level - 0 means no debug output (default), 1 means normal
+                      debug output from boto, and 2 means boto debug output
+                      plus request/response output from httplib
+        ignore_dirs - a comma-separated list of directory names that will
+                      be ignored and not uploaded to S3.
+        num_cb - The number of progress callbacks to display.  The default
+                 is zero which means no callbacks.  If you supplied a value
+                 of "-c 10" for example, the progress callback would be
+                 called 10 times for each file transferred.
+        prefix - A file path prefix that will be stripped from the full
+                 path of the file when determining the key name in S3.
+                 For example, if the full path of a file is:
+                     /home/foo/bar/fie.baz
+                 and the prefix is specified as "-p /home/foo/" the
+                 resulting key name in S3 will be:
+                     /bar/fie.baz
+                 The prefix must end in a trailing separator and if it
+                 does not then one will be added.
+        grant - A canned ACL policy that will be granted on each file
+                transferred to S3.  The value of provided must be one
+                of the "canned" ACL policies supported by S3:
+                private|public-read|public-read-write|authenticated-read
+        no_overwrite - No files will be overwritten on S3, if the file/key 
+                       exists on s3 it will be kept. This is useful for 
+                       resuming interrupted transfers. Note this is not a 
+                       sync, even if the file has been updated locally if 
+                       the key exists on s3 the file on s3 will not be 
+                       updated.
+
+     If the -n option is provided, no files will be transferred to S3 but
+     informational messages will be printed about what would happen.
+"""
+def usage():
+    print usage_string
+    sys.exit()
+  
+def submit_cb(bytes_so_far, total_bytes):
+    print '%d bytes transferred / %d bytes total' % (bytes_so_far, total_bytes)
+
+def get_key_name(fullpath, prefix):
+    key_name = fullpath[len(prefix):]
+    l = key_name.split(os.sep)
+    return '/'.join(l)
+
+def main():
+    try:
+        opts, args = getopt.getopt(sys.argv[1:], 'a:b:c::d:g:hi:np:qs:vw',
+                                   ['access_key', 'bucket', 'callback', 'debug', 'help', 'grant',
+                                    'ignore', 'no_op', 'prefix', 'quiet', 'secret_key', 'no_overwrite'])
+    except:
+        usage()
+    ignore_dirs = []
+    aws_access_key_id = None
+    aws_secret_access_key = None
+    bucket_name = ''
+    total = 0
+    debug = 0
+    cb = None
+    num_cb = 0
+    quiet = False
+    no_op = False
+    prefix = '/'
+    grant = None
+    no_overwrite = False
+    for o, a in opts:
+        if o in ('-h', '--help'):
+            usage()
+            sys.exit()
+        if o in ('-a', '--access_key'):
+            aws_access_key_id = a
+        if o in ('-b', '--bucket'):
+            bucket_name = a
+        if o in ('-c', '--callback'):
+            num_cb = int(a)
+            cb = submit_cb
+        if o in ('-d', '--debug'):
+            debug = int(a)
+        if o in ('-g', '--grant'):
+            grant = a
+        if o in ('-i', '--ignore'):
+            ignore_dirs = a.split(',')
+        if o in ('-n', '--no_op'):
+            no_op = True
+        if o in ('w', '--no_overwrite'):
+            no_overwrite = True
+        if o in ('-p', '--prefix'):
+            prefix = a
+            if prefix[-1] != os.sep:
+                prefix = prefix + os.sep
+        if o in ('-q', '--quiet'):
+            quiet = True
+        if o in ('-s', '--secret_key'):
+            aws_secret_access_key = a
+    if len(args) != 1:
+        print usage()
+    path = os.path.expanduser(args[0])
+    path = os.path.expandvars(path)
+    path = os.path.abspath(path)
+    if bucket_name:
+        c = boto.connect_s3(aws_access_key_id=aws_access_key_id,
+                            aws_secret_access_key=aws_secret_access_key)
+        c.debug = debug
+        b = c.get_bucket(bucket_name)
+        if os.path.isdir(path):
+            if no_overwrite:
+                if not quiet:
+                    print 'Getting list of existing keys to check against'
+                keys = []
+                for key in b.list():
+                    keys.append(key.name)
+            for root, dirs, files in os.walk(path):
+                for ignore in ignore_dirs:
+                    if ignore in dirs:
+                        dirs.remove(ignore)
+                for file in files:
+                    fullpath = os.path.join(root, file)
+                    key_name = get_key_name(fullpath, prefix)
+                    copy_file = True
+                    if no_overwrite:
+                        if key_name in keys:
+                            copy_file = False
+                            if not quiet:
+                                print 'Skipping %s as it exists in s3' % file
+                    if copy_file:
+                        if not quiet:
+                            print 'Copying %s to %s/%s' % (file, bucket_name, key_name)
+                        if not no_op:
+                            k = b.new_key(key_name)
+                            k.set_contents_from_filename(fullpath, cb=cb,
+                                                            num_cb=num_cb, policy=grant)
+                    total += 1
+        elif os.path.isfile(path):
+            key_name = os.path.split(path)[1]
+            copy_file = True
+            if no_overwrite:
+                if b.get_key(key_name):
+                    copy_file = False
+                    if not quiet:
+                        print 'Skipping %s as it exists in s3' % path
+            if copy_file:
+                k = b.new_key(key_name)
+                k.set_contents_from_filename(path, cb=cb, num_cb=num_cb, policy=grant)
+    else:
+        print usage()
+
+if __name__ == "__main__":
+    main()
+        
diff --git a/bin/sdbadmin b/bin/sdbadmin
new file mode 100755
index 0000000..e8ff9b5
--- /dev/null
+++ b/bin/sdbadmin
@@ -0,0 +1,168 @@
+#!/usr/bin/env python
+# Copyright (c) 2009 Chris Moyer http://kopertop.blogspot.com/
+#
+# 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
+
+#
+# Tools to dump and recover an SDB domain
+#
+VERSION = "%prog version 1.0"
+import boto
+import time
+from boto import sdb
+
+def choice_input(options, default=None, title=None):
+    """
+    Choice input
+    """
+    if title == None:
+        title = "Please choose"
+    print title
+    objects = []
+    for n, obj in enumerate(options):
+        print "%s: %s" % (n, obj)
+        objects.append(obj)
+    choice = int(raw_input(">>> "))
+    try:
+        choice = objects[choice]
+    except:
+        choice = default
+    return choice
+
+def confirm(message="Are you sure?"):
+    choice = raw_input("%s [yN] " % message)
+    return choice and len(choice) > 0 and choice[0].lower() == "y"
+
+
+def dump_db(domain, file_name):
+    """
+    Dump SDB domain to file
+    """
+    doc = domain.to_xml(open(file_name, "w"))
+
+def empty_db(domain):
+    """
+    Remove all entries from domain
+    """
+    for item in domain:
+        item.delete()
+
+def load_db(domain, file):
+    """
+    Load a domain from a file, this doesn't overwrite any existing
+    data in the file so if you want to do a full recovery and restore
+    you need to call empty_db before calling this
+
+    :param domain: The SDB Domain object to load to
+    :param file: The File to load the DB from
+    """
+    domain.from_xml(file)
+
+def create_db(domain_name, region_name):
+    """Create a new DB
+
+    :param domain: Name of the domain to create
+    :type domain: str
+    """
+    sdb = boto.sdb.connect_to_region(region_name)
+    return sdb.create_domain(domain_name)
+
+if __name__ == "__main__":
+    from optparse import OptionParser
+    parser = OptionParser(version=VERSION, usage="Usage: %prog [--dump|--load|--empty|--list|-l] [options]")
+
+    # Commands
+    parser.add_option("--dump", help="Dump domain to file", dest="dump", default=False, action="store_true")
+    parser.add_option("--load", help="Load domain contents from file", dest="load", default=False, action="store_true")
+    parser.add_option("--empty", help="Empty all contents of domain", dest="empty", default=False, action="store_true")
+    parser.add_option("-l", "--list", help="List All domains", dest="list", default=False, action="store_true")
+    parser.add_option("-c", "--create", help="Create domain", dest="create", default=False, action="store_true")
+
+    parser.add_option("-a", "--all-domains", help="Operate on all domains", action="store_true", default=False, dest="all_domains")
+    parser.add_option("-d", "--domain", help="Do functions on domain (may be more then one)", action="append", dest="domains")
+    parser.add_option("-f", "--file", help="Input/Output file we're operating on", dest="file_name")
+    parser.add_option("-r", "--region", help="Region (e.g. us-east-1[default] or eu-west-1)", default="us-east-1", dest="region_name")
+    (options, args) = parser.parse_args()
+
+    if options.create:
+        for domain_name in options.domains:
+            create_db(domain_name, options.region_name)
+        exit()
+
+    sdb = boto.sdb.connect_to_region(options.region_name)
+    if options.list:
+        for db in sdb.get_all_domains():
+            print db
+        exit()
+
+    if not options.dump and not options.load and not options.empty:
+            parser.print_help()
+            exit()
+
+
+
+
+    #
+    # Setup
+    #
+    if options.domains:
+        domains = []
+        for domain_name in options.domains:
+            domains.append(sdb.get_domain(domain_name))
+    elif options.all_domains:
+        domains = sdb.get_all_domains()
+    else:
+        domains = [choice_input(options=sdb.get_all_domains(), title="No domain specified, please choose one")]
+
+
+    #
+    # Execute the commands
+    #
+    stime = time.time()
+    if options.empty:
+        if confirm("WARNING!!! Are you sure you want to empty the following domains?: %s" % domains):
+            stime = time.time()
+            for domain in domains:
+                print "--------> Emptying %s <--------" % domain.name
+                empty_db(domain)
+        else:
+            print "Canceling operations"
+            exit()
+
+    if options.dump:
+        for domain in domains:
+            print "--------> Dumping %s <---------" % domain.name
+            if options.file_name:
+                file_name = options.file_name
+            else:
+                file_name = "%s.db" % domain.name
+            dump_db(domain, file_name)
+
+    if options.load:
+        for domain in domains:
+            print "---------> Loading %s <----------" % domain.name
+            if options.file_name:
+                file_name = options.file_name
+            else:
+                file_name = "%s.db" % domain.name
+            load_db(domain, open(file_name, "rb"))
+
+
+    total_time = round(time.time() - stime, 2)
+    print "--------> Finished in %s <--------" % total_time
diff --git a/bin/taskadmin b/bin/taskadmin
new file mode 100755
index 0000000..5d5302a
--- /dev/null
+++ b/bin/taskadmin
@@ -0,0 +1,116 @@
+#!/usr/bin/env python
+# Copyright (c) 2009 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
+
+#
+# Task/Job Administration utility
+#
+VERSION="0.1"
+__version__ = VERSION
+usage = """%prog [options] [command]
+Commands:
+    list|ls                                List all Tasks in SDB
+    delete      <id>                       Delete Task with id <id>
+    get         <name>                     Get Task <name>
+    create|mk   <name> <hour> <command>    Create a new Task <name> with command <command> running every <hour>
+"""
+
+def list():
+    """List all Tasks in SDB"""
+    from boto.manage.task import Task
+    print "%-8s %-40s %s" %  ("Hour", "Name", "Command")
+    print "-"*100
+    for t in Task.all():
+        print "%-8s %-40s %s" % (t.hour, t.name, t.command)
+
+def get(name):
+    """Get a task
+    :param name: The name of the task to fetch
+    :type name: str
+    """
+    from boto.manage.task import Task
+    q = Task.find()
+    q.filter("name like", "%s%%" % name)
+    for t in q:
+        print "="*80
+        print "|               ", t.id
+        print "|%s" %  ("-"*79)
+        print "| Name:         ", t.name
+        print "| Hour:         ", t.hour
+        print "| Command:      ", t.command
+        if t.last_executed:
+            print "| Last Run:     ", t.last_executed.ctime()
+            print "| Last Status:  ", t.last_status
+            print "| Last Run Log: ", t.last_output
+        print "="*80
+
+def delete(id):
+    from boto.manage.task import Task
+    t = Task.get_by_id(id)
+    print "Deleting task: %s" % t.name
+    if raw_input("Are you sure? ").lower() in ["y", "yes"]:
+        t.delete()
+        print "Deleted"
+    else:
+        print "Canceled"
+
+def create(name, hour, command):
+    """Create a new task
+    :param name: Name of the task to create
+    :type name: str
+    :param hour: What hour to run it at, "*" for every hour
+    :type hour: str
+    :param command: The command to execute
+    :type command: str
+    """
+    from boto.manage.task import Task
+    t = Task()
+    t.name = name
+    t.hour = hour
+    t.command = command
+    t.put()
+    print "Created task: %s" % t.id
+
+if __name__ == "__main__":
+    try:
+        import readline
+    except ImportError:
+        pass
+    import boto
+    import sys
+    from optparse import OptionParser
+    from boto.mashups.iobject import IObject
+    parser = OptionParser(version=__version__, usage=usage)
+
+    (options, args) = parser.parse_args()
+
+    if len(args) < 1:
+        parser.print_help()
+        sys.exit(1)
+
+    command = args[0].lower()
+    if command in ("ls", "list"):
+        list()
+    elif command == "get":
+        get(args[1])
+    elif command == "create":
+        create(args[1], args[2], args[3])
+    elif command == "delete":
+        delete(args[1])
diff --git a/boto/__init__.py b/boto/__init__.py
new file mode 100644
index 0000000..d11b578
--- /dev/null
+++ b/boto/__init__.py
@@ -0,0 +1,542 @@
+# Copyright (c) 2006-2011 Mitch Garnaat http://garnaat.org/
+# Copyright (c) 2010-2011, Eucalyptus Systems, Inc.
+# 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 boto
+from boto.pyami.config import Config, BotoConfigLocations
+from boto.storage_uri import BucketStorageUri, FileStorageUri
+import boto.plugin
+import os, re, sys
+import logging
+import logging.config
+from boto.exception import InvalidUriError
+
+__version__ = '2.0b4'
+Version = __version__ # for backware compatibility
+
+UserAgent = 'Boto/%s (%s)' % (__version__, sys.platform)
+config = Config()
+
+def init_logging():
+    for file in BotoConfigLocations:
+        try:
+            logging.config.fileConfig(os.path.expanduser(file))
+        except:
+            pass
+
+class NullHandler(logging.Handler):
+    def emit(self, record):
+        pass
+
+log = logging.getLogger('boto')
+log.addHandler(NullHandler())
+init_logging()
+
+# convenience function to set logging to a particular file
+def set_file_logger(name, filepath, level=logging.INFO, format_string=None):
+    global log
+    if not format_string:
+        format_string = "%(asctime)s %(name)s [%(levelname)s]:%(message)s"
+    logger = logging.getLogger(name)
+    logger.setLevel(level)
+    fh = logging.FileHandler(filepath)
+    fh.setLevel(level)
+    formatter = logging.Formatter(format_string)
+    fh.setFormatter(formatter)
+    logger.addHandler(fh)
+    log = logger
+
+def set_stream_logger(name, level=logging.DEBUG, format_string=None):
+    global log
+    if not format_string:
+        format_string = "%(asctime)s %(name)s [%(levelname)s]:%(message)s"
+    logger = logging.getLogger(name)
+    logger.setLevel(level)
+    fh = logging.StreamHandler()
+    fh.setLevel(level)
+    formatter = logging.Formatter(format_string)
+    fh.setFormatter(formatter)
+    logger.addHandler(fh)
+    log = logger
+
+def connect_sqs(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.sqs.connection.SQSConnection`
+    :return: A connection to Amazon's SQS
+    """
+    from boto.sqs.connection import SQSConnection
+    return SQSConnection(aws_access_key_id, aws_secret_access_key, **kwargs)
+
+def connect_s3(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.s3.connection.S3Connection`
+    :return: A connection to Amazon's S3
+    """
+    from boto.s3.connection import S3Connection
+    return S3Connection(aws_access_key_id, aws_secret_access_key, **kwargs)
+
+def connect_gs(gs_access_key_id=None, gs_secret_access_key=None, **kwargs):
+    """
+    @type gs_access_key_id: string
+    @param gs_access_key_id: Your Google Storage Access Key ID
+
+    @type gs_secret_access_key: string
+    @param gs_secret_access_key: Your Google Storage Secret Access Key
+
+    @rtype: L{GSConnection<boto.gs.connection.GSConnection>}
+    @return: A connection to Google's Storage service
+    """
+    from boto.gs.connection import GSConnection
+    return GSConnection(gs_access_key_id, gs_secret_access_key, **kwargs)
+
+def connect_ec2(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.ec2.connection.EC2Connection`
+    :return: A connection to Amazon's EC2
+    """
+    from boto.ec2.connection import EC2Connection
+    return EC2Connection(aws_access_key_id, aws_secret_access_key, **kwargs)
+
+def connect_elb(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.ec2.elb.ELBConnection`
+    :return: A connection to Amazon's Load Balancing Service
+    """
+    from boto.ec2.elb import ELBConnection
+    return ELBConnection(aws_access_key_id, aws_secret_access_key, **kwargs)
+
+def connect_autoscale(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.ec2.autoscale.AutoScaleConnection`
+    :return: A connection to Amazon's Auto Scaling Service
+    """
+    from boto.ec2.autoscale import AutoScaleConnection
+    return AutoScaleConnection(aws_access_key_id, aws_secret_access_key, **kwargs)
+
+def connect_cloudwatch(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.ec2.cloudwatch.CloudWatchConnection`
+    :return: A connection to Amazon's EC2 Monitoring service
+    """
+    from boto.ec2.cloudwatch import CloudWatchConnection
+    return CloudWatchConnection(aws_access_key_id, aws_secret_access_key, **kwargs)
+
+def connect_sdb(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.sdb.connection.SDBConnection`
+    :return: A connection to Amazon's SDB
+    """
+    from boto.sdb.connection import SDBConnection
+    return SDBConnection(aws_access_key_id, aws_secret_access_key, **kwargs)
+
+def connect_fps(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.fps.connection.FPSConnection`
+    :return: A connection to FPS
+    """
+    from boto.fps.connection import FPSConnection
+    return FPSConnection(aws_access_key_id, aws_secret_access_key, **kwargs)
+
+def connect_mturk(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.mturk.connection.MTurkConnection`
+    :return: A connection to MTurk
+    """
+    from boto.mturk.connection import MTurkConnection
+    return MTurkConnection(aws_access_key_id, aws_secret_access_key, **kwargs)
+
+def connect_cloudfront(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.fps.connection.FPSConnection`
+    :return: A connection to FPS
+    """
+    from boto.cloudfront import CloudFrontConnection
+    return CloudFrontConnection(aws_access_key_id, aws_secret_access_key, **kwargs)
+
+def connect_vpc(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.vpc.VPCConnection`
+    :return: A connection to VPC
+    """
+    from boto.vpc import VPCConnection
+    return VPCConnection(aws_access_key_id, aws_secret_access_key, **kwargs)
+
+def connect_rds(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.rds.RDSConnection`
+    :return: A connection to RDS
+    """
+    from boto.rds import RDSConnection
+    return RDSConnection(aws_access_key_id, aws_secret_access_key, **kwargs)
+
+def connect_emr(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.emr.EmrConnection`
+    :return: A connection to Elastic mapreduce
+    """
+    from boto.emr import EmrConnection
+    return EmrConnection(aws_access_key_id, aws_secret_access_key, **kwargs)
+
+def connect_sns(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.sns.SNSConnection`
+    :return: A connection to Amazon's SNS
+    """
+    from boto.sns import SNSConnection
+    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_route53(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.dns.Route53Connection`
+    :return: A connection to Amazon's Route53 DNS Service
+    """
+    from boto.route53 import Route53Connection
+    return Route53Connection(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 connect_ses(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.ses.SESConnection`
+    :return: A connection to Amazon's SES
+    """
+    from boto.ses import SESConnection
+    return SESConnection(aws_access_key_id, aws_secret_access_key, **kwargs)
+
+def connect_ia(ia_access_key_id=None, ia_secret_access_key=None,
+               is_secure=False, **kwargs):
+    """
+    Connect to the Internet Archive via their S3-like API.
+
+    :type ia_access_key_id: string
+    :param ia_access_key_id: Your IA Access Key ID.  This will also look in your
+                             boto config file for an entry in the Credentials
+                             section called "ia_access_key_id"
+
+    :type ia_secret_access_key: string
+    :param ia_secret_access_key: Your IA Secret Access Key.  This will also look in your
+                                 boto config file for an entry in the Credentials
+                                 section called "ia_secret_access_key"
+
+    :rtype: :class:`boto.s3.connection.S3Connection`
+    :return: A connection to the Internet Archive
+    """
+    from boto.s3.connection import S3Connection
+    from boto.s3.connection import OrdinaryCallingFormat
+
+    access_key = config.get('Credentials', 'ia_access_key_id',
+                            ia_access_key_id)
+    secret_key = config.get('Credentials', 'ia_secret_access_key',
+                            ia_secret_access_key)
+
+    return S3Connection(access_key, secret_key,
+                        host='s3.us.archive.org',
+                        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
+    __init__.py file of all boto modules.  See:
+    http://code.google.com/p/boto/wiki/ExtendModules
+
+    for details.
+    """
+    option_name = '%s_extend' % module_name
+    version = config.get('Boto', option_name, None)
+    if version:
+        dirname = module_path[0]
+        path = os.path.join(dirname, version)
+        if os.path.isdir(path):
+            log.info('extending module %s with: %s' % (module_name, path))
+            module_path.insert(0, path)
+
+_aws_cache = {}
+
+def _get_aws_conn(service):
+    global _aws_cache
+    conn = _aws_cache.get(service)
+    if not conn:
+        meth = getattr(sys.modules[__name__], 'connect_' + service)
+        conn = meth()
+        _aws_cache[service] = conn
+    return conn
+
+def lookup(service, name):
+    global _aws_cache
+    conn = _get_aws_conn(service)
+    obj = _aws_cache.get('.'.join((service, name)), None)
+    if not obj:
+        obj = conn.lookup(name)
+        _aws_cache['.'.join((service, name))] = obj
+    return obj
+
+def storage_uri(uri_str, default_scheme='file', debug=0, validate=True,
+                bucket_storage_uri_class=BucketStorageUri):
+    """
+    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: 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.
+    :type bucket_storage_uri_class: BucketStorageUri interface.
+    :param bucket_storage_uri_class: Allows mocking for unit tests.
+
+    We allow validate to be disabled to allow caller
+    to implement bucket-level wildcarding (outside the boto library;
+    see gsutil).
+
+    :rtype: :class:`boto.StorageUri` subclass
+    :return: StorageUri subclass for given URI.
+
+    ``uri_str`` must be one of the following formats:
+
+    * gs://bucket/name
+    * s3://bucket/name
+    * gs://bucket
+    * s3://bucket
+    * filename
+
+    The last example uses the default scheme ('file', unless overridden)
+    """
+
+    # Manually parse URI components instead of using urlparse.urlparse because
+    # what we're calling URIs don't really fit the standard syntax for URIs
+    # (the latter includes an optional host/net location part).
+    end_scheme_idx = uri_str.find('://')
+    if end_scheme_idx == -1:
+        # Check for common error: user specifies gs:bucket instead
+        # of gs://bucket. Some URI parsers allow this, but it can cause
+        # confusion for callers, so we don't.
+        if uri_str.find(':') != -1:
+            raise InvalidUriError('"%s" contains ":" instead of "://"' % uri_str)
+        scheme = default_scheme.lower()
+        path = uri_str
+    else:
+        scheme = uri_str[0:end_scheme_idx].lower()
+        path = uri_str[end_scheme_idx + 3:]
+
+    if scheme not in ['file', 's3', 'gs']:
+        raise InvalidUriError('Unrecognized scheme "%s"' % scheme)
+    if scheme == 'file':
+        # For file URIs we have no bucket name, and use the complete path
+        # (minus 'file://') as the object name.
+        return FileStorageUri(path, debug)
+    else:
+        path_parts = path.split('/', 1)
+        bucket_name = path_parts[0]
+        if (validate and bucket_name and
+            # Disallow buckets violating charset or not [3..255] chars total.
+            (not re.match('^[a-z0-9][a-z0-9\._-]{1,253}[a-z0-9]$', bucket_name)
+            # Disallow buckets with individual DNS labels longer than 63.
+             or re.search('[-_a-z0-9]{64}', bucket_name))):
+            raise InvalidUriError('Invalid bucket name in URI "%s"' % uri_str)
+        # If enabled, ensure the bucket name is valid, to avoid possibly
+        # confusing other parts of the code. (For example if we didn't
+        # catch bucket names containing ':', when a user tried to connect to
+        # the server with that name they might get a confusing error about
+        # non-integer port numbers.)
+        object_name = ''
+        if len(path_parts) > 1:
+            object_name = path_parts[1]
+        return bucket_storage_uri_class(scheme, bucket_name, object_name, debug)
+
+def storage_uri_for_key(key):
+    """Returns a StorageUri for the given key.
+
+    :type key: :class:`boto.s3.key.Key` or subclass
+    :param key: URI naming bucket + optional object.
+    """
+    if not isinstance(key, boto.s3.key.Key):
+        raise InvalidUriError('Requested key (%s) is not a subclass of '
+                              'boto.s3.key.Key' % str(type(key)))
+    prov_name = key.bucket.connection.provider.get_provider_name()
+    uri_str = '%s://%s/%s' % (prov_name, key.bucket.name, key.name)
+    return storage_uri(uri_str)
+
+boto.plugin.load_plugins(config)
diff --git a/boto/auth.py b/boto/auth.py
new file mode 100644
index 0000000..6c6c1f2
--- /dev/null
+++ b/boto/auth.py
@@ -0,0 +1,319 @@
+# Copyright 2010 Google Inc.
+# Copyright (c) 2011 Mitch Garnaat http://garnaat.org/
+# Copyright (c) 2011, Eucalyptus Systems, Inc.
+#
+# 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.
+
+
+"""
+Handles authentication required to AWS and GS
+"""
+
+import base64
+import boto
+import boto.auth_handler
+import boto.exception
+import boto.plugin
+import boto.utils
+import hmac
+import sys
+import time
+import urllib
+
+from boto.auth_handler import AuthHandler
+from boto.exception import BotoClientError
+#
+# the following is necessary because of the incompatibilities
+# between Python 2.4, 2.5, and 2.6 as well as the fact that some
+# people running 2.4 have installed hashlib as a separate module
+# this fix was provided by boto user mccormix.
+# see: http://code.google.com/p/boto/issues/detail?id=172
+# for more details.
+#
+try:
+    from hashlib import sha1 as sha
+    from hashlib import sha256 as sha256
+
+    if sys.version[:3] == "2.4":
+        # we are using an hmac that expects a .new() method.
+        class Faker:
+            def __init__(self, which):
+                self.which = which
+                self.digest_size = self.which().digest_size
+
+            def new(self, *args, **kwargs):
+                return self.which(*args, **kwargs)
+
+        sha = Faker(sha)
+        sha256 = Faker(sha256)
+
+except ImportError:
+    import sha
+    sha256 = None
+
+class HmacKeys(object):
+    """Key based Auth handler helper."""
+
+    def __init__(self, host, config, provider):
+        if provider.access_key is None or provider.secret_key is None:
+            raise boto.auth_handler.NotReadyToAuthenticate()
+        self._provider = provider
+        self._hmac = hmac.new(self._provider.secret_key, digestmod=sha)
+        if sha256:
+            self._hmac_256 = hmac.new(self._provider.secret_key, digestmod=sha256)
+        else:
+            self._hmac_256 = None
+
+    def algorithm(self):
+        if self._hmac_256:
+            return 'HmacSHA256'
+        else:
+            return 'HmacSHA1'
+
+    def sign_string(self, string_to_sign):
+        boto.log.debug('Canonical: %s' % string_to_sign)
+        if self._hmac_256:
+            hmac = self._hmac_256.copy()
+        else:
+            hmac = self._hmac.copy()
+        hmac.update(string_to_sign)
+        return base64.encodestring(hmac.digest()).strip()
+
+class HmacAuthV1Handler(AuthHandler, HmacKeys):
+    """    Implements the HMAC request signing used by S3 and GS."""
+    
+    capability = ['hmac-v1', 's3']
+    
+    def __init__(self, host, config, provider):
+        AuthHandler.__init__(self, host, config, provider)
+        HmacKeys.__init__(self, host, config, provider)
+        self._hmac_256 = None
+        
+    def add_auth(self, http_request, **kwargs):
+        headers = http_request.headers
+        method = http_request.method
+        auth_path = http_request.auth_path
+        if not headers.has_key('Date'):
+            headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT",
+                                            time.gmtime())
+
+        c_string = boto.utils.canonical_string(method, auth_path, headers,
+                                               None, self._provider)
+        b64_hmac = self.sign_string(c_string)
+        auth_hdr = self._provider.auth_header
+        headers['Authorization'] = ("%s %s:%s" %
+                                    (auth_hdr,
+                                     self._provider.access_key, b64_hmac))
+
+class HmacAuthV2Handler(AuthHandler, HmacKeys):
+    """
+    Implements the simplified HMAC authorization used by CloudFront.
+    """
+    capability = ['hmac-v2', 'cloudfront']
+    
+    def __init__(self, host, config, provider):
+        AuthHandler.__init__(self, host, config, provider)
+        HmacKeys.__init__(self, host, config, provider)
+        self._hmac_256 = None
+        
+    def add_auth(self, http_request, **kwargs):
+        headers = http_request.headers
+        if not headers.has_key('Date'):
+            headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT",
+                                            time.gmtime())
+
+        b64_hmac = self.sign_string(headers['Date'])
+        auth_hdr = self._provider.auth_header
+        headers['Authorization'] = ("%s %s:%s" %
+                                    (auth_hdr,
+                                     self._provider.access_key, b64_hmac))
+        
+class HmacAuthV3Handler(AuthHandler, HmacKeys):
+    """Implements the new Version 3 HMAC authorization used by Route53."""
+    
+    capability = ['hmac-v3', 'route53', 'ses']
+    
+    def __init__(self, host, config, provider):
+        AuthHandler.__init__(self, host, config, provider)
+        HmacKeys.__init__(self, host, config, provider)
+        
+    def add_auth(self, http_request, **kwargs):
+        headers = http_request.headers
+        if not headers.has_key('Date'):
+            headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT",
+                                            time.gmtime())
+
+        b64_hmac = self.sign_string(headers['Date'])
+        s = "AWS3-HTTPS AWSAccessKeyId=%s," % self._provider.access_key
+        s += "Algorithm=%s,Signature=%s" % (self.algorithm(), b64_hmac)
+        headers['X-Amzn-Authorization'] = s
+
+class QuerySignatureHelper(HmacKeys):
+    """Helper for Query signature based Auth handler.
+
+    Concrete sub class need to implement _calc_sigature method.
+    """
+
+    def add_auth(self, http_request, **kwargs):
+        headers = http_request.headers
+        params = http_request.params
+        params['AWSAccessKeyId'] = self._provider.access_key
+        params['SignatureVersion'] = self.SignatureVersion
+        params['Timestamp'] = boto.utils.get_ts()
+        qs, signature = self._calc_signature(
+            http_request.params, http_request.method,
+            http_request.path, http_request.host)
+        boto.log.debug('query_string: %s Signature: %s' % (qs, signature))
+        if http_request.method == 'POST':
+            headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
+            http_request.body = qs + '&Signature=' + urllib.quote(signature)
+        else:
+            http_request.body = ''
+            http_request.path = (http_request.path + '?' + qs + '&Signature=' + urllib.quote(signature))
+        # Now that query params are part of the path, clear the 'params' field
+        # in request.
+        http_request.params = {}
+
+class QuerySignatureV0AuthHandler(QuerySignatureHelper, AuthHandler):
+    """Class SQS query signature based Auth handler."""
+
+    SignatureVersion = 0
+    capability = ['sign-v0']
+
+    def _calc_signature(self, params, *args):
+        boto.log.debug('using _calc_signature_0')
+        hmac = self._hmac.copy()
+        s = params['Action'] + params['Timestamp']
+        hmac.update(s)
+        keys = params.keys()
+        keys.sort(cmp = lambda x, y: cmp(x.lower(), y.lower()))
+        pairs = []
+        for key in keys:
+            val = bot.utils.get_utf8_value(params[key])
+            pairs.append(key + '=' + urllib.quote(val))
+        qs = '&'.join(pairs)
+        return (qs, base64.b64encode(hmac.digest()))
+
+class QuerySignatureV1AuthHandler(QuerySignatureHelper, AuthHandler):
+    """
+    Provides Query Signature V1 Authentication.
+    """
+
+    SignatureVersion = 1
+    capability = ['sign-v1', 'mturk']
+
+    def _calc_signature(self, params, *args):
+        boto.log.debug('using _calc_signature_1')
+        hmac = self._hmac.copy()
+        keys = params.keys()
+        keys.sort(cmp = lambda x, y: cmp(x.lower(), y.lower()))
+        pairs = []
+        for key in keys:
+            hmac.update(key)
+            val = boto.utils.get_utf8_value(params[key])
+            hmac.update(val)
+            pairs.append(key + '=' + urllib.quote(val))
+        qs = '&'.join(pairs)
+        return (qs, base64.b64encode(hmac.digest()))
+
+class QuerySignatureV2AuthHandler(QuerySignatureHelper, AuthHandler):
+    """Provides Query Signature V2 Authentication."""
+
+    SignatureVersion = 2
+    capability = ['sign-v2', 'ec2', 'ec2', 'emr', 'fps', 'ecs',
+                  'sdb', 'iam', 'rds', 'sns', 'sqs']
+
+    def _calc_signature(self, params, verb, path, server_name):
+        boto.log.debug('using _calc_signature_2')
+        string_to_sign = '%s\n%s\n%s\n' % (verb, server_name.lower(), path)
+        if self._hmac_256:
+            hmac = self._hmac_256.copy()
+            params['SignatureMethod'] = 'HmacSHA256'
+        else:
+            hmac = self._hmac.copy()
+            params['SignatureMethod'] = 'HmacSHA1'
+        keys = params.keys()
+        keys.sort()
+        pairs = []
+        for key in keys:
+            val = boto.utils.get_utf8_value(params[key])
+            pairs.append(urllib.quote(key, safe='') + '=' +
+                         urllib.quote(val, safe='-_~'))
+        qs = '&'.join(pairs)
+        boto.log.debug('query string: %s' % qs)
+        string_to_sign += qs
+        boto.log.debug('string_to_sign: %s' % string_to_sign)
+        hmac.update(string_to_sign)
+        b64 = base64.b64encode(hmac.digest())
+        boto.log.debug('len(b64)=%d' % len(b64))
+        boto.log.debug('base64 encoded digest: %s' % b64)
+        return (qs, b64)
+
+
+def get_auth_handler(host, config, provider, requested_capability=None):
+    """Finds an AuthHandler that is ready to authenticate.
+
+    Lists through all the registered AuthHandlers to find one that is willing
+    to handle for the requested capabilities, config and provider.
+
+    :type host: string
+    :param host: The name of the host
+
+    :type config: 
+    :param config:
+
+    :type provider:
+    :param provider:
+
+    Returns:
+        An implementation of AuthHandler.
+
+    Raises:
+        boto.exception.NoAuthHandlerFound:
+        boto.exception.TooManyAuthHandlerReadyToAuthenticate:
+    """
+    ready_handlers = []
+    auth_handlers = boto.plugin.get_plugin(AuthHandler, requested_capability)
+    total_handlers = len(auth_handlers)
+    for handler in auth_handlers:
+        try:
+            ready_handlers.append(handler(host, config, provider))
+        except boto.auth_handler.NotReadyToAuthenticate:
+            pass
+ 
+    if not ready_handlers:
+        checked_handlers = auth_handlers
+        names = [handler.__name__ for handler in checked_handlers]
+        raise boto.exception.NoAuthHandlerFound(
+              'No handler was ready to authenticate. %d handlers were checked.'
+              ' %s ' % (len(names), str(names)))
+
+    if len(ready_handlers) > 1:
+        # NOTE: Even though it would be nice to accept more than one handler
+        # by using one of the many ready handlers, we are never sure that each
+        # of them are referring to the same storage account. Since we cannot
+        # easily guarantee that, it is always safe to fail, rather than operate
+        # on the wrong account.
+        names = [handler.__class__.__name__ for handler in ready_handlers]
+        raise boto.exception.TooManyAuthHandlerReadyToAuthenticate(
+               '%d AuthHandlers ready to authenticate, '
+               'only 1 expected: %s' % (len(names), str(names)))
+
+    return ready_handlers[0]
diff --git a/boto/auth_handler.py b/boto/auth_handler.py
new file mode 100644
index 0000000..ab2d317
--- /dev/null
+++ b/boto/auth_handler.py
@@ -0,0 +1,58 @@
+# Copyright 2010 Google Inc.
+#
+# 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.
+
+"""
+Defines an interface which all Auth handlers need to implement.
+"""
+
+from plugin import Plugin
+
+class NotReadyToAuthenticate(Exception):
+  pass
+
+class AuthHandler(Plugin):
+
+    capability = []
+
+    def __init__(self, host, config, provider):
+        """Constructs the handlers.
+        :type host: string
+        :param host: The host to which the request is being sent.
+
+        :type config: boto.pyami.Config 
+        :param config: Boto configuration.
+
+        :type provider: boto.provider.Provider  
+        :param provider: Provider details.
+
+        Raises:
+            NotReadyToAuthenticate: if this handler is not willing to
+                authenticate for the given provider and config.
+        """
+        pass
+
+    def add_auth(self, http_request):
+        """Invoked to add authentication details to request.
+
+        :type http_request: boto.connection.HTTPRequest
+        :param http_request: HTTP request that needs to be authenticated.
+        """
+        pass
diff --git a/boto/cloudfront/__init__.py b/boto/cloudfront/__init__.py
new file mode 100644
index 0000000..bd02b00
--- /dev/null
+++ b/boto/cloudfront/__init__.py
@@ -0,0 +1,248 @@
+# Copyright (c) 2006-2009 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 xml.sax
+import time
+import boto
+from boto.connection import AWSAuthConnection
+from boto import handler
+from boto.cloudfront.distribution import Distribution, DistributionSummary, DistributionConfig
+from boto.cloudfront.distribution import StreamingDistribution, StreamingDistributionSummary, StreamingDistributionConfig
+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-11-01'
+
+    def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
+                 port=None, proxy=None, proxy_port=None,
+                 host=DefaultHost, debug=0):
+        AWSAuthConnection.__init__(self, host,
+                aws_access_key_id, aws_secret_access_key,
+                True, port, proxy, proxy_port, debug=debug)
+
+    def get_etag(self, response):
+        response_headers = response.msg
+        for key in response_headers.keys():
+            if key.lower() == 'etag':
+                return response_headers[key]
+        return None
+
+    def _required_auth_capability(self):
+        return ['cloudfront']
+
+    # Generics
+    
+    def _get_all_objects(self, resource, tags):
+        if not tags:
+            tags=[('DistributionSummary', DistributionSummary)]
+        response = self.make_request('GET', '/%s/%s' % (self.Version, resource))
+        body = response.read()
+        boto.log.debug(body)
+        if response.status >= 300:
+            raise CloudFrontServerError(response.status, response.reason, body)
+        rs = ResultSet(tags)
+        h = handler.XmlHandler(rs, self)
+        xml.sax.parseString(body, h)
+        return rs
+
+    def _get_info(self, id, resource, dist_class):
+        uri = '/%s/%s/%s' % (self.Version, resource, id)
+        response = self.make_request('GET', uri)
+        body = response.read()
+        boto.log.debug(body)
+        if response.status >= 300:
+            raise CloudFrontServerError(response.status, response.reason, body)
+        d = dist_class(connection=self)
+        response_headers = response.msg
+        for key in response_headers.keys():
+            if key.lower() == 'etag':
+                d.etag = response_headers[key]
+        h = handler.XmlHandler(d, self)
+        xml.sax.parseString(body, h)
+        return d
+
+    def _get_config(self, id, resource, config_class):
+        uri = '/%s/%s/%s/config' % (self.Version, resource, id)
+        response = self.make_request('GET', uri)
+        body = response.read()
+        boto.log.debug(body)
+        if response.status >= 300:
+            raise CloudFrontServerError(response.status, response.reason, body)
+        d = config_class(connection=self)
+        d.etag = self.get_etag(response)
+        h = handler.XmlHandler(d, self)
+        xml.sax.parseString(body, h)
+        return d
+    
+    def _set_config(self, distribution_id, etag, config):
+        if isinstance(config, StreamingDistributionConfig):
+            resource = 'streaming-distribution'
+        else:
+            resource = 'distribution'
+        uri = '/%s/%s/%s/config' % (self.Version, resource, distribution_id)
+        headers = {'If-Match' : etag, 'Content-Type' : 'text/xml'}
+        response = self.make_request('PUT', uri, headers, config.to_xml())
+        body = response.read()
+        boto.log.debug(body)
+        if response.status != 200:
+            raise CloudFrontServerError(response.status, response.reason, body)
+        return self.get_etag(response)
+    
+    def _create_object(self, config, resource, dist_class):
+        response = self.make_request('POST', '/%s/%s' % (self.Version, resource),
+                                     {'Content-Type' : 'text/xml'}, data=config.to_xml())
+        body = response.read()
+        boto.log.debug(body)
+        if response.status == 201:
+            d = dist_class(connection=self)
+            h = handler.XmlHandler(d, self)
+            xml.sax.parseString(body, h)
+            d.etag = self.get_etag(response)
+            return d
+        else:
+            raise CloudFrontServerError(response.status, response.reason, body)
+        
+    def _delete_object(self, id, etag, resource):
+        uri = '/%s/%s/%s' % (self.Version, resource, id)
+        response = self.make_request('DELETE', uri, {'If-Match' : etag})
+        body = response.read()
+        boto.log.debug(body)
+        if response.status != 204:
+            raise CloudFrontServerError(response.status, response.reason, body)
+
+    # Distributions
+        
+    def get_all_distributions(self):
+        tags=[('DistributionSummary', DistributionSummary)]
+        return self._get_all_objects('distribution', tags)
+
+    def get_distribution_info(self, distribution_id):
+        return self._get_info(distribution_id, 'distribution', Distribution)
+
+    def get_distribution_config(self, distribution_id):
+        return self._get_config(distribution_id, 'distribution',
+                                DistributionConfig)
+    
+    def set_distribution_config(self, distribution_id, etag, config):
+        return self._set_config(distribution_id, etag, config)
+    
+    def create_distribution(self, origin, enabled, caller_reference='',
+                            cnames=None, comment=''):
+        config = DistributionConfig(origin=origin, enabled=enabled,
+                                    caller_reference=caller_reference,
+                                    cnames=cnames, comment=comment)
+        return self._create_object(config, 'distribution', Distribution)
+        
+    def delete_distribution(self, distribution_id, etag):
+        return self._delete_object(distribution_id, etag, 'distribution')
+
+    # Streaming Distributions
+        
+    def get_all_streaming_distributions(self):
+        tags=[('StreamingDistributionSummary', StreamingDistributionSummary)]
+        return self._get_all_objects('streaming-distribution', tags)
+
+    def get_streaming_distribution_info(self, distribution_id):
+        return self._get_info(distribution_id, 'streaming-distribution',
+                              StreamingDistribution)
+
+    def get_streaming_distribution_config(self, distribution_id):
+        return self._get_config(distribution_id, 'streaming-distribution',
+                                StreamingDistributionConfig)
+    
+    def set_streaming_distribution_config(self, distribution_id, etag, config):
+        return self._set_config(distribution_id, etag, config)
+    
+    def create_streaming_distribution(self, origin, enabled,
+                                      caller_reference='',
+                                      cnames=None, comment=''):
+        config = StreamingDistributionConfig(origin=origin, enabled=enabled,
+                                             caller_reference=caller_reference,
+                                             cnames=cnames, comment=comment)
+        return self._create_object(config, 'streaming-distribution',
+                                   StreamingDistribution)
+        
+    def delete_streaming_distribution(self, distribution_id, etag):
+        return self._delete_object(distribution_id, etag, 'streaming-distribution')
+
+    # Origin Access Identity
+
+    def get_all_origin_access_identity(self):
+        tags=[('CloudFrontOriginAccessIdentitySummary',
+               OriginAccessIdentitySummary)]
+        return self._get_all_objects('origin-access-identity/cloudfront', tags)
+
+    def get_origin_access_identity_info(self, access_id):
+        return self._get_info(access_id, 'origin-access-identity/cloudfront',
+                              OriginAccessIdentity)
+
+    def get_origin_access_identity_config(self, access_id):
+        return self._get_config(access_id,
+                                'origin-access-identity/cloudfront',
+                                OriginAccessIdentityConfig)
+    
+    def set_origin_access_identity_config(self, access_id,
+                                          etag, config):
+        return self._set_config(access_id, etag, config)
+    
+    def create_origin_access_identity(self, caller_reference='', comment=''):
+        config = OriginAccessIdentityConfig(caller_reference=caller_reference,
+                                            comment=comment)
+        return self._create_object(config, 'origin-access-identity/cloudfront',
+                                   OriginAccessIdentity)
+        
+    def delete_origin_access_identity(self, access_id, etag):
+        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://goo.gl/8vECq
+        """
+        # We allow you to pass in either an array or
+        # an InvalidationBatch object
+        if not isinstance(paths, InvalidationBatch):
+            paths = InvalidationBatch(paths)
+        paths.connection = self
+        uri = '/%s/distribution/%s/invalidation' % (self.Version,
+                                                    distribution_id)
+        response = self.make_request('POST', uri,
+                                     {'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/distribution.py b/boto/cloudfront/distribution.py
new file mode 100644
index 0000000..ed245cb
--- /dev/null
+++ b/boto/cloudfront/distribution.py
@@ -0,0 +1,540 @@
+# Copyright (c) 2006-2009 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 uuid
+from boto.cloudfront.identity import OriginAccessIdentity
+from boto.cloudfront.object import Object, StreamingObject
+from boto.cloudfront.signers import ActiveTrustedSigners, TrustedSigners
+from boto.cloudfront.logging import LoggingInfo
+from boto.cloudfront.origin import S3Origin, CustomOrigin
+from boto.s3.acl import ACL
+
+class DistributionConfig:
+
+    def __init__(self, connection=None, origin=None, enabled=False,
+                 caller_reference='', cnames=None, comment='',
+                 trusted_signers=None, default_root_object=None,
+                 logging=None):
+        """
+        :param origin: Origin information to associate with the
+                       distribution.  If your distribution will use
+                       an Amazon S3 origin, then this should be an
+                       S3Origin object. If your distribution will use
+                       a custom origin (non Amazon S3), then this
+                       should be a CustomOrigin object.
+        :type origin: :class:`boto.cloudfront.origin.S3Origin` or
+                      :class:`boto.cloudfront.origin.CustomOrigin`
+
+        :param enabled: Whether the distribution is enabled to accept
+                        end user requests for content.
+        :type enabled: bool
+        
+        :param caller_reference: A unique number that ensures the
+                                 request can't be replayed.  If no
+                                 caller_reference is provided, boto
+                                 will generate a type 4 UUID for use
+                                 as the caller reference.
+        :type enabled: str
+        
+        :param cnames: A CNAME alias you want to associate with this
+                       distribution. You can have up to 10 CNAME aliases
+                       per distribution.
+        :type enabled: array of str
+        
+        :param comment: Any comments you want to include about the
+                        distribution.
+        :type comment: str
+        
+        :param trusted_signers: Specifies any AWS accounts you want to
+                                permit to create signed URLs for private
+                                content. If you want the distribution to
+                                use signed URLs, this should contain a
+                                TrustedSigners object; if you want the
+                                distribution to use basic URLs, leave
+                                this None.
+        :type trusted_signers: :class`boto.cloudfront.signers.TrustedSigners`
+        
+        :param default_root_object: Designates a default root object.
+                                    Only include a DefaultRootObject value
+                                    if you are going to assign a default
+                                    root object for the distribution.
+        :type comment: str
+
+        :param logging: Controls whether access logs are written for the
+                        distribution. If you want to turn on access logs,
+                        this should contain a LoggingInfo object; otherwise
+                        it should contain None.
+        :type logging: :class`boto.cloudfront.logging.LoggingInfo`
+        
+        """
+        self.connection = connection
+        self.origin = origin
+        self.enabled = enabled
+        if caller_reference:
+            self.caller_reference = caller_reference
+        else:
+            self.caller_reference = str(uuid.uuid4())
+        self.cnames = []
+        if cnames:
+            self.cnames = cnames
+        self.comment = comment
+        self.trusted_signers = trusted_signers
+        self.logging = None
+        self.default_root_object = default_root_object
+
+    def to_xml(self):
+        s = '<?xml version="1.0" encoding="UTF-8"?>\n'
+        s += '<DistributionConfig xmlns="http://cloudfront.amazonaws.com/doc/2010-07-15/">\n'
+        if self.origin:
+            s += self.origin.to_xml()
+        s += '  <CallerReference>%s</CallerReference>\n' % self.caller_reference
+        for cname in self.cnames:
+            s += '  <CNAME>%s</CNAME>\n' % cname
+        if self.comment:
+            s += '  <Comment>%s</Comment>\n' % self.comment
+        s += '  <Enabled>'
+        if self.enabled:
+            s += 'true'
+        else:
+            s += 'false'
+        s += '</Enabled>\n'
+        if self.trusted_signers:
+            s += '<TrustedSigners>\n'
+            for signer in self.trusted_signers:
+                if signer == 'Self':
+                    s += '  <Self></Self>\n'
+                else:
+                    s += '  <AwsAccountNumber>%s</AwsAccountNumber>\n' % signer
+            s += '</TrustedSigners>\n'
+        if self.logging:
+            s += '<Logging>\n'
+            s += '  <Bucket>%s</Bucket>\n' % self.logging.bucket
+            s += '  <Prefix>%s</Prefix>\n' % self.logging.prefix
+            s += '</Logging>\n'
+        if self.default_root_object:
+            dro = self.default_root_object
+            s += '<DefaultRootObject>%s</DefaultRootObject>\n' % dro
+        s += '</DistributionConfig>\n'
+        return s
+
+    def startElement(self, name, attrs, connection):
+        if name == 'TrustedSigners':
+            self.trusted_signers = TrustedSigners()
+            return self.trusted_signers
+        elif name == 'Logging':
+            self.logging = LoggingInfo()
+            return self.logging
+        elif name == 'S3Origin':
+            self.origin = S3Origin()
+            return self.origin
+        elif name == 'CustomOrigin':
+            self.origin = CustomOrigin()
+            return self.origin
+        else:
+            return None
+
+    def endElement(self, name, value, connection):
+        if name == 'CNAME':
+            self.cnames.append(value)
+        elif name == 'Comment':
+            self.comment = value
+        elif name == 'Enabled':
+            if value.lower() == 'true':
+                self.enabled = True
+            else:
+                self.enabled = False
+        elif name == 'CallerReference':
+            self.caller_reference = value
+        elif name == 'DefaultRootObject':
+            self.default_root_object = value
+        else:
+            setattr(self, name, value)
+
+class StreamingDistributionConfig(DistributionConfig):
+
+    def __init__(self, connection=None, origin='', enabled=False,
+                 caller_reference='', cnames=None, comment='',
+                 trusted_signers=None, logging=None):
+        DistributionConfig.__init__(self, connection=connection,
+                                    origin=origin, enabled=enabled,
+                                    caller_reference=caller_reference,
+                                    cnames=cnames, comment=comment,
+                                    trusted_signers=trusted_signers,
+                                    logging=logging)
+    def to_xml(self):
+        s = '<?xml version="1.0" encoding="UTF-8"?>\n'
+        s += '<StreamingDistributionConfig xmlns="http://cloudfront.amazonaws.com/doc/2010-07-15/">\n'
+        if self.origin:
+            s += self.origin.to_xml()
+        s += '  <CallerReference>%s</CallerReference>\n' % self.caller_reference
+        for cname in self.cnames:
+            s += '  <CNAME>%s</CNAME>\n' % cname
+        if self.comment:
+            s += '  <Comment>%s</Comment>\n' % self.comment
+        s += '  <Enabled>'
+        if self.enabled:
+            s += 'true'
+        else:
+            s += 'false'
+        s += '</Enabled>\n'
+        if self.trusted_signers:
+            s += '<TrustedSigners>\n'
+            for signer in self.trusted_signers:
+                if signer == 'Self':
+                    s += '  <Self/>\n'
+                else:
+                    s += '  <AwsAccountNumber>%s</AwsAccountNumber>\n' % signer
+            s += '</TrustedSigners>\n'
+        if self.logging:
+            s += '<Logging>\n'
+            s += '  <Bucket>%s</Bucket>\n' % self.logging.bucket
+            s += '  <Prefix>%s</Prefix>\n' % self.logging.prefix
+            s += '</Logging>\n'
+        s += '</StreamingDistributionConfig>\n'
+        return s
+
+class DistributionSummary:
+
+    def __init__(self, connection=None, domain_name='', id='',
+                 last_modified_time=None, status='', origin=None,
+                 cname='', comment='', enabled=False):
+        self.connection = connection
+        self.domain_name = domain_name
+        self.id = id
+        self.last_modified_time = last_modified_time
+        self.status = status
+        self.origin = origin
+        self.enabled = enabled
+        self.cnames = []
+        if cname:
+            self.cnames.append(cname)
+        self.comment = comment
+        self.trusted_signers = None
+        self.etag = None
+        self.streaming = False
+
+    def startElement(self, name, attrs, connection):
+        if name == 'TrustedSigners':
+            self.trusted_signers = TrustedSigners()
+            return self.trusted_signers
+        elif name == 'S3Origin':
+            self.origin = S3Origin()
+            return self.origin
+        elif name == 'CustomOrigin':
+            self.origin = CustomOrigin()
+            return self.origin
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'Id':
+            self.id = value
+        elif name == 'Status':
+            self.status = value
+        elif name == 'LastModifiedTime':
+            self.last_modified_time = value
+        elif name == 'DomainName':
+            self.domain_name = value
+        elif name == 'Origin':
+            self.origin = value
+        elif name == 'CNAME':
+            self.cnames.append(value)
+        elif name == 'Comment':
+            self.comment = value
+        elif name == 'Enabled':
+            if value.lower() == 'true':
+                self.enabled = True
+            else:
+                self.enabled = False
+        elif name == 'StreamingDistributionSummary':
+            self.streaming = True
+        else:
+            setattr(self, name, value)
+
+    def get_distribution(self):
+        return self.connection.get_distribution_info(self.id)
+
+class StreamingDistributionSummary(DistributionSummary):
+
+    def get_distribution(self):
+        return self.connection.get_streaming_distribution_info(self.id)
+    
+class Distribution:
+
+    def __init__(self, connection=None, config=None, domain_name='',
+                 id='', last_modified_time=None, status=''):
+        self.connection = connection
+        self.config = config
+        self.domain_name = domain_name
+        self.id = id
+        self.last_modified_time = last_modified_time
+        self.status = status
+        self.active_signers = None
+        self.etag = None
+        self._bucket = None
+        self._object_class = Object
+
+    def startElement(self, name, attrs, connection):
+        if name == 'DistributionConfig':
+            self.config = DistributionConfig()
+            return self.config
+        elif name == 'ActiveTrustedSigners':
+            self.active_signers = ActiveTrustedSigners()
+            return self.active_signers
+        else:
+            return None
+
+    def endElement(self, name, value, connection):
+        if name == 'Id':
+            self.id = value
+        elif name == 'LastModifiedTime':
+            self.last_modified_time = value
+        elif name == 'Status':
+            self.status = value
+        elif name == 'DomainName':
+            self.domain_name = value
+        else:
+            setattr(self, name, value)
+
+    def update(self, enabled=None, cnames=None, comment=None):
+        """
+        Update the configuration of the Distribution.  The only values
+        of the DistributionConfig that can be updated are:
+
+         * CNAMES
+         * Comment
+         * Whether the Distribution is enabled or not
+
+        :type enabled: bool
+        :param enabled: Whether the Distribution is active or not.
+
+        :type cnames: list of str
+        :param cnames: The DNS CNAME's associated with this
+                        Distribution.  Maximum of 10 values.
+
+        :type comment: str or unicode
+        :param comment: The comment associated with the Distribution.
+
+        """
+        new_config = DistributionConfig(self.connection, self.config.origin,
+                                        self.config.enabled, self.config.caller_reference,
+                                        self.config.cnames, self.config.comment,
+                                        self.config.trusted_signers,
+                                        self.config.default_root_object)
+        if enabled != None:
+            new_config.enabled = enabled
+        if cnames != None:
+            new_config.cnames = cnames
+        if comment != None:
+            new_config.comment = comment
+        self.etag = self.connection.set_distribution_config(self.id, self.etag, new_config)
+        self.config = new_config
+        self._object_class = Object
+
+    def enable(self):
+        """
+        Deactivate the Distribution.  A convenience wrapper around
+        the update method.
+        """
+        self.update(enabled=True)
+
+    def disable(self):
+        """
+        Activate the Distribution.  A convenience wrapper around
+        the update method.
+        """
+        self.update(enabled=False)
+
+    def delete(self):
+        """
+        Delete this CloudFront Distribution.  The content
+        associated with the Distribution is not deleted from
+        the underlying Origin bucket in S3.
+        """
+        self.connection.delete_distribution(self.id, self.etag)
+
+    def _get_bucket(self):
+        if not self._bucket:
+            bucket_name = self.config.origin.replace('.s3.amazonaws.com', '')
+            from boto.s3.connection import S3Connection
+            s3 = S3Connection(self.connection.aws_access_key_id,
+                              self.connection.aws_secret_access_key,
+                              proxy=self.connection.proxy,
+                              proxy_port=self.connection.proxy_port,
+                              proxy_user=self.connection.proxy_user,
+                              proxy_pass=self.connection.proxy_pass)
+            self._bucket = s3.get_bucket(bucket_name)
+            self._bucket.distribution = self
+            self._bucket.set_key_class(self._object_class)
+        return self._bucket
+    
+    def get_objects(self):
+        """
+        Return a list of all content objects in this distribution.
+        
+        :rtype: list of :class:`boto.cloudfront.object.Object`
+        :return: The content objects
+        """
+        bucket = self._get_bucket()
+        objs = []
+        for key in bucket:
+            objs.append(key)
+        return objs
+
+    def set_permissions(self, object, replace=False):
+        """
+        Sets the S3 ACL grants for the given object to the appropriate
+        value based on the type of Distribution.  If the Distribution
+        is serving private content the ACL will be set to include the
+        Origin Access Identity associated with the Distribution.  If
+        the Distribution is serving public content the content will
+        be set up with "public-read".
+
+        :type object: :class:`boto.cloudfront.object.Object`
+        :param enabled: The Object whose ACL is being set
+
+        :type replace: bool
+        :param replace: If False, the Origin Access Identity will be
+                        appended to the existing ACL for the object.
+                        If True, the ACL for the object will be
+                        completely replaced with one that grants
+                        READ permission to the Origin Access Identity.
+
+        """
+        if isinstance(self.config.origin, S3Origin):
+            if self.config.origin.origin_access_identity:
+                id = self.config.origin.origin_access_identity.split('/')[-1]
+                oai = self.connection.get_origin_access_identity_info(id)
+                policy = object.get_acl()
+                if replace:
+                    policy.acl = ACL()
+                policy.acl.add_user_grant('READ', oai.s3_user_id)
+                object.set_acl(policy)
+            else:
+                object.set_canned_acl('public-read')
+
+    def set_permissions_all(self, replace=False):
+        """
+        Sets the S3 ACL grants for all objects in the Distribution
+        to the appropriate value based on the type of Distribution.
+
+        :type replace: bool
+        :param replace: If False, the Origin Access Identity will be
+                        appended to the existing ACL for the object.
+                        If True, the ACL for the object will be
+                        completely replaced with one that grants
+                        READ permission to the Origin Access Identity.
+
+        """
+        bucket = self._get_bucket()
+        for key in bucket:
+            self.set_permissions(key, replace)
+
+    def add_object(self, name, content, headers=None, replace=True):
+        """
+        Adds a new content object to the Distribution.  The content
+        for the object will be copied to a new Key in the S3 Bucket
+        and the permissions will be set appropriately for the type
+        of Distribution.
+
+        :type name: str or unicode
+        :param name: The name or key of the new object.
+
+        :type content: file-like object
+        :param content: A file-like object that contains the content
+                        for the new object.
+
+        :type headers: dict
+        :param headers: A dictionary containing additional headers
+                        you would like associated with the new
+                        object in S3.
+
+        :rtype: :class:`boto.cloudfront.object.Object`
+        :return: The newly created object.
+        """
+        if self.config.origin_access_identity:
+            policy = 'private'
+        else:
+            policy = 'public-read'
+        bucket = self._get_bucket()
+        object = bucket.new_key(name)
+        object.set_contents_from_file(content, headers=headers, policy=policy)
+        if self.config.origin_access_identity:
+            self.set_permissions(object, replace)
+        return object
+            
+class StreamingDistribution(Distribution):
+
+    def __init__(self, connection=None, config=None, domain_name='',
+                 id='', last_modified_time=None, status=''):
+        Distribution.__init__(self, connection, config, domain_name,
+                              id, last_modified_time, status)
+        self._object_class = StreamingObject
+
+    def startElement(self, name, attrs, connection):
+        if name == 'StreamingDistributionConfig':
+            self.config = StreamingDistributionConfig()
+            return self.config
+        else:
+            return Distribution.startElement(self, name, attrs, connection)
+
+    def update(self, enabled=None, cnames=None, comment=None):
+        """
+        Update the configuration of the StreamingDistribution.  The only values
+        of the StreamingDistributionConfig that can be updated are:
+
+         * CNAMES
+         * Comment
+         * Whether the Distribution is enabled or not
+
+        :type enabled: bool
+        :param enabled: Whether the StreamingDistribution is active or not.
+
+        :type cnames: list of str
+        :param cnames: The DNS CNAME's associated with this
+                        Distribution.  Maximum of 10 values.
+
+        :type comment: str or unicode
+        :param comment: The comment associated with the Distribution.
+
+        """
+        new_config = StreamingDistributionConfig(self.connection,
+                                                 self.config.origin,
+                                                 self.config.enabled,
+                                                 self.config.caller_reference,
+                                                 self.config.cnames,
+                                                 self.config.comment,
+                                                 self.config.trusted_signers)
+        if enabled != None:
+            new_config.enabled = enabled
+        if cnames != None:
+            new_config.cnames = cnames
+        if comment != None:
+            new_config.comment = comment
+        self.etag = self.connection.set_streaming_distribution_config(self.id,
+                                                                      self.etag,
+                                                                      new_config)
+        self.config = new_config
+        self._object_class = StreamingObject
+
+    def delete(self):
+        self.connection.delete_streaming_distribution(self.id, self.etag)
+            
+        
diff --git a/boto/cloudfront/exception.py b/boto/cloudfront/exception.py
new file mode 100644
index 0000000..7680642
--- /dev/null
+++ b/boto/cloudfront/exception.py
@@ -0,0 +1,26 @@
+# Copyright (c) 2006,2007 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.exception import BotoServerError
+
+class CloudFrontServerError(BotoServerError):
+
+    pass
diff --git a/boto/cloudfront/identity.py b/boto/cloudfront/identity.py
new file mode 100644
index 0000000..1571e87
--- /dev/null
+++ b/boto/cloudfront/identity.py
@@ -0,0 +1,122 @@
+# Copyright (c) 2006-2009 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 uuid
+
+class OriginAccessIdentity:
+
+    def __init__(self, connection=None, config=None, id='',
+                 s3_user_id='', comment=''):
+        self.connection = connection
+        self.config = config
+        self.id = id
+        self.s3_user_id = s3_user_id
+        self.comment = comment
+        self.etag = None
+        
+    def startElement(self, name, attrs, connection):
+        if name == 'CloudFrontOriginAccessIdentityConfig':
+            self.config = OriginAccessIdentityConfig()
+            return self.config
+        else:
+            return None
+
+    def endElement(self, name, value, connection):
+        if name == 'Id':
+            self.id = value
+        elif name == 'S3CanonicalUserId':
+            self.s3_user_id = value
+        elif name == 'Comment':
+            self.comment = value
+        else:
+            setattr(self, name, value)
+
+    def update(self, comment=None):
+        new_config = OriginAccessIdentityConfig(self.connection,
+                                                self.config.caller_reference,
+                                                self.config.comment)
+        if comment != None:
+            new_config.comment = comment
+        self.etag = self.connection.set_origin_identity_config(self.id, self.etag, new_config)
+        self.config = new_config
+
+    def delete(self):
+        return self.connection.delete_origin_access_identity(self.id, self.etag)
+
+    def uri(self):
+        return 'origin-access-identity/cloudfront/%s' % self.id
+            
+class OriginAccessIdentityConfig:
+
+    def __init__(self, connection=None, caller_reference='', comment=''):
+        self.connection = connection
+        if caller_reference:
+            self.caller_reference = caller_reference
+        else:
+            self.caller_reference = str(uuid.uuid4())
+        self.comment = comment
+
+    def to_xml(self):
+        s = '<?xml version="1.0" encoding="UTF-8"?>\n'
+        s += '<CloudFrontOriginAccessIdentityConfig xmlns="http://cloudfront.amazonaws.com/doc/2009-09-09/">\n'
+        s += '  <CallerReference>%s</CallerReference>\n' % self.caller_reference
+        if self.comment:
+            s += '  <Comment>%s</Comment>\n' % self.comment
+        s += '</CloudFrontOriginAccessIdentityConfig>\n'
+        return s
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'Comment':
+            self.comment = value
+        elif name == 'CallerReference':
+            self.caller_reference = value
+        else:
+            setattr(self, name, value)
+
+class OriginAccessIdentitySummary:
+
+    def __init__(self, connection=None, id='',
+                 s3_user_id='', comment=''):
+        self.connection = connection
+        self.id = id
+        self.s3_user_id = s3_user_id
+        self.comment = comment
+        self.etag = None
+        
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'Id':
+            self.id = value
+        elif name == 'S3CanonicalUserId':
+            self.s3_user_id = value
+        elif name == 'Comment':
+            self.comment = value
+        else:
+            setattr(self, name, value)
+
+    def get_origin_access_identity(self):
+        return self.connection.get_origin_access_identity_info(self.id)
+    
diff --git a/boto/cloudfront/invalidation.py b/boto/cloudfront/invalidation.py
new file mode 100644
index 0000000..ea13a67
--- /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 = value
+        elif name == "CreateTime":
+            self.create_time = value
+        elif name == "CallerReference":
+            self.caller_reference = value
+        return None
diff --git a/boto/cloudfront/logging.py b/boto/cloudfront/logging.py
new file mode 100644
index 0000000..6c2f4fd
--- /dev/null
+++ b/boto/cloudfront/logging.py
@@ -0,0 +1,38 @@
+# Copyright (c) 2006-2009 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.
+
+class LoggingInfo(object):
+
+    def __init__(self, bucket='', prefix=''):
+        self.bucket = bucket
+        self.prefix = prefix
+    
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'Bucket':
+            self.bucket = value
+        elif name == 'Prefix':
+            self.prefix = value
+        else:
+            setattr(self, name, value)
+            
diff --git a/boto/cloudfront/object.py b/boto/cloudfront/object.py
new file mode 100644
index 0000000..3574d13
--- /dev/null
+++ b/boto/cloudfront/object.py
@@ -0,0 +1,48 @@
+# Copyright (c) 2006-2009 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.s3.key import Key
+
+class Object(Key):
+
+    def __init__(self, bucket, name=None):
+        Key.__init__(self, bucket, name=name)
+        self.distribution = bucket.distribution
+
+    def __repr__(self):
+        return '<Object: %s/%s>' % (self.distribution.config.origin, self.name)
+
+    def url(self, scheme='http'):
+        url = '%s://' % scheme
+        url += self.distribution.domain_name
+        if scheme.lower().startswith('rtmp'):
+            url += '/cfx/st/'
+        else:
+            url += '/'
+        url += self.name
+        return url
+
+class StreamingObject(Object):
+
+    def url(self, scheme='rtmp'):
+        return Object.url(self, scheme)
+
+        
diff --git a/boto/cloudfront/origin.py b/boto/cloudfront/origin.py
new file mode 100644
index 0000000..57af846
--- /dev/null
+++ b/boto/cloudfront/origin.py
@@ -0,0 +1,150 @@
+# Copyright (c) 2006-2010 Mitch Garnaat http://garnaat.org/
+# Copyright (c) 2010, Eucalyptus Systems, Inc.
+#
+# 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 identity import OriginAccessIdentity
+
+def get_oai_value(origin_access_identity):
+    if isinstance(origin_access_identity, OriginAccessIdentity):
+        return origin_access_identity.uri()
+    else:
+        return origin_access_identity
+                
+class S3Origin(object):
+    """
+    Origin information to associate with the distribution.
+    If your distribution will use an Amazon S3 origin,
+    then you use the S3Origin element.
+    """
+
+    def __init__(self, dns_name=None, origin_access_identity=None):
+        """
+        :param dns_name: The DNS name of your Amazon S3 bucket to
+                         associate with the distribution.
+                         For example: mybucket.s3.amazonaws.com.
+        :type dns_name: str
+        
+        :param origin_access_identity: The CloudFront origin access
+                                       identity to associate with the
+                                       distribution. If you want the
+                                       distribution to serve private content,
+                                       include this element; if you want the
+                                       distribution to serve public content,
+                                       remove this element.
+        :type origin_access_identity: str
+        
+        """
+        self.dns_name = dns_name
+        self.origin_access_identity = origin_access_identity
+
+    def __repr__(self):
+        return '<S3Origin: %s>' % self.dns_name
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'DNSName':
+            self.dns_name = value
+        elif name == 'OriginAccessIdentity':
+            self.origin_access_identity = value
+        else:
+            setattr(self, name, value)
+
+    def to_xml(self):
+        s = '  <S3Origin>\n'
+        s += '    <DNSName>%s</DNSName>\n' % self.dns_name
+        if self.origin_access_identity:
+            val = get_oai_value(self.origin_access_identity)
+            s += '    <OriginAccessIdentity>%s</OriginAccessIdentity>\n' % val
+        s += '  </S3Origin>\n'
+        return s
+    
+class CustomOrigin(object):
+    """
+    Origin information to associate with the distribution.
+    If your distribution will use a non-Amazon S3 origin,
+    then you use the CustomOrigin element.
+    """
+
+    def __init__(self, dns_name=None, http_port=80, https_port=443,
+                 origin_protocol_policy=None):
+        """
+        :param dns_name: The DNS name of your Amazon S3 bucket to
+                         associate with the distribution.
+                         For example: mybucket.s3.amazonaws.com.
+        :type dns_name: str
+        
+        :param http_port: The HTTP port the custom origin listens on.
+        :type http_port: int
+        
+        :param https_port: The HTTPS port the custom origin listens on.
+        :type http_port: int
+        
+        :param origin_protocol_policy: The origin protocol policy to
+                                       apply to your origin. If you
+                                       specify http-only, CloudFront
+                                       will use HTTP only to access the origin.
+                                       If you specify match-viewer, CloudFront
+                                       will fetch from your origin using HTTP
+                                       or HTTPS, based on the protocol of the
+                                       viewer request.
+        :type origin_protocol_policy: str
+        
+        """
+        self.dns_name = dns_name
+        self.http_port = http_port
+        self.https_port = https_port
+        self.origin_protocol_policy = origin_protocol_policy
+
+    def __repr__(self):
+        return '<CustomOrigin: %s>' % self.dns_name
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'DNSName':
+            self.dns_name = value
+        elif name == 'HTTPPort':
+            try:
+                self.http_port = int(value)
+            except ValueError:
+                self.http_port = value
+        elif name == 'HTTPSPort':
+            try:
+                self.https_port = int(value)
+            except ValueError:
+                self.https_port = value
+        elif name == 'OriginProtocolPolicy':
+            self.origin_protocol_policy = value
+        else:
+            setattr(self, name, value)
+
+    def to_xml(self):
+        s = '  <CustomOrigin>\n'
+        s += '    <DNSName>%s</DNSName>\n' % self.dns_name
+        s += '    <HTTPPort>%d</HTTPPort>\n' % self.http_port
+        s += '    <HTTPSPort>%d</HTTPSPort>\n' % self.https_port
+        s += '    <OriginProtocolPolicy>%s</OriginProtocolPolicy>\n' % self.origin_protocol_policy
+        s += '  </CustomOrigin>\n'
+        return s
+    
diff --git a/boto/cloudfront/signers.py b/boto/cloudfront/signers.py
new file mode 100644
index 0000000..0b0cd50
--- /dev/null
+++ b/boto/cloudfront/signers.py
@@ -0,0 +1,60 @@
+# Copyright (c) 2006-2009 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.
+
+class Signer:
+
+    def __init__(self):
+        self.id = None
+        self.key_pair_ids = []
+        
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'Self':
+            self.id = 'Self'
+        elif name == 'AwsAccountNumber':
+            self.id = value
+        elif name == 'KeyPairId':
+            self.key_pair_ids.append(value)
+            
+class ActiveTrustedSigners(list):
+
+    def startElement(self, name, attrs, connection):
+        if name == 'Signer':
+            s = Signer()
+            self.append(s)
+            return s
+
+    def endElement(self, name, value, connection):
+        pass
+
+class TrustedSigners(list):
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'Self':
+            self.append(name)
+        elif name == 'AwsAccountNumber':
+            self.append(value)
+
diff --git a/boto/connection.py b/boto/connection.py
new file mode 100644
index 0000000..76e9ffe
--- /dev/null
+++ b/boto/connection.py
@@ -0,0 +1,637 @@
+# Copyright (c) 2006-2010 Mitch Garnaat http://garnaat.org/
+# Copyright (c) 2010 Google
+# Copyright (c) 2008 rPath, Inc.
+# Copyright (c) 2009 The Echo Nest Corporation
+# Copyright (c) 2010, Eucalyptus Systems, Inc.
+# 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.
+
+#
+# Parts of this code were copied or derived from sample code supplied by AWS.
+# The following notice applies to that code.
+#
+#  This software code is made available "AS IS" without warranties of any
+#  kind.  You may copy, display, modify and redistribute the software
+#  code either by itself or as incorporated into your code; provided that
+#  you do not remove any proprietary notices.  Your use of this software
+#  code is at your own risk and you waive any claim against Amazon
+#  Digital Services, Inc. or its affiliates with respect to your use of
+#  this software code. (c) 2006 Amazon Digital Services, Inc. or its
+#  affiliates.
+
+"""
+Handles basic connections to AWS
+"""
+
+import base64
+import errno
+import httplib
+import os
+import Queue
+import re
+import socket
+import sys
+import time
+import urllib, urlparse
+import xml.sax
+
+import auth
+import auth_handler
+import boto
+import boto.utils
+
+from boto import config, UserAgent, handler
+from boto.exception import AWSConnectionError, BotoClientError, BotoServerError
+from boto.provider import Provider
+from boto.resultset import ResultSet
+
+
+PORTS_BY_SECURITY = { True: 443, False: 80 }
+
+class ConnectionPool:
+    def __init__(self, hosts, connections_per_host):
+        self._hosts = boto.utils.LRUCache(hosts)
+        self.connections_per_host = connections_per_host
+
+    def __getitem__(self, key):
+        if key not in self._hosts:
+            self._hosts[key] = Queue.Queue(self.connections_per_host)
+        return self._hosts[key]
+
+    def __repr__(self):
+        return 'ConnectionPool:%s' % ','.join(self._hosts._dict.keys())
+
+class HTTPRequest(object):
+
+    def __init__(self, method, protocol, host, port, path, auth_path,
+                 params, headers, body):
+        """Represents an HTTP request.
+
+        :type method: string
+        :param method: The HTTP method name, 'GET', 'POST', 'PUT' etc.
+
+        :type protocol: string
+        :param protocol: The http protocol used, 'http' or 'https'. 
+
+        :type host: string
+        :param host: Host to which the request is addressed. eg. abc.com
+
+        :type port: int
+        :param port: port on which the request is being sent. Zero means unset,
+                     in which case default port will be chosen.
+
+        :type path: string 
+        :param path: URL path that is bein accessed.
+
+        :type auth_path: string 
+        :param path: The part of the URL path used when creating the
+                     authentication string.
+
+        :type params: dict
+        :param params: HTTP url query parameters, with key as name of the param,
+                       and value as value of param.
+
+        :type headers: dict
+        :param headers: HTTP headers, with key as name of the header and value
+                        as value of header.
+
+        :type body: string
+        :param body: Body of the HTTP request. If not present, will be None or
+                     empty string ('').
+        """
+        self.method = method
+        self.protocol = protocol
+        self.host = host 
+        self.port = port
+        self.path = path
+        self.auth_path = auth_path
+        self.params = params
+        self.headers = headers
+        self.body = body
+
+    def __str__(self):
+        return (('method:(%s) protocol:(%s) host(%s) port(%s) path(%s) '
+                 'params(%s) headers(%s) body(%s)') % (self.method,
+                 self.protocol, self.host, self.port, self.path, self.params,
+                 self.headers, self.body))
+
+class AWSAuthConnection(object):
+    def __init__(self, host, 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, debug=0,
+                 https_connection_factory=None, path='/', provider='aws'):
+        """
+        :type host: str
+        :param host: The host to make the connection to
+       
+        :keyword str aws_access_key_id: Your AWS Access Key ID (provided by
+            Amazon). If none is specified, the value in your 
+            ``AWS_ACCESS_KEY_ID`` environmental variable is used.
+        :keyword str aws_secret_access_key: Your AWS Secret Access Key 
+            (provided by Amazon). If none is specified, the value in your 
+            ``AWS_SECRET_ACCESS_KEY`` environmental variable is used.
+
+        :type is_secure: boolean
+        :param is_secure: Whether the connection is over SSL
+
+        :type https_connection_factory: list or tuple
+        :param https_connection_factory: A pair of an HTTP connection
+                                         factory and the exceptions to catch.
+                                         The factory should have a similar
+                                         interface to L{httplib.HTTPSConnection}.
+
+        :param str proxy: Address/hostname for a proxy server
+
+        :type proxy_port: int
+        :param proxy_port: The port to use when connecting over a proxy
+
+        :type proxy_user: str
+        :param proxy_user: The username to connect with on the proxy
+
+        :type proxy_pass: str
+        :param proxy_pass: The password to use when connection over a proxy.
+
+        :type port: int
+        :param port: The port to use to connect
+        """
+        self.num_retries = 5
+        # Override passed-in is_secure setting if value was defined in config.
+        if config.has_option('Boto', 'is_secure'):
+            is_secure = config.getboolean('Boto', 'is_secure')
+        self.is_secure = is_secure
+        self.handle_proxy(proxy, proxy_port, proxy_user, proxy_pass)
+        # define exceptions from httplib that we want to catch and retry
+        self.http_exceptions = (httplib.HTTPException, socket.error,
+                                socket.gaierror)
+        # define values in socket exceptions we don't want to catch
+        self.socket_exception_values = (errno.EINTR,)
+        if https_connection_factory is not None:
+            self.https_connection_factory = https_connection_factory[0]
+            self.http_exceptions += https_connection_factory[1]
+        else:
+            self.https_connection_factory = None
+        if (is_secure):
+            self.protocol = 'https'
+        else:
+            self.protocol = 'http'
+        self.host = host
+        self.path = path
+        if debug:
+            self.debug = debug
+        else:
+            self.debug = config.getint('Boto', 'debug', debug)
+        if port:
+            self.port = port
+        else:
+            self.port = PORTS_BY_SECURITY[is_secure]
+
+        self.provider = Provider(provider,
+                                 aws_access_key_id,
+                                 aws_secret_access_key)
+
+        # allow config file to override default host
+        if self.provider.host:
+            self.host = self.provider.host
+
+        # cache up to 20 connections per host, up to 20 hosts
+        self._pool = ConnectionPool(20, 20)
+        self._connection = (self.server_name(), self.is_secure)
+        self._last_rs = None
+        self._auth_handler = auth.get_auth_handler(
+              host, config, self.provider, self._required_auth_capability()) 
+
+    def __repr__(self):
+        return '%s:%s' % (self.__class__.__name__, self.host)
+
+    def _required_auth_capability(self):
+        return []
+
+    def _cached_name(self, host, is_secure):
+        if host is None:
+            host = self.server_name()
+        cached_name = is_secure and 'https://' or 'http://'
+        cached_name += host
+        return cached_name
+
+    def connection(self):
+        return self.get_http_connection(*self._connection)
+    connection = property(connection)
+
+    def aws_access_key_id(self):
+        return self.provider.access_key
+    aws_access_key_id = property(aws_access_key_id)
+    gs_access_key_id = aws_access_key_id
+    access_key = aws_access_key_id
+
+    def aws_secret_access_key(self):
+        return self.provider.secret_key
+    aws_secret_access_key = property(aws_secret_access_key)
+    gs_secret_access_key = aws_secret_access_key
+    secret_key = aws_secret_access_key
+
+    def get_path(self, path='/'):
+        pos = path.find('?')
+        if pos >= 0:
+            params = path[pos:]
+            path = path[:pos]
+        else:
+            params = None
+        if path[-1] == '/':
+            need_trailing = True
+        else:
+            need_trailing = False
+        path_elements = self.path.split('/')
+        path_elements.extend(path.split('/'))
+        path_elements = [p for p in path_elements if p]
+        path = '/' + '/'.join(path_elements)
+        if path[-1] != '/' and need_trailing:
+            path += '/'
+        if params:
+            path = path + params
+        return path
+
+    def server_name(self, port=None):
+        if not port:
+            port = self.port
+        if port == 80:
+            signature_host = self.host
+        else:
+            # This unfortunate little hack can be attributed to
+            # a difference in the 2.6 version of httplib.  In old
+            # versions, it would append ":443" to the hostname sent
+            # in the Host header and so we needed to make sure we
+            # did the same when calculating the V2 signature.  In 2.6
+            # (and higher!)
+            # it no longer does that.  Hence, this kludge.
+            if sys.version[:3] in ('2.6', '2.7') and port == 443:
+                signature_host = self.host
+            else:
+                signature_host = '%s:%d' % (self.host, port)
+        return signature_host
+
+    def handle_proxy(self, proxy, proxy_port, proxy_user, proxy_pass):
+        self.proxy = proxy
+        self.proxy_port = proxy_port
+        self.proxy_user = proxy_user
+        self.proxy_pass = proxy_pass
+        if os.environ.has_key('http_proxy') and not self.proxy:
+            pattern = re.compile(
+                '(?:http://)?' \
+                '(?:(?P<user>\w+):(?P<pass>.*)@)?' \
+                '(?P<host>[\w\-\.]+)' \
+                '(?::(?P<port>\d+))?'
+            )
+            match = pattern.match(os.environ['http_proxy'])
+            if match:
+                self.proxy = match.group('host')
+                self.proxy_port = match.group('port')
+                self.proxy_user = match.group('user')
+                self.proxy_pass = match.group('pass')
+        else:
+            if not self.proxy:
+                self.proxy = config.get_value('Boto', 'proxy', None)
+            if not self.proxy_port:
+                self.proxy_port = config.get_value('Boto', 'proxy_port', None)
+            if not self.proxy_user:
+                self.proxy_user = config.get_value('Boto', 'proxy_user', None)
+            if not self.proxy_pass:
+                self.proxy_pass = config.get_value('Boto', 'proxy_pass', None)
+
+        if not self.proxy_port and self.proxy:
+            print "http_proxy environment variable does not specify " \
+                "a port, using default"
+            self.proxy_port = self.port
+        self.use_proxy = (self.proxy != None)
+
+    def get_http_connection(self, host, is_secure):
+        queue = self._pool[self._cached_name(host, is_secure)]
+        try:
+            return queue.get_nowait()
+        except Queue.Empty:
+            return self.new_http_connection(host, is_secure)
+
+    def new_http_connection(self, host, is_secure):
+        if self.use_proxy:
+            host = '%s:%d' % (self.proxy, int(self.proxy_port))
+        if host is None:
+            host = self.server_name()
+        if is_secure:
+            boto.log.debug('establishing HTTPS connection')
+            if self.use_proxy:
+                connection = self.proxy_ssl()
+            elif self.https_connection_factory:
+                connection = self.https_connection_factory(host)
+            else:
+                connection = httplib.HTTPSConnection(host)
+        else:
+            boto.log.debug('establishing HTTP connection')
+            connection = httplib.HTTPConnection(host)
+        if self.debug > 1:
+            connection.set_debuglevel(self.debug)
+        # self.connection must be maintained for backwards-compatibility
+        # however, it must be dynamically pulled from the connection pool
+        # set a private variable which will enable that
+        if host.split(':')[0] == self.host and is_secure == self.is_secure:
+            self._connection = (host, is_secure)
+        return connection
+
+    def put_http_connection(self, host, is_secure, connection):
+        try:
+            self._pool[self._cached_name(host, is_secure)].put_nowait(connection)
+        except Queue.Full:
+            # gracefully fail in case of pool overflow
+            connection.close()
+
+    def proxy_ssl(self):
+        host = '%s:%d' % (self.host, self.port)
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        try:
+            sock.connect((self.proxy, int(self.proxy_port)))
+        except:
+            raise
+        sock.sendall("CONNECT %s HTTP/1.0\r\n" % host)
+        sock.sendall("User-Agent: %s\r\n" % UserAgent)
+        if self.proxy_user and self.proxy_pass:
+            for k, v in self.get_proxy_auth_header().items():
+                sock.sendall("%s: %s\r\n" % (k, v))
+        sock.sendall("\r\n")
+        resp = httplib.HTTPResponse(sock, strict=True)
+        resp.begin()
+
+        if resp.status != 200:
+            # Fake a socket error, use a code that make it obvious it hasn't
+            # been generated by the socket library
+            raise socket.error(-71,
+                               "Error talking to HTTP proxy %s:%s: %s (%s)" %
+                               (self.proxy, self.proxy_port, resp.status, resp.reason))
+
+        # We can safely close the response, it duped the original socket
+        resp.close()
+
+        h = httplib.HTTPConnection(host)
+
+        # Wrap the socket in an SSL socket
+        if hasattr(httplib, 'ssl'):
+            sslSock = httplib.ssl.SSLSocket(sock)
+        else: # Old Python, no ssl module
+            sslSock = socket.ssl(sock, None, None)
+            sslSock = httplib.FakeSocket(sock, sslSock)
+        # This is a bit unclean
+        h.sock = sslSock
+        return h
+
+    def prefix_proxy_to_path(self, path, host=None):
+        path = self.protocol + '://' + (host or self.server_name()) + path
+        return path
+
+    def get_proxy_auth_header(self):
+        auth = base64.encodestring(self.proxy_user + ':' + self.proxy_pass)
+        return {'Proxy-Authorization': 'Basic %s' % auth}
+
+    def _mexe(self, method, path, data, headers, host=None, sender=None,
+              override_num_retries=None):
+        """
+        mexe - Multi-execute inside a loop, retrying multiple times to handle
+               transient Internet errors by simply trying again.
+               Also handles redirects.
+
+        This code was inspired by the S3Utils classes posted to the boto-users
+        Google group by Larry Bates.  Thanks!
+        """
+        boto.log.debug('Method: %s' % method)
+        boto.log.debug('Path: %s' % path)
+        boto.log.debug('Data: %s' % data)
+        boto.log.debug('Headers: %s' % headers)
+        boto.log.debug('Host: %s' % host)
+        response = None
+        body = None
+        e = None
+        if override_num_retries is None:
+            num_retries = config.getint('Boto', 'num_retries', self.num_retries)
+        else:
+            num_retries = override_num_retries
+        i = 0
+        connection = self.get_http_connection(host, self.is_secure)
+        while i <= num_retries:
+            try:
+                if callable(sender):
+                    response = sender(connection, method, path, data, headers)
+                else:
+                    connection.request(method, path, data, headers)
+                    response = connection.getresponse()
+                location = response.getheader('location')
+                # -- gross hack --
+                # httplib gets confused with chunked responses to HEAD requests
+                # so I have to fake it out
+                if method == 'HEAD' and getattr(response, 'chunked', False):
+                    response.chunked = 0
+                if response.status == 500 or response.status == 503:
+                    boto.log.debug('received %d response, retrying in %d seconds' % (response.status, 2 ** i))
+                    body = response.read()
+                elif response.status == 408:
+                    body = response.read()
+                    print '-------------------------'
+                    print '         4 0 8           '
+                    print 'path=%s' % path
+                    print body
+                    print '-------------------------'
+                elif response.status < 300 or response.status >= 400 or \
+                        not location:
+                    self.put_http_connection(host, self.is_secure, connection)
+                    return response
+                else:
+                    scheme, host, path, params, query, fragment = \
+                            urlparse.urlparse(location)
+                    if query:
+                        path += '?' + query
+                    boto.log.debug('Redirecting: %s' % scheme + '://' + host + path)
+                    connection = self.get_http_connection(host, scheme == 'https')
+                    continue
+            except KeyboardInterrupt:
+                sys.exit('Keyboard Interrupt')
+            except self.http_exceptions, e:
+                boto.log.debug('encountered %s exception, reconnecting' % \
+                                  e.__class__.__name__)
+                connection = self.new_http_connection(host, self.is_secure)
+            time.sleep(2 ** i)
+            i += 1
+        # If we made it here, it's because we have exhausted our retries and stil haven't
+        # succeeded.  So, if we have a response object, use it to raise an exception.
+        # Otherwise, raise the exception that must have already happened.
+        if response:
+            raise BotoServerError(response.status, response.reason, body)
+        elif e:
+            raise e
+        else:
+            raise BotoClientError('Please report this exception as a Boto Issue!')
+
+    def build_base_http_request(self, method, path, auth_path,
+                                params=None, headers=None, data='', host=None):
+        path = self.get_path(path)
+        if auth_path is not None:
+            auth_path = self.get_path(auth_path)
+        if params == None:
+            params = {}
+        else:
+            params = params.copy()
+        if headers == None:
+            headers = {}
+        else:
+            headers = headers.copy()
+        host = host or self.host
+        if self.use_proxy:
+            path = self.prefix_proxy_to_path(path, host)
+            if self.proxy_user and self.proxy_pass and not self.is_secure:
+                # If is_secure, we don't have to set the proxy authentication
+                # header here, we did that in the CONNECT to the proxy.
+                headers.update(self.get_proxy_auth_header())
+        return HTTPRequest(method, self.protocol, host, self.port,
+                           path, auth_path, params, headers, data)
+
+    def fill_in_auth(self, http_request, **kwargs):
+        headers = http_request.headers
+        for key in headers:
+            val = headers[key]
+            if isinstance(val, unicode):
+                headers[key] = urllib.quote_plus(val.encode('utf-8'))
+
+        self._auth_handler.add_auth(http_request, **kwargs)
+
+        headers['User-Agent'] = UserAgent
+        if not headers.has_key('Content-Length'):
+            headers['Content-Length'] = str(len(http_request.body))
+        return http_request
+
+    def _send_http_request(self, http_request, sender=None,
+                           override_num_retries=None):
+        return self._mexe(http_request.method, http_request.path,
+                          http_request.body, http_request.headers,
+                          http_request.host, sender, override_num_retries)
+
+    def make_request(self, method, path, headers=None, data='', host=None,
+                     auth_path=None, sender=None, override_num_retries=None):
+        """Makes a request to the server, with stock multiple-retry logic."""
+        http_request = self.build_base_http_request(method, path, auth_path,
+                                                    {}, headers, data, host)
+        http_request = self.fill_in_auth(http_request)
+        return self._send_http_request(http_request, sender,
+                                       override_num_retries)
+
+    def close(self):
+        """(Optional) Close any open HTTP connections.  This is non-destructive,
+        and making a new request will open a connection again."""
+
+        boto.log.debug('closing all HTTP connections')
+        self.connection = None  # compat field
+
+class AWSQueryConnection(AWSAuthConnection):
+
+    APIVersion = ''
+    ResponseError = BotoServerError
+
+    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=None, debug=0,
+                 https_connection_factory=None, path='/'):
+        AWSAuthConnection.__init__(self, host, aws_access_key_id, aws_secret_access_key,
+                                   is_secure, port, proxy, proxy_port, proxy_user, proxy_pass,
+                                   debug, https_connection_factory, path)
+
+    def _required_auth_capability(self):
+        return []
+
+    def get_utf8_value(self, value):
+        return boto.utils.get_utf8_value(value)
+
+    def make_request(self, action, params=None, path='/', verb='GET'):
+        http_request = self.build_base_http_request(verb, path, None,
+                                                    params, {}, '',
+                                                    self.server_name())
+        if action:
+            http_request.params['Action'] = action
+        http_request.params['Version'] = self.APIVersion
+        http_request = self.fill_in_auth(http_request)
+        return self._send_http_request(http_request)
+
+    def build_list_params(self, params, items, label):
+        if isinstance(items, str):
+            items = [items]
+        for i in range(1, len(items) + 1):
+            params['%s.%d' % (label, i)] = items[i - 1]
+
+    # generics
+
+    def get_list(self, action, params, markers, path='/', parent=None, verb='GET'):
+        if not parent:
+            parent = self
+        response = self.make_request(action, params, path, verb)
+        body = response.read()
+        boto.log.debug(body)
+        if not body:
+            boto.log.error('Null body %s' % body)
+            raise self.ResponseError(response.status, response.reason, body)
+        elif response.status == 200:
+            rs = ResultSet(markers)
+            h = handler.XmlHandler(rs, parent)
+            xml.sax.parseString(body, h)
+            return rs
+        else:
+            boto.log.error('%s %s' % (response.status, response.reason))
+            boto.log.error('%s' % body)
+            raise self.ResponseError(response.status, response.reason, body)
+
+    def get_object(self, action, params, cls, path='/', parent=None, verb='GET'):
+        if not parent:
+            parent = self
+        response = self.make_request(action, params, path, verb)
+        body = response.read()
+        boto.log.debug(body)
+        if not body:
+            boto.log.error('Null body %s' % body)
+            raise self.ResponseError(response.status, response.reason, body)
+        elif response.status == 200:
+            obj = cls(parent)
+            h = handler.XmlHandler(obj, parent)
+            xml.sax.parseString(body, h)
+            return obj
+        else:
+            boto.log.error('%s %s' % (response.status, response.reason))
+            boto.log.error('%s' % body)
+            raise self.ResponseError(response.status, response.reason, body)
+
+    def get_status(self, action, params, path='/', parent=None, verb='GET'):
+        if not parent:
+            parent = self
+        response = self.make_request(action, params, path, verb)
+        body = response.read()
+        boto.log.debug(body)
+        if not body:
+            boto.log.error('Null body %s' % body)
+            raise self.ResponseError(response.status, response.reason, body)
+        elif response.status == 200:
+            rs = ResultSet()
+            h = handler.XmlHandler(rs, parent)
+            xml.sax.parseString(body, h)
+            return rs.status
+        else:
+            boto.log.error('%s %s' % (response.status, response.reason))
+            boto.log.error('%s' % body)
+            raise self.ResponseError(response.status, response.reason, body)
diff --git a/boto/contrib/__init__.py b/boto/contrib/__init__.py
new file mode 100644
index 0000000..303dbb6
--- /dev/null
+++ b/boto/contrib/__init__.py
@@ -0,0 +1,22 @@
+# Copyright (c) 2006,2007 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.
+#
+
diff --git a/boto/contrib/m2helpers.py b/boto/contrib/m2helpers.py
new file mode 100644
index 0000000..82d2730
--- /dev/null
+++ b/boto/contrib/m2helpers.py
@@ -0,0 +1,52 @@
+# Copyright (c) 2006,2007 Jon Colverson
+#
+# 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.
+
+"""
+This module was contributed by Jon Colverson.  It provides a couple of helper
+functions that allow you to use M2Crypto's implementation of HTTPSConnection
+rather than the default version in httplib.py.  The main benefit is that
+M2Crypto's version verifies the certificate of the server.
+
+To use this feature, do something like this:
+
+from boto.ec2.connection import EC2Connection
+
+ec2 = EC2Connection(ACCESS_KEY_ID, SECRET_ACCESS_KEY,
+    https_connection_factory=https_connection_factory(cafile=CA_FILE))
+
+See http://code.google.com/p/boto/issues/detail?id=57 for more details.
+"""
+from M2Crypto import SSL
+from M2Crypto.httpslib import HTTPSConnection
+
+def secure_context(cafile=None, capath=None):
+    ctx = SSL.Context()
+    ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, depth=9)
+    if ctx.load_verify_locations(cafile=cafile, capath=capath) != 1:
+        raise Exception("Couldn't load certificates")
+    return ctx
+
+def https_connection_factory(cafile=None, capath=None):
+    def factory(*args, **kwargs):
+        return HTTPSConnection(
+            ssl_context=secure_context(cafile=cafile, capath=capath),
+                *args, **kwargs)
+    return (factory, (SSL.SSLError,))
diff --git a/boto/contrib/ymlmessage.py b/boto/contrib/ymlmessage.py
new file mode 100644
index 0000000..b9a2c93
--- /dev/null
+++ b/boto/contrib/ymlmessage.py
@@ -0,0 +1,52 @@
+# Copyright (c) 2006,2007 Chris Moyer
+#
+# 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.
+
+"""
+This module was contributed by Chris Moyer.  It provides a subclass of the
+SQS Message class that supports YAML as the body of the message.
+
+This module requires the yaml module.
+"""
+from boto.sqs.message import Message
+import yaml
+
+class YAMLMessage(Message):
+    """
+    The YAMLMessage class provides a YAML compatible message. Encoding and
+    decoding are handled automaticaly.
+
+    Access this message data like such:
+
+    m.data = [ 1, 2, 3]
+    m.data[0] # Returns 1
+
+    This depends on the PyYAML package
+    """
+
+    def __init__(self, queue=None, body='', xml_attrs=None):
+        self.data = None
+        Message.__init__(self, queue, body)
+
+    def set_body(self, body):
+        self.data = yaml.load(body)
+
+    def get_body(self):
+        return yaml.dump(self.data)
diff --git a/boto/ec2/__init__.py b/boto/ec2/__init__.py
new file mode 100644
index 0000000..8bb3f53
--- /dev/null
+++ b/boto/ec2/__init__.py
@@ -0,0 +1,52 @@
+# Copyright (c) 2006-2008 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.
+#
+"""
+This module provides an interface to the Elastic Compute Cloud (EC2)
+service from AWS.
+"""
+from boto.ec2.connection import EC2Connection
+
+def regions(**kw_params):
+    """
+    Get all available regions for the EC2 service.
+    You may pass any of the arguments accepted by the EC2Connection
+    object's constructor as keyword arguments and they will be
+    passed along to the EC2Connection object.
+        
+    :rtype: list
+    :return: A list of :class:`boto.ec2.regioninfo.RegionInfo`
+    """
+    c = EC2Connection(**kw_params)
+    return c.get_all_regions()
+
+def connect_to_region(region_name, **kw_params):
+    for region in regions(**kw_params):
+        if region.name == region_name:
+            return region.connect(**kw_params)
+    return None
+    
+def get_region(region_name, **kw_params):
+    for region in regions(**kw_params):
+        if region.name == region_name:
+            return region
+    return None
+    
diff --git a/boto/ec2/address.py b/boto/ec2/address.py
new file mode 100644
index 0000000..60ed406
--- /dev/null
+++ b/boto/ec2/address.py
@@ -0,0 +1,58 @@
+# Copyright (c) 2006-2009 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.
+
+"""
+Represents an EC2 Elastic IP Address
+"""
+
+from boto.ec2.ec2object import EC2Object
+
+class Address(EC2Object):
+    
+    def __init__(self, connection=None, public_ip=None, instance_id=None):
+        EC2Object.__init__(self, connection)
+        self.connection = connection
+        self.public_ip = public_ip
+        self.instance_id = instance_id
+
+    def __repr__(self):
+        return 'Address:%s' % self.public_ip
+
+    def endElement(self, name, value, connection):
+        if name == 'publicIp':
+            self.public_ip = value
+        elif name == 'instanceId':
+            self.instance_id = value
+        else:
+            setattr(self, name, value)
+
+    def release(self):
+        return self.connection.release_address(self.public_ip)
+
+    delete = release
+
+    def associate(self, instance_id):
+        return self.connection.associate_address(instance_id, self.public_ip)
+
+    def disassociate(self):
+        return self.connection.disassociate_address(self.public_ip)
+
+    
diff --git a/boto/ec2/autoscale/__init__.py b/boto/ec2/autoscale/__init__.py
new file mode 100644
index 0000000..5d68b32
--- /dev/null
+++ b/boto/ec2/autoscale/__init__.py
@@ -0,0 +1,244 @@
+# Copyright (c) 2009 Reza Lotun http://reza.lotun.name/
+#
+# 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.
+
+"""
+This module provides an interface to the Elastic Compute Cloud (EC2)
+Auto Scaling service.
+"""
+
+import boto
+from boto.connection import AWSQueryConnection
+from boto.ec2.regioninfo import RegionInfo
+from boto.ec2.autoscale.request import Request
+from boto.ec2.autoscale.trigger import Trigger
+from boto.ec2.autoscale.launchconfig import LaunchConfiguration
+from boto.ec2.autoscale.group import AutoScalingGroup
+from boto.ec2.autoscale.activity import Activity
+
+
+class AutoScaleConnection(AWSQueryConnection):
+    APIVersion = boto.config.get('Boto', 'autoscale_version', '2009-05-15')
+    Endpoint = boto.config.get('Boto', 'autoscale_endpoint',
+                               'autoscaling.amazonaws.com')
+    DefaultRegionName = 'us-east-1'
+    DefaultRegionEndpoint = 'autoscaling.amazonaws.com'
+
+    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, debug=1,
+                 https_connection_factory=None, region=None, path='/'):
+        """
+        Init method to create a new connection to the AutoScaling service.
+
+        B{Note:} The host argument is overridden by the host specified in the
+                 boto configuration file.
+        """
+        if not region:
+            region = RegionInfo(self, self.DefaultRegionName,
+                                self.DefaultRegionEndpoint,
+                                AutoScaleConnection)
+        self.region = region
+        AWSQueryConnection.__init__(self, aws_access_key_id,
+                                    aws_secret_access_key,
+                                    is_secure, port, proxy, proxy_port,
+                                    proxy_user, proxy_pass,
+                                    self.region.endpoint, debug,
+                                    https_connection_factory, path=path)
+
+    def _required_auth_capability(self):
+        return ['ec2']
+
+    def build_list_params(self, params, items, label):
+        """ items is a list of dictionaries or strings:
+                [{'Protocol' : 'HTTP',
+                 'LoadBalancerPort' : '80',
+                 'InstancePort' : '80'},..] etc.
+             or
+                ['us-east-1b',...]
+        """
+        # different from EC2 list params
+        for i in xrange(1, len(items)+1):
+            if isinstance(items[i-1], dict):
+                for k, v in items[i-1].iteritems():
+                    params['%s.member.%d.%s' % (label, i, k)] = v
+            elif isinstance(items[i-1], basestring):
+                params['%s.member.%d' % (label, i)] = items[i-1]
+
+    def _update_group(self, op, as_group):
+        params = {
+                  'AutoScalingGroupName'    : as_group.name,
+                  'Cooldown'                : as_group.cooldown,
+                  'LaunchConfigurationName' : as_group.launch_config_name,
+                  'MinSize'                 : as_group.min_size,
+                  'MaxSize'                 : as_group.max_size,
+                  }
+        if op.startswith('Create'):
+            if as_group.availability_zones:
+                zones = as_group.availability_zones
+            else:
+                zones = [as_group.availability_zone]
+            self.build_list_params(params, as_group.load_balancers,
+                                   'LoadBalancerNames')
+            self.build_list_params(params, zones,
+                                    'AvailabilityZones')
+        return self.get_object(op, params, Request)
+
+    def create_auto_scaling_group(self, as_group):
+        """
+        Create auto scaling group.
+        """
+        return self._update_group('CreateAutoScalingGroup', as_group)
+
+    def create_launch_configuration(self, launch_config):
+        """
+        Creates a new Launch Configuration.
+
+        :type launch_config: boto.ec2.autoscale.launchconfig.LaunchConfiguration
+        :param launch_config: LaunchConfiguraiton object.
+
+        """
+        params = {
+                  'ImageId'                 : launch_config.image_id,
+                  'KeyName'                 : launch_config.key_name,
+                  'LaunchConfigurationName' : launch_config.name,
+                  'InstanceType'            : launch_config.instance_type,
+                 }
+        if launch_config.user_data:
+            params['UserData'] = launch_config.user_data
+        if launch_config.kernel_id:
+            params['KernelId'] = launch_config.kernel_id
+        if launch_config.ramdisk_id:
+            params['RamdiskId'] = launch_config.ramdisk_id
+        if launch_config.block_device_mappings:
+            self.build_list_params(params, launch_config.block_device_mappings,
+                                   'BlockDeviceMappings')
+        self.build_list_params(params, launch_config.security_groups,
+                               'SecurityGroups')
+        return self.get_object('CreateLaunchConfiguration', params,
+                                  Request, verb='POST')
+
+    def create_trigger(self, trigger):
+        """
+
+        """
+        params = {'TriggerName'                 : trigger.name,
+                  'AutoScalingGroupName'        : trigger.autoscale_group.name,
+                  'MeasureName'                 : trigger.measure_name,
+                  'Statistic'                   : trigger.statistic,
+                  'Period'                      : trigger.period,
+                  'Unit'                        : trigger.unit,
+                  'LowerThreshold'              : trigger.lower_threshold,
+                  'LowerBreachScaleIncrement'   : trigger.lower_breach_scale_increment,
+                  'UpperThreshold'              : trigger.upper_threshold,
+                  'UpperBreachScaleIncrement'   : trigger.upper_breach_scale_increment,
+                  'BreachDuration'              : trigger.breach_duration}
+        # dimensions should be a list of tuples
+        dimensions = []
+        for dim in trigger.dimensions:
+            name, value = dim
+            dimensions.append(dict(Name=name, Value=value))
+        self.build_list_params(params, dimensions, 'Dimensions')
+
+        req = self.get_object('CreateOrUpdateScalingTrigger', params,
+                               Request)
+        return req
+
+    def get_all_groups(self, names=None):
+        """
+        """
+        params = {}
+        if names:
+            self.build_list_params(params, names, 'AutoScalingGroupNames')
+        return self.get_list('DescribeAutoScalingGroups', params,
+                             [('member', AutoScalingGroup)])
+
+    def get_all_launch_configurations(self, names=None):
+        """
+        """
+        params = {}
+        if names:
+            self.build_list_params(params, names, 'LaunchConfigurationNames')
+        return self.get_list('DescribeLaunchConfigurations', params,
+                             [('member', LaunchConfiguration)])
+
+    def get_all_activities(self, autoscale_group,
+                           activity_ids=None,
+                           max_records=100):
+        """
+        Get all activities for the given autoscaling group.
+
+        :type autoscale_group: str or AutoScalingGroup object
+        :param autoscale_group: The auto scaling group to get activities on.
+
+        @max_records: int
+        :param max_records: Maximum amount of activities to return.
+        """
+        name = autoscale_group
+        if isinstance(autoscale_group, AutoScalingGroup):
+            name = autoscale_group.name
+        params = {'AutoScalingGroupName' : name}
+        if activity_ids:
+            self.build_list_params(params, activity_ids, 'ActivityIds')
+        return self.get_list('DescribeScalingActivities', params,
+                             [('member', Activity)])
+
+    def get_all_triggers(self, autoscale_group):
+        params = {'AutoScalingGroupName' : autoscale_group}
+        return self.get_list('DescribeTriggers', params,
+                             [('member', Trigger)])
+
+    def terminate_instance(self, instance_id, decrement_capacity=True):
+        params = {
+                  'InstanceId' : instance_id,
+                  'ShouldDecrementDesiredCapacity' : decrement_capacity
+                  }
+        return self.get_object('TerminateInstanceInAutoScalingGroup', params,
+                               Activity)
+
+    def set_instance_health(self, instance_id, health_status,
+                            should_respect_grace_period=True):
+        """
+        Explicitly set the health status of an instance.
+
+        :type instance_id: str
+        :param instance_id: The identifier of the EC2 instance.
+
+        :type health_status: str
+        :param health_status: The health status of the instance.
+                              "Healthy" means that the instance is
+                              healthy and should remain in service.
+                              "Unhealthy" means that the instance is
+                              unhealthy. Auto Scaling should terminate
+                              and replace it.
+
+        :type should_respect_grace_period: bool
+        :param should_respect_grace_period: If True, this call should
+                                            respect the grace period
+                                            associated with the group.
+        """
+        params = {'InstanceId' : instance_id,
+                  'HealthStatus' : health_status}
+        if should_respect_grace_period:
+            params['ShouldRespectGracePeriod'] = 'true'
+        else:
+            params['ShouldRespectGracePeriod'] = 'false'
+        return self.get_status('SetInstanceHealth', params)
+
diff --git a/boto/ec2/autoscale/activity.py b/boto/ec2/autoscale/activity.py
new file mode 100644
index 0000000..f895d65
--- /dev/null
+++ b/boto/ec2/autoscale/activity.py
@@ -0,0 +1,55 @@
+# Copyright (c) 2009 Reza Lotun http://reza.lotun.name/
+#
+# 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.
+
+
+class Activity(object):
+    def __init__(self, connection=None):
+        self.connection = connection
+        self.start_time = None
+        self.activity_id = None
+        self.progress = None
+        self.status_code = None
+        self.cause = None
+        self.description = None
+
+    def __repr__(self):
+        return 'Activity:%s status:%s progress:%s' % (self.description,
+                                                      self.status_code,
+                                                      self.progress)
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'ActivityId':
+            self.activity_id = value
+        elif name == 'StartTime':
+            self.start_time = value
+        elif name == 'Progress':
+            self.progress = value
+        elif name == 'Cause':
+            self.cause = value
+        elif name == 'Description':
+            self.description = value
+        elif name == 'StatusCode':
+            self.status_code = value
+        else:
+            setattr(self, name, value)
+
diff --git a/boto/ec2/autoscale/group.py b/boto/ec2/autoscale/group.py
new file mode 100644
index 0000000..3fa6d68
--- /dev/null
+++ b/boto/ec2/autoscale/group.py
@@ -0,0 +1,189 @@
+# Copyright (c) 2009 Reza Lotun http://reza.lotun.name/
+#
+# 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 weakref
+
+from boto.ec2.elb.listelement import ListElement
+from boto.resultset import ResultSet
+from boto.ec2.autoscale.trigger import Trigger
+from boto.ec2.autoscale.request import Request
+
+class Instance(object):
+    def __init__(self, connection=None):
+        self.connection = connection
+        self.instance_id = ''
+
+    def __repr__(self):
+        return 'Instance:%s' % self.instance_id
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'InstanceId':
+            self.instance_id = value
+        else:
+            setattr(self, name, value)
+
+
+class AutoScalingGroup(object):
+    def __init__(self, connection=None, group_name=None,
+                 availability_zone=None, launch_config=None,
+                 availability_zones=None,
+                 load_balancers=None, cooldown=0,
+                 min_size=None, max_size=None):
+        """
+        Creates a new AutoScalingGroup with the specified name.
+
+        You must not have already used up your entire quota of
+        AutoScalingGroups in order for this call to be successful. Once the
+        creation request is completed, the AutoScalingGroup is ready to be
+        used in other calls.
+
+        :type name: str
+        :param name: Name of autoscaling group.
+
+        :type availability_zone: str
+        :param availability_zone: An availability zone. DEPRECATED - use the
+                                  availability_zones parameter, which expects
+                                  a list of availability zone
+                                  strings
+
+        :type availability_zone: list
+        :param availability_zone: List of availability zones.
+
+        :type launch_config: str
+        :param launch_config: Name of launch configuration name.
+
+        :type load_balancers: list
+        :param load_balancers: List of load balancers.
+
+        :type minsize: int
+        :param minsize: Minimum size of group
+
+        :type maxsize: int
+        :param maxsize: Maximum size of group
+
+        :type cooldown: int
+        :param cooldown: Amount of time after a Scaling Activity completes
+                         before any further scaling activities can start.
+
+        :rtype: tuple
+        :return: Updated healthcheck for the instances.
+        """
+        self.name = group_name
+        self.connection = connection
+        self.min_size = min_size
+        self.max_size = max_size
+        self.created_time = None
+        self.cooldown = cooldown
+        self.launch_config = launch_config
+        if self.launch_config:
+            self.launch_config_name = self.launch_config.name
+        else:
+            self.launch_config_name = None
+        self.desired_capacity = None
+        lbs = load_balancers or []
+        self.load_balancers = ListElement(lbs)
+        zones = availability_zones or []
+        self.availability_zone = availability_zone
+        self.availability_zones = ListElement(zones)
+        self.instances = None
+
+    def __repr__(self):
+        return 'AutoScalingGroup:%s' % self.name
+
+    def startElement(self, name, attrs, connection):
+        if name == 'Instances':
+            self.instances = ResultSet([('member', Instance)])
+            return self.instances
+        elif name == 'LoadBalancerNames':
+            return self.load_balancers
+        elif name == 'AvailabilityZones':
+            return self.availability_zones
+        else:
+            return
+
+    def endElement(self, name, value, connection):
+        if name == 'MinSize':
+            self.min_size = value
+        elif name == 'CreatedTime':
+            self.created_time = value
+        elif name == 'Cooldown':
+            self.cooldown = value
+        elif name == 'LaunchConfigurationName':
+            self.launch_config_name = value
+        elif name == 'DesiredCapacity':
+            self.desired_capacity = value
+        elif name == 'MaxSize':
+            self.max_size = value
+        elif name == 'AutoScalingGroupName':
+            self.name = value
+        else:
+            setattr(self, name, value)
+
+    def set_capacity(self, capacity):
+        """ Set the desired capacity for the group. """
+        params = {
+                  'AutoScalingGroupName' : self.name,
+                  'DesiredCapacity'      : capacity,
+                 }
+        req = self.connection.get_object('SetDesiredCapacity', params,
+                                            Request)
+        self.connection.last_request = req
+        return req
+
+    def update(self):
+        """ Sync local changes with AutoScaling group. """
+        return self.connection._update_group('UpdateAutoScalingGroup', self)
+
+    def shutdown_instances(self):
+        """ Convenience method which shuts down all instances associated with
+        this group.
+        """
+        self.min_size = 0
+        self.max_size = 0
+        self.update()
+
+    def get_all_triggers(self):
+        """ Get all triggers for this auto scaling group. """
+        params = {'AutoScalingGroupName' : self.name}
+        triggers = self.connection.get_list('DescribeTriggers', params,
+                                            [('member', Trigger)])
+
+        # allow triggers to be able to access the autoscale group
+        for tr in triggers:
+            tr.autoscale_group = weakref.proxy(self)
+
+        return triggers
+
+    def delete(self):
+        """ Delete this auto-scaling group. """
+        params = {'AutoScalingGroupName' : self.name}
+        return self.connection.get_object('DeleteAutoScalingGroup', params,
+                                          Request)
+
+    def get_activities(self, activity_ids=None, max_records=100):
+        """
+        Get all activies for this group.
+        """
+        return self.connection.get_all_activities(self, activity_ids, max_records)
+
diff --git a/boto/ec2/autoscale/instance.py b/boto/ec2/autoscale/instance.py
new file mode 100644
index 0000000..ffdd5b1
--- /dev/null
+++ b/boto/ec2/autoscale/instance.py
@@ -0,0 +1,46 @@
+# Copyright (c) 2009 Reza Lotun http://reza.lotun.name/
+#
+# 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.
+
+
+class Instance(object):
+    def __init__(self, connection=None):
+        self.connection = connection
+        self.instance_id = ''
+        self.lifecycle_state = None
+        self.availability_zone = ''
+
+    def __repr__(self):
+        return 'Instance:%s' % self.instance_id
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'InstanceId':
+            self.instance_id = value
+        elif name == 'LifecycleState':
+            self.lifecycle_state = value
+        elif name == 'AvailabilityZone':
+            self.availability_zone = value
+        else:
+            setattr(self, name, value)
+
+
diff --git a/boto/ec2/autoscale/launchconfig.py b/boto/ec2/autoscale/launchconfig.py
new file mode 100644
index 0000000..7587cb6
--- /dev/null
+++ b/boto/ec2/autoscale/launchconfig.py
@@ -0,0 +1,98 @@
+# Copyright (c) 2009 Reza Lotun http://reza.lotun.name/
+#
+# 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.ec2.autoscale.request import Request
+from boto.ec2.elb.listelement import ListElement
+
+
+class LaunchConfiguration(object):
+    def __init__(self, connection=None, name=None, image_id=None,
+                 key_name=None, security_groups=None, user_data=None,
+                 instance_type='m1.small', kernel_id=None,
+                 ramdisk_id=None, block_device_mappings=None):
+        """
+        A launch configuration.
+
+        :type name: str
+        :param name: Name of the launch configuration to create.
+
+        :type image_id: str
+        :param image_id: Unique ID of the Amazon Machine Image (AMI) which was
+                         assigned during registration.
+
+        :type key_name: str
+        :param key_name: The name of the EC2 key pair.
+
+        :type security_groups: list
+        :param security_groups: Names of the security groups with which to
+                                associate the EC2 instances.
+
+        """
+        self.connection = connection
+        self.name = name
+        self.instance_type = instance_type
+        self.block_device_mappings = block_device_mappings
+        self.key_name = key_name
+        sec_groups = security_groups or []
+        self.security_groups = ListElement(sec_groups)
+        self.image_id = image_id
+        self.ramdisk_id = ramdisk_id
+        self.created_time = None
+        self.kernel_id = kernel_id
+        self.user_data = user_data
+        self.created_time = None
+
+    def __repr__(self):
+        return 'LaunchConfiguration:%s' % self.name
+
+    def startElement(self, name, attrs, connection):
+        if name == 'SecurityGroups':
+            return self.security_groups
+        else:
+            return
+
+    def endElement(self, name, value, connection):
+        if name == 'InstanceType':
+            self.instance_type = value
+        elif name == 'LaunchConfigurationName':
+            self.name = value
+        elif name == 'KeyName':
+            self.key_name = value
+        elif name == 'ImageId':
+            self.image_id = value
+        elif name == 'CreatedTime':
+            self.created_time = value
+        elif name == 'KernelId':
+            self.kernel_id = value
+        elif name == 'RamdiskId':
+            self.ramdisk_id = value
+        elif name == 'UserData':
+            self.user_data = value
+        else:
+            setattr(self, name, value)
+
+    def delete(self):
+        """ Delete this launch configuration. """
+        params = {'LaunchConfigurationName' : self.name}
+        return self.connection.get_object('DeleteLaunchConfiguration', params,
+                                          Request)
+
diff --git a/boto/ec2/autoscale/request.py b/boto/ec2/autoscale/request.py
new file mode 100644
index 0000000..c066dff
--- /dev/null
+++ b/boto/ec2/autoscale/request.py
@@ -0,0 +1,38 @@
+# Copyright (c) 2009 Reza Lotun http://reza.lotun.name/
+#
+# 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.
+
+class Request(object):
+    def __init__(self, connection=None):
+        self.connection = connection
+        self.request_id = ''
+
+    def __repr__(self):
+        return 'Request:%s' % self.request_id
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'RequestId':
+            self.request_id = value
+        else:
+            setattr(self, name, value)
+
diff --git a/boto/ec2/autoscale/trigger.py b/boto/ec2/autoscale/trigger.py
new file mode 100644
index 0000000..2840e67
--- /dev/null
+++ b/boto/ec2/autoscale/trigger.py
@@ -0,0 +1,134 @@
+# Copyright (c) 2009 Reza Lotun http://reza.lotun.name/
+#
+# 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 weakref
+
+from boto.ec2.autoscale.request import Request
+
+
+class Trigger(object):
+    """
+    An auto scaling trigger.
+    """
+
+    def __init__(self, connection=None, name=None, autoscale_group=None,
+                 dimensions=None, measure_name=None,
+                 statistic=None, unit=None, period=60,
+                 lower_threshold=None,
+                 lower_breach_scale_increment=None,
+                 upper_threshold=None,
+                 upper_breach_scale_increment=None,
+                 breach_duration=None):
+        """
+        Initialize an auto-scaling trigger object.
+        
+        :type name: str
+        :param name: The name for this trigger
+        
+        :type autoscale_group: str
+        :param autoscale_group: The name of the AutoScalingGroup that will be
+                                associated with the trigger. The AutoScalingGroup
+                                that will be affected by the trigger when it is
+                                activated.
+        
+        :type dimensions: list
+        :param dimensions: List of tuples, i.e.
+                            ('ImageId', 'i-13lasde') etc.
+        
+        :type measure_name: str
+        :param measure_name: The measure name associated with the metric used by
+                             the trigger to determine when to activate, for
+                             example, CPU, network I/O, or disk I/O.
+        
+        :type statistic: str
+        :param statistic: The particular statistic used by the trigger when
+                          fetching metric statistics to examine.
+        
+        :type period: int
+        :param period: The period associated with the metric statistics in
+                       seconds. Valid Values: 60 or a multiple of 60.
+        
+        :type unit: str
+        :param unit: The unit of measurement.
+        """
+        self.name = name
+        self.connection = connection
+        self.dimensions = dimensions
+        self.breach_duration = breach_duration
+        self.upper_breach_scale_increment = upper_breach_scale_increment
+        self.created_time = None
+        self.upper_threshold = upper_threshold
+        self.status = None
+        self.lower_threshold = lower_threshold
+        self.period = period
+        self.lower_breach_scale_increment = lower_breach_scale_increment
+        self.statistic = statistic
+        self.unit = unit
+        self.namespace = None
+        if autoscale_group:
+            self.autoscale_group = weakref.proxy(autoscale_group)
+        else:
+            self.autoscale_group = None
+        self.measure_name = measure_name
+
+    def __repr__(self):
+        return 'Trigger:%s' % (self.name)
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'BreachDuration':
+            self.breach_duration = value
+        elif name == 'TriggerName':
+            self.name = value
+        elif name == 'Period':
+            self.period = value
+        elif name == 'CreatedTime':
+            self.created_time = value
+        elif name == 'Statistic':
+            self.statistic = value
+        elif name == 'Unit':
+            self.unit = value
+        elif name == 'Namespace':
+            self.namespace = value
+        elif name == 'AutoScalingGroupName':
+            self.autoscale_group_name = value
+        elif name == 'MeasureName':
+            self.measure_name = value
+        else:
+            setattr(self, name, value)
+
+    def update(self):
+        """ Write out differences to trigger. """
+        self.connection.create_trigger(self)
+
+    def delete(self):
+        """ Delete this trigger. """
+        params = {
+                  'TriggerName'          : self.name,
+                  'AutoScalingGroupName' : self.autoscale_group_name,
+                  }
+        req =self.connection.get_object('DeleteTrigger', params,
+                                        Request)
+        self.connection.last_request = req
+        return req
+
diff --git a/boto/ec2/blockdevicemapping.py b/boto/ec2/blockdevicemapping.py
new file mode 100644
index 0000000..efbc38b
--- /dev/null
+++ b/boto/ec2/blockdevicemapping.py
@@ -0,0 +1,103 @@
+# Copyright (c) 2009 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.
+#
+
+class BlockDeviceType(object):
+
+    def __init__(self, connection=None):
+        self.connection = connection
+        self.ephemeral_name = None
+        self.no_device = False
+        self.volume_id = None
+        self.snapshot_id = None
+        self.status = None
+        self.attach_time = None
+        self.delete_on_termination = False
+        self.size = None
+
+    def startElement(self, name, attrs, connection):
+        pass
+
+    def endElement(self, name, value, connection):
+        if name =='volumeId':
+            self.volume_id = value
+        elif name == 'virtualName':
+            self.ephemeral_name = value
+        elif name =='NoDevice':
+            self.no_device = (value == 'true')
+        elif name =='snapshotId':
+            self.snapshot_id = value
+        elif name == 'volumeSize':
+            self.size = int(value)
+        elif name == 'status':
+            self.status = value
+        elif name == 'attachTime':
+            self.attach_time = value
+        elif name == 'deleteOnTermination':
+            if value == 'true':
+                self.delete_on_termination = True
+            else:
+                self.delete_on_termination = False
+        else:
+            setattr(self, name, value)
+
+# for backwards compatibility
+EBSBlockDeviceType = BlockDeviceType
+
+class BlockDeviceMapping(dict):
+
+    def __init__(self, connection=None):
+        dict.__init__(self)
+        self.connection = connection
+        self.current_name = None
+        self.current_value = None
+
+    def startElement(self, name, attrs, connection):
+        if name == 'ebs':
+            self.current_value = BlockDeviceType(self)
+            return self.current_value
+
+    def endElement(self, name, value, connection):
+        if name == 'device' or name == 'deviceName':
+            self.current_name = value
+        elif name == 'item':
+            self[self.current_name] = self.current_value
+
+    def build_list_params(self, params, prefix=''):
+        i = 1
+        for dev_name in self:
+            pre = '%sBlockDeviceMapping.%d' % (prefix, i)
+            params['%s.DeviceName' % pre] = dev_name
+            block_dev = self[dev_name]
+            if block_dev.ephemeral_name:
+                params['%s.VirtualName' % pre] = block_dev.ephemeral_name
+            else:
+                if block_dev.no_device:
+                    params['%s.Ebs.NoDevice' % pre] = 'true'
+                if block_dev.snapshot_id:
+                    params['%s.Ebs.SnapshotId' % pre] = block_dev.snapshot_id
+                if block_dev.size:
+                    params['%s.Ebs.VolumeSize' % pre] = block_dev.size
+                if block_dev.delete_on_termination:
+                    params['%s.Ebs.DeleteOnTermination' % pre] = 'true'
+                else:
+                    params['%s.Ebs.DeleteOnTermination' % pre] = 'false'
+            i += 1
diff --git a/boto/ec2/bundleinstance.py b/boto/ec2/bundleinstance.py
new file mode 100644
index 0000000..9651992
--- /dev/null
+++ b/boto/ec2/bundleinstance.py
@@ -0,0 +1,78 @@
+# 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.
+
+"""
+Represents an EC2 Bundle Task 
+"""
+
+from boto.ec2.ec2object import EC2Object
+
+class BundleInstanceTask(EC2Object):
+    
+    def __init__(self, connection=None):
+        EC2Object.__init__(self, connection)
+        self.id = None
+        self.instance_id = None
+        self.progress = None
+        self.start_time = None
+        self.state = None
+        self.bucket = None
+        self.prefix = None
+        self.upload_policy = None
+        self.upload_policy_signature = None
+        self.update_time = None 
+        self.code = None
+        self.message = None
+
+    def __repr__(self):
+        return 'BundleInstanceTask:%s' % self.id
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'bundleId':
+            self.id = value
+        elif name == 'instanceId':
+            self.instance_id = value
+        elif name == 'progress':
+            self.progress = value
+        elif name == 'startTime':
+            self.start_time = value
+        elif name == 'state':
+            self.state = value
+        elif name == 'bucket':
+            self.bucket = value
+        elif name == 'prefix':
+            self.prefix = value
+        elif name == 'uploadPolicy':
+            self.upload_policy = value
+        elif name == 'uploadPolicySignature':
+            self.upload_policy_signature = value
+        elif name == 'updateTime':
+            self.update_time = value
+        elif name == 'code':
+            self.code = value
+        elif name == 'message':
+            self.message = value
+        else:
+            setattr(self, name, value)
+
diff --git a/boto/ec2/buyreservation.py b/boto/ec2/buyreservation.py
new file mode 100644
index 0000000..fcd8a77
--- /dev/null
+++ b/boto/ec2/buyreservation.py
@@ -0,0 +1,84 @@
+# Copyright (c) 2006-2009 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.ec2
+from boto.sdb.db.property import StringProperty, IntegerProperty
+from boto.manage import propget
+
+InstanceTypes = ['m1.small', 'm1.large', 'm1.xlarge',
+                 'c1.medium', 'c1.xlarge', 'm2.xlarge',
+                 'm2.2xlarge', 'm2.4xlarge', 'cc1.4xlarge',
+                 't1.micro']
+
+class BuyReservation(object):
+
+    def get_region(self, params):
+        if not params.get('region', None):
+            prop = StringProperty(name='region', verbose_name='EC2 Region',
+                                  choices=boto.ec2.regions)
+            params['region'] = propget.get(prop, choices=boto.ec2.regions)
+
+    def get_instance_type(self, params):
+        if not params.get('instance_type', None):
+            prop = StringProperty(name='instance_type', verbose_name='Instance Type',
+                                  choices=InstanceTypes)
+            params['instance_type'] = propget.get(prop)
+
+    def get_quantity(self, params):
+        if not params.get('quantity', None):
+            prop = IntegerProperty(name='quantity', verbose_name='Number of Instances')
+            params['quantity'] = propget.get(prop)
+
+    def get_zone(self, params):
+        if not params.get('zone', None):
+            prop = StringProperty(name='zone', verbose_name='EC2 Availability Zone',
+                                  choices=self.ec2.get_all_zones)
+            params['zone'] = propget.get(prop)
+            
+    def get(self, params):
+        self.get_region(params)
+        self.ec2 = params['region'].connect()
+        self.get_instance_type(params)
+        self.get_zone(params)
+        self.get_quantity(params)
+
+if __name__ == "__main__":
+    obj = BuyReservation()
+    params = {}
+    obj.get(params)
+    offerings = obj.ec2.get_all_reserved_instances_offerings(instance_type=params['instance_type'],
+                                                             availability_zone=params['zone'].name)
+    print '\nThe following Reserved Instances Offerings are available:\n'
+    for offering in offerings:
+        offering.describe()
+    prop = StringProperty(name='offering', verbose_name='Offering',
+                          choices=offerings)
+    offering = propget.get(prop)
+    print '\nYou have chosen this offering:'
+    offering.describe()
+    unit_price = float(offering.fixed_price)
+    total_price = unit_price * params['quantity']
+    print '!!! You are about to purchase %d of these offerings for a total of $%.2f !!!' % (params['quantity'], total_price)
+    answer = raw_input('Are you sure you want to do this?  If so, enter YES: ')
+    if answer.strip().lower() == 'yes':
+        offering.purchase(params['quantity'])
+    else:
+        print 'Purchase cancelled'
diff --git a/boto/ec2/cloudwatch/__init__.py b/boto/ec2/cloudwatch/__init__.py
new file mode 100644
index 0000000..a02baa3
--- /dev/null
+++ b/boto/ec2/cloudwatch/__init__.py
@@ -0,0 +1,502 @@
+# Copyright (c) 2006-2009 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.
+#
+"""
+This module provides an interface to the Elastic Compute Cloud (EC2)
+CloudWatch service from AWS.
+
+The 5 Minute How-To Guide
+-------------------------
+First, make sure you have something to monitor.  You can either create a
+LoadBalancer or enable monitoring on an existing EC2 instance.  To enable
+monitoring, you can either call the monitor_instance method on the
+EC2Connection object or call the monitor method on the Instance object.
+
+It takes a while for the monitoring data to start accumulating but once
+it does, you can do this:
+
+>>> import boto
+>>> c = boto.connect_cloudwatch()
+>>> metrics = c.list_metrics()
+>>> metrics
+[Metric:NetworkIn,
+ Metric:NetworkOut,
+ Metric:NetworkOut(InstanceType,m1.small),
+ Metric:NetworkIn(InstanceId,i-e573e68c),
+ Metric:CPUUtilization(InstanceId,i-e573e68c),
+ Metric:DiskWriteBytes(InstanceType,m1.small),
+ Metric:DiskWriteBytes(ImageId,ami-a1ffb63),
+ Metric:NetworkOut(ImageId,ami-a1ffb63),
+ Metric:DiskWriteOps(InstanceType,m1.small),
+ Metric:DiskReadBytes(InstanceType,m1.small),
+ Metric:DiskReadOps(ImageId,ami-a1ffb63),
+ Metric:CPUUtilization(InstanceType,m1.small),
+ Metric:NetworkIn(ImageId,ami-a1ffb63),
+ Metric:DiskReadOps(InstanceType,m1.small),
+ Metric:DiskReadBytes,
+ Metric:CPUUtilization,
+ Metric:DiskWriteBytes(InstanceId,i-e573e68c),
+ Metric:DiskWriteOps(InstanceId,i-e573e68c),
+ Metric:DiskWriteOps,
+ Metric:DiskReadOps,
+ Metric:CPUUtilization(ImageId,ami-a1ffb63),
+ Metric:DiskReadOps(InstanceId,i-e573e68c),
+ Metric:NetworkOut(InstanceId,i-e573e68c),
+ Metric:DiskReadBytes(ImageId,ami-a1ffb63),
+ Metric:DiskReadBytes(InstanceId,i-e573e68c),
+ Metric:DiskWriteBytes,
+ Metric:NetworkIn(InstanceType,m1.small),
+ Metric:DiskWriteOps(ImageId,ami-a1ffb63)]
+
+The list_metrics call will return a list of all of the available metrics
+that you can query against.  Each entry in the list is a Metric object.
+As you can see from the list above, some of the metrics are generic metrics
+and some have Dimensions associated with them (e.g. InstanceType=m1.small).
+The Dimension can be used to refine your query.  So, for example, I could
+query the metric Metric:CPUUtilization which would create the desired statistic
+by aggregating cpu utilization data across all sources of information available
+or I could refine that by querying the metric
+Metric:CPUUtilization(InstanceId,i-e573e68c) which would use only the data
+associated with the instance identified by the instance ID i-e573e68c.
+
+Because for this example, I'm only monitoring a single instance, the set
+of metrics available to me are fairly limited.  If I was monitoring many
+instances, using many different instance types and AMI's and also several
+load balancers, the list of available metrics would grow considerably.
+
+Once you have the list of available metrics, you can actually
+query the CloudWatch system for that metric.  Let's choose the CPU utilization
+metric for our instance.
+
+>>> m = metrics[5]
+>>> m
+Metric:CPUUtilization(InstanceId,i-e573e68c)
+
+The Metric object has a query method that lets us actually perform
+the query against the collected data in CloudWatch.  To call that,
+we need a start time and end time to control the time span of data
+that we are interested in.  For this example, let's say we want the
+data for the previous hour:
+
+>>> import datetime
+>>> end = datetime.datetime.now()
+>>> start = end - datetime.timedelta(hours=1)
+
+We also need to supply the Statistic that we want reported and
+the Units to use for the results.  The Statistic can be one of these
+values:
+
+['Minimum', 'Maximum', 'Sum', 'Average', 'SampleCount']
+
+And Units must be one of the following:
+
+['Seconds', 'Percent', 'Bytes', 'Bits', 'Count',
+'Bytes/Second', 'Bits/Second', 'Count/Second']
+
+The query method also takes an optional parameter, period.  This
+parameter controls the granularity (in seconds) of the data returned.
+The smallest period is 60 seconds and the value must be a multiple
+of 60 seconds.  So, let's ask for the average as a percent:
+
+>>> datapoints = m.query(start, end, 'Average', 'Percent')
+>>> len(datapoints)
+60
+
+Our period was 60 seconds and our duration was one hour so
+we should get 60 data points back and we can see that we did.
+Each element in the datapoints list is a DataPoint object
+which is a simple subclass of a Python dict object.  Each
+Datapoint object contains all of the information available
+about that particular data point.
+
+>>> d = datapoints[0]
+>>> d
+{u'Average': 0.0,
+ u'SampleCount': 1.0,
+ u'Timestamp': u'2009-05-21T19:55:00Z',
+ u'Unit': u'Percent'}
+
+My server obviously isn't very busy right now!
+"""
+try:
+    import simplejson as json
+except ImportError:
+    import json
+from boto.connection import AWSQueryConnection
+from boto.ec2.cloudwatch.metric import Metric
+from boto.ec2.cloudwatch.alarm import MetricAlarm, AlarmHistoryItem
+from boto.ec2.cloudwatch.datapoint import Datapoint
+from boto.regioninfo import RegionInfo
+import boto
+
+RegionData = {
+    'us-east-1' : 'monitoring.us-east-1.amazonaws.com',
+    'us-west-1' : 'monitoring.us-west-1.amazonaws.com',
+    'eu-west-1' : 'monitoring.eu-west-1.amazonaws.com',
+    'ap-southeast-1' : 'monitoring.ap-southeast-1.amazonaws.com'}
+
+def regions():
+    """
+    Get all available regions for the CloudWatch service.
+
+    :rtype: list
+    :return: A list of :class:`boto.RegionInfo` instances
+    """
+    regions = []
+    for region_name in RegionData:
+        region = RegionInfo(name=region_name,
+                            endpoint=RegionData[region_name],
+                            connection_cls=CloudWatchConnection)
+        regions.append(region)
+    return regions
+
+def connect_to_region(region_name, **kw_params):
+    """
+    Given a valid region name, return a
+    :class:`boto.ec2.cloudwatch.CloudWatchConnection`.
+
+    :param str region_name: The name of the region to connect to.
+
+    :rtype: :class:`boto.ec2.CloudWatchConnection` or ``None``
+    :return: A connection to the given region, or None if an invalid region
+        name is given
+    """
+    for region in regions():
+        if region.name == region_name:
+            return region.connect(**kw_params)
+    return None
+
+
+class CloudWatchConnection(AWSQueryConnection):
+
+    APIVersion = boto.config.get('Boto', 'cloudwatch_version', '2010-08-01')
+    DefaultRegionName = boto.config.get('Boto', 'cloudwatch_region_name', 'us-east-1')
+    DefaultRegionEndpoint = boto.config.get('Boto', 'cloudwatch_region_endpoint',
+                                            'monitoring.amazonaws.com')
+
+
+    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, debug=0,
+                 https_connection_factory=None, region=None, path='/'):
+        """
+        Init method to create a new connection to EC2 Monitoring Service.
+
+        B{Note:} The host argument is overridden by the host specified in the
+        boto configuration file.
+        """
+        if not region:
+            region = RegionInfo(self, self.DefaultRegionName,
+                                self.DefaultRegionEndpoint)
+        self.region = region
+
+        AWSQueryConnection.__init__(self, aws_access_key_id,
+                                    aws_secret_access_key, 
+                                    is_secure, port, proxy, proxy_port,
+                                    proxy_user, proxy_pass,
+                                    self.region.endpoint, debug,
+                                    https_connection_factory, path)
+
+    def _required_auth_capability(self):
+        return ['ec2']
+
+    def build_list_params(self, params, items, label):
+        if isinstance(items, str):
+            items = [items]
+        for i in range(1, len(items)+1):
+            params[label % i] = items[i-1]
+
+    def get_metric_statistics(self, period, start_time, end_time, metric_name,
+                              namespace, statistics, dimensions=None, unit=None):
+        """
+        Get time-series data for one or more statistics of a given metric.
+
+        :type metric_name: string
+        :param metric_name: CPUUtilization|NetworkIO-in|NetworkIO-out|DiskIO-ALL-read|
+                             DiskIO-ALL-write|DiskIO-ALL-read-bytes|DiskIO-ALL-write-bytes
+
+        :rtype: list
+        """
+        params = {'Period' : period,
+                  'MetricName' : metric_name,
+                  'Namespace' : namespace,
+                  'StartTime' : start_time.isoformat(),
+                  'EndTime' : end_time.isoformat()}
+        self.build_list_params(params, statistics, 'Statistics.member.%d')
+        if dimensions:
+            i = 1
+            for name in dimensions:
+                params['Dimensions.member.%d.Name' % i] = name
+                params['Dimensions.member.%d.Value' % i] = dimensions[name]
+                i += 1
+        return self.get_list('GetMetricStatistics', params, [('member', Datapoint)])
+
+    def list_metrics(self, next_token=None):
+        """
+        Returns a list of the valid metrics for which there is recorded data available.
+
+        :type next_token: string
+        :param next_token: A maximum of 500 metrics will be returned at one time.
+                           If more results are available, the ResultSet returned
+                           will contain a non-Null next_token attribute.  Passing
+                           that token as a parameter to list_metrics will retrieve
+                           the next page of metrics.
+        """
+        params = {}
+        if next_token:
+            params['NextToken'] = next_token
+        return self.get_list('ListMetrics', params, [('member', Metric)])
+
+    def describe_alarms(self, action_prefix=None, alarm_name_prefix=None, alarm_names=None,
+                        max_records=None, state_value=None, next_token=None):
+        """
+        Retrieves alarms with the specified names. If no name is specified, all
+        alarms for the user are returned. Alarms can be retrieved by using only
+        a prefix for the alarm name, the alarm state, or a prefix for any
+        action.
+
+        :type action_prefix: string
+        :param action_name: The action name prefix.
+
+        :type alarm_name_prefix: string
+        :param alarm_name_prefix: The alarm name prefix. AlarmNames cannot be specified
+                                  if this parameter is specified.
+
+        :type alarm_names: list
+        :param alarm_names: A list of alarm names to retrieve information for.
+
+        :type max_records: int
+        :param max_records: The maximum number of alarm descriptions to retrieve.
+
+        :type state_value: string
+        :param state_value: The state value to be used in matching alarms.
+
+        :type next_token: string
+        :param next_token: The token returned by a previous call to indicate that there is more data.
+
+        :rtype list
+        """
+        params = {}
+        if action_prefix:
+            params['ActionPrefix'] = action_prefix
+        if alarm_name_prefix:
+            params['AlarmNamePrefix'] = alarm_name_prefix
+        elif alarm_names:
+            self.build_list_params(params, alarm_names, 'AlarmNames.member.%s')
+        if max_records:
+            params['MaxRecords'] = max_records
+        if next_token:
+            params['NextToken'] = next_token
+        if state_value:
+            params['StateValue'] = state_value
+        return self.get_list('DescribeAlarms', params, [('member', MetricAlarm)])
+
+    def describe_alarm_history(self, alarm_name=None, start_date=None, end_date=None,
+                               max_records=None, history_item_type=None, next_token=None):
+        """
+        Retrieves history for the specified alarm. Filter alarms by date range
+        or item type. If an alarm name is not specified, Amazon CloudWatch
+        returns histories for all of the owner's alarms.
+
+        Amazon CloudWatch retains the history of deleted alarms for a period of
+        six weeks. If an alarm has been deleted, its history can still be
+        queried.
+
+        :type alarm_name: string
+        :param alarm_name: The name of the alarm.
+
+        :type start_date: datetime
+        :param start_date: The starting date to retrieve alarm history.
+
+        :type end_date: datetime
+        :param end_date: The starting date to retrieve alarm history.
+
+        :type history_item_type: string
+        :param history_item_type: The type of alarm histories to retreive (ConfigurationUpdate | StateUpdate | Action)
+
+        :type max_records: int
+        :param max_records: The maximum number of alarm descriptions to retrieve.
+
+        :type next_token: string
+        :param next_token: The token returned by a previous call to indicate that there is more data.
+
+        :rtype list
+        """
+        params = {}
+        if alarm_name:
+            params['AlarmName'] = alarm_name
+        if start_date:
+            params['StartDate'] = start_date.isoformat()
+        if end_date:
+            params['EndDate'] = end_date.isoformat()
+        if history_item_type:
+            params['HistoryItemType'] = history_item_type
+        if max_records:
+            params['MaxRecords'] = max_records
+        if next_token:
+            params['NextToken'] = next_token
+        return self.get_list('DescribeAlarmHistory', params, [('member', AlarmHistoryItem)])
+
+    def describe_alarms_for_metric(self, metric_name, namespace, period=None, statistic=None, dimensions=None, unit=None):
+        """
+        Retrieves all alarms for a single metric. Specify a statistic, period,
+        or unit to filter the set of alarms further.
+
+        :type metric_name: string
+        :param metric_name: The name of the metric
+
+        :type namespace: string
+        :param namespace: The namespace of the metric.
+
+        :type period: int
+        :param period: The period in seconds over which the statistic is applied.
+
+        :type statistic: string
+        :param statistic: The statistic for the metric.
+
+        :type dimensions: list
+
+        :type unit: string
+
+        :rtype list
+        """
+        params = {
+                    'MetricName'        :   metric_name,
+                    'Namespace'         :   namespace,
+                 }
+        if period:
+            params['Period'] = period
+        if statistic:
+            params['Statistic'] = statistic
+        if dimensions:
+            self.build_list_params(params, dimensions, 'Dimensions.member.%s')
+        if unit:
+            params['Unit'] = unit
+        return self.get_list('DescribeAlarmsForMetric', params, [('member', MetricAlarm)])
+
+    def put_metric_alarm(self, alarm):
+        """
+        Creates or updates an alarm and associates it with the specified Amazon
+        CloudWatch metric. Optionally, this operation can associate one or more
+        Amazon Simple Notification Service resources with the alarm.
+
+        When this operation creates an alarm, the alarm state is immediately
+        set to INSUFFICIENT_DATA. The alarm is evaluated and its StateValue is
+        set appropriately. Any actions associated with the StateValue is then
+        executed.
+
+        When updating an existing alarm, its StateValue is left unchanged.
+
+        :type alarm: boto.ec2.cloudwatch.alarm.MetricAlarm
+        :param alarm: MetricAlarm object.
+        """
+        params = {
+                    'AlarmName'             :       alarm.name,
+                    'MetricName'            :       alarm.metric,
+                    'Namespace'             :       alarm.namespace,
+                    'Statistic'             :       alarm.statistic,
+                    'ComparisonOperator'    :       MetricAlarm._cmp_map[alarm.comparison],
+                    'Threshold'             :       alarm.threshold,
+                    'EvaluationPeriods'     :       alarm.evaluation_periods,
+                    'Period'                :       alarm.period,
+                 }
+        if alarm.actions_enabled is not None:
+            params['ActionsEnabled'] = alarm.actions_enabled
+        if alarm.alarm_actions:
+            self.build_list_params(params, alarm.alarm_actions, 'AlarmActions.member.%s')
+        if alarm.description:
+            params['AlarmDescription'] = alarm.description
+        if alarm.dimensions:
+            self.build_list_params(params, alarm.dimensions, 'Dimensions.member.%s')
+        if alarm.insufficient_data_actions:
+            self.build_list_params(params, alarm.insufficient_data_actions, 'InsufficientDataActions.member.%s')
+        if alarm.ok_actions:
+            self.build_list_params(params, alarm.ok_actions, 'OKActions.member.%s')
+        if alarm.unit:
+            params['Unit'] = alarm.unit
+        alarm.connection = self
+        return self.get_status('PutMetricAlarm', params)
+    create_alarm = put_metric_alarm
+    update_alarm = put_metric_alarm
+
+    def delete_alarms(self, alarms):
+        """
+        Deletes all specified alarms. In the event of an error, no alarms are deleted.
+
+        :type alarms: list
+        :param alarms: List of alarm names.
+        """
+        params = {}
+        self.build_list_params(params, alarms, 'AlarmNames.member.%s')
+        return self.get_status('DeleteAlarms', params)
+
+    def set_alarm_state(self, alarm_name, state_reason, state_value, state_reason_data=None):
+        """
+        Temporarily sets the state of an alarm. When the updated StateValue
+        differs from the previous value, the action configured for the
+        appropriate state is invoked. This is not a permanent change. The next
+        periodic alarm check (in about a minute) will set the alarm to its
+        actual state.
+
+        :type alarm_name: string
+        :param alarm_name: Descriptive name for alarm.
+
+        :type state_reason: string
+        :param state_reason: Human readable reason.
+
+        :type state_value: string
+        :param state_value: OK | ALARM | INSUFFICIENT_DATA
+
+        :type state_reason_data: string
+        :param state_reason_data: Reason string (will be jsonified).
+        """
+        params = {
+                    'AlarmName'             :   alarm_name,
+                    'StateReason'           :   state_reason,
+                    'StateValue'            :   state_value,
+                 }
+        if state_reason_data:
+            params['StateReasonData'] = json.dumps(state_reason_data)
+
+        return self.get_status('SetAlarmState', params)
+
+    def enable_alarm_actions(self, alarm_names):
+        """
+        Enables actions for the specified alarms.
+
+        :type alarms: list
+        :param alarms: List of alarm names.
+        """
+        params = {}
+        self.build_list_params(params, alarm_names, 'AlarmNames.member.%s')
+        return self.get_status('EnableAlarmActions', params)
+
+    def disable_alarm_actions(self, alarm_names):
+        """
+        Disables actions for the specified alarms.
+
+        :type alarms: list
+        :param alarms: List of alarm names.
+        """
+        params = {}
+        self.build_list_params(params, alarm_names, 'AlarmNames.member.%s')
+        return self.get_status('DisableAlarmActions', params)
+
diff --git a/boto/ec2/cloudwatch/alarm.py b/boto/ec2/cloudwatch/alarm.py
new file mode 100644
index 0000000..81c0fc3
--- /dev/null
+++ b/boto/ec2/cloudwatch/alarm.py
@@ -0,0 +1,183 @@
+# Copyright (c) 2010 Reza Lotun http://reza.lotun.name
+#
+# 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 datetime import datetime
+import json
+
+
+class MetricAlarm(object):
+
+    OK = 'OK'
+    ALARM = 'ALARM'
+    INSUFFICIENT_DATA = 'INSUFFICIENT_DATA'
+
+    _cmp_map = {
+                    '>='    :   'GreaterThanOrEqualToThreshold',
+                    '>'     :   'GreaterThanThreshold',
+                    '<'     :   'LessThanThreshold',
+                    '<='    :   'LessThanOrEqualToThreshold',
+               }
+    _rev_cmp_map = dict((v, k) for (k, v) in _cmp_map.iteritems())
+
+    def __init__(self, connection=None, name=None, metric=None,
+                 namespace=None, statistic=None, comparison=None, threshold=None,
+                 period=None, evaluation_periods=None):
+        """
+        Creates a new Alarm.
+
+        :type name: str
+        :param name: Name of alarm.
+
+        :type metric: str
+        :param metric: Name of alarm's associated metric.
+
+        :type namespace: str
+        :param namespace: The namespace for the alarm's metric.
+
+        :type statistic: str
+        :param statistic: The statistic to apply to the alarm's associated metric. Can
+                          be one of 'SampleCount', 'Average', 'Sum', 'Minimum', 'Maximum'
+
+        :type comparison: str
+        :param comparison: Comparison used to compare statistic with threshold. Can be
+                           one of '>=', '>', '<', '<='
+
+        :type threshold: float
+        :param threshold: The value against which the specified statistic is compared.
+
+        :type period: int
+        :param period: The period in seconds over which teh specified statistic is applied.
+
+        :type evaluation_periods: int
+        :param evaluation_period: The number of periods over which data is compared to
+                                  the specified threshold
+        """
+        self.name = name
+        self.connection = connection
+        self.metric = metric
+        self.namespace = namespace
+        self.statistic = statistic
+        self.threshold = float(threshold) if threshold is not None else None
+        self.comparison = self._cmp_map.get(comparison)
+        self.period = int(period) if period is not None else None
+        self.evaluation_periods = int(evaluation_periods) if evaluation_periods is not None else None
+        self.actions_enabled = None
+        self.alarm_actions = []
+        self.alarm_arn = None
+        self.last_updated = None
+        self.description = ''
+        self.dimensions = []
+        self.insufficient_data_actions = []
+        self.ok_actions = []
+        self.state_reason = None
+        self.state_value = None
+        self.unit = None
+
+    def __repr__(self):
+        return 'MetricAlarm:%s[%s(%s) %s %s]' % (self.name, self.metric, self.statistic, self.comparison, self.threshold)
+
+    def startElement(self, name, attrs, connection):
+        return
+
+    def endElement(self, name, value, connection):
+        if name == 'ActionsEnabled':
+            self.actions_enabled = value
+        elif name == 'AlarmArn':
+            self.alarm_arn = value
+        elif name == 'AlarmConfigurationUpdatedTimestamp':
+            self.last_updated = value
+        elif name == 'AlarmDescription':
+            self.description = value
+        elif name == 'AlarmName':
+            self.name = value
+        elif name == 'ComparisonOperator':
+            setattr(self, 'comparison', self._rev_cmp_map[value])
+        elif name == 'EvaluationPeriods':
+            self.evaluation_periods = int(value)
+        elif name == 'MetricName':
+            self.metric = value
+        elif name == 'Namespace':
+            self.namespace = value
+        elif name == 'Period':
+            self.period = int(value)
+        elif name == 'StateReason':
+            self.state_reason = value
+        elif name == 'StateValue':
+            self.state_value = None
+        elif name == 'Statistic':
+            self.statistic = value
+        elif name == 'Threshold':
+            self.threshold = float(value)
+        elif name == 'Unit':
+            self.unit = value
+        else:
+            setattr(self, name, value)
+
+    def set_state(self, value, reason, data=None):
+        """ Temporarily sets the state of an alarm.
+
+        :type value: str
+        :param value: OK | ALARM | INSUFFICIENT_DATA
+
+        :type reason: str
+        :param reason: Reason alarm set (human readable).
+
+        :type data: str
+        :param data: Reason data (will be jsonified).
+        """
+        return self.connection.set_alarm_state(self.name, reason, value, data)
+
+    def update(self):
+        return self.connection.update_alarm(self)
+
+    def enable_actions(self):
+        return self.connection.enable_alarm_actions([self.name])
+
+    def disable_actions(self):
+        return self.connection.disable_alarm_actions([self.name])
+
+    def describe_history(self, start_date=None, end_date=None, max_records=None, history_item_type=None, next_token=None):
+        return self.connection.describe_alarm_history(self.name, start_date, end_date,
+                                                      max_records, history_item_type, next_token)
+
+class AlarmHistoryItem(object):
+    def __init__(self, connection=None):
+        self.connection = connection
+
+    def __repr__(self):
+        return 'AlarmHistory:%s[%s at %s]' % (self.name, self.summary, self.timestamp)
+
+    def startElement(self, name, attrs, connection):
+        pass
+
+    def endElement(self, name, value, connection):
+        if name == 'AlarmName':
+            self.name = value
+        elif name == 'HistoryData':
+            self.data = json.loads(value)
+        elif name == 'HistoryItemType':
+            self.tem_type = value
+        elif name == 'HistorySummary':
+            self.summary = value
+        elif name == 'Timestamp':
+            self.timestamp = datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%fZ')
+
diff --git a/boto/ec2/cloudwatch/datapoint.py b/boto/ec2/cloudwatch/datapoint.py
new file mode 100644
index 0000000..d4350ce
--- /dev/null
+++ b/boto/ec2/cloudwatch/datapoint.py
@@ -0,0 +1,40 @@
+# Copyright (c) 2006-2009 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 datetime import datetime
+
+class Datapoint(dict):
+
+    def __init__(self, connection=None):
+        dict.__init__(self)
+        self.connection = connection
+
+    def startElement(self, name, attrs, connection):
+        pass
+
+    def endElement(self, name, value, connection):
+        if name in ['Average', 'Maximum', 'Minimum', 'Sum', 'SampleCount']:
+            self[name] = float(value)
+        elif name == 'Timestamp':
+            self[name] = datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')
+        elif name != 'member':
+            self[name] = value
+
diff --git a/boto/ec2/cloudwatch/metric.py b/boto/ec2/cloudwatch/metric.py
new file mode 100644
index 0000000..cd8c4bc
--- /dev/null
+++ b/boto/ec2/cloudwatch/metric.py
@@ -0,0 +1,80 @@
+# Copyright (c) 2006-2009 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.
+#
+
+class Dimensions(dict):
+
+    def startElement(self, name, attrs, connection):
+        pass
+
+    def endElement(self, name, value, connection):
+        if name == 'Name':
+            self._name = value
+        elif name == 'Value':
+            self[self._name] = value
+        elif name != 'Dimensions' and name != 'member':
+            self[name] = value
+
+class Metric(object):
+
+    Statistics = ['Minimum', 'Maximum', 'Sum', 'Average', 'SampleCount']
+    Units = ['Seconds', 'Percent', 'Bytes', 'Bits', 'Count',
+             'Bytes/Second', 'Bits/Second', 'Count/Second']
+
+    def __init__(self, connection=None):
+        self.connection = connection
+        self.name = None
+        self.namespace = None
+        self.dimensions = None
+
+    def __repr__(self):
+        s = 'Metric:%s' % self.name
+        if self.dimensions:
+            for name,value in self.dimensions.items():
+                s += '(%s,%s)' % (name, value)
+        return s
+
+    def startElement(self, name, attrs, connection):
+        if name == 'Dimensions':
+            self.dimensions = Dimensions()
+            return self.dimensions
+
+    def endElement(self, name, value, connection):
+        if name == 'MetricName':
+            self.name = value
+        elif name == 'Namespace':
+            self.namespace = value
+        else:
+            setattr(self, name, value)
+
+    def query(self, start_time, end_time, statistic, unit=None, period=60):
+        return self.connection.get_metric_statistics(period, start_time, end_time,
+                                                     self.name, self.namespace, [statistic],
+                                                     self.dimensions, unit)
+
+    def describe_alarms(self, period=None, statistic=None, dimensions=None, unit=None):
+        return self.connection.describe_alarms_for_metric(self.name,
+                                                          self.namespace,
+                                                          period,
+                                                          statistic,
+                                                          dimensions,
+                                                          unit)
+
diff --git a/boto/ec2/connection.py b/boto/ec2/connection.py
new file mode 100644
index 0000000..89d1d4e
--- /dev/null
+++ b/boto/ec2/connection.py
@@ -0,0 +1,2287 @@
+# Copyright (c) 2006-2010 Mitch Garnaat http://garnaat.org/
+# Copyright (c) 2010, Eucalyptus Systems, Inc.
+#
+# 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.
+
+"""
+Represents a connection to the EC2 service.
+"""
+
+import base64
+import warnings
+from datetime import datetime
+from datetime import timedelta
+import boto
+from boto.connection import AWSQueryConnection
+from boto.resultset import ResultSet
+from boto.ec2.image import Image, ImageAttribute
+from boto.ec2.instance import Reservation, Instance, ConsoleOutput, InstanceAttribute
+from boto.ec2.keypair import KeyPair
+from boto.ec2.address import Address
+from boto.ec2.volume import Volume
+from boto.ec2.snapshot import Snapshot
+from boto.ec2.snapshot import SnapshotAttribute
+from boto.ec2.zone import Zone
+from boto.ec2.securitygroup import SecurityGroup
+from boto.ec2.regioninfo import RegionInfo
+from boto.ec2.instanceinfo import InstanceInfo
+from boto.ec2.reservedinstance import ReservedInstancesOffering, ReservedInstance
+from boto.ec2.spotinstancerequest import SpotInstanceRequest
+from boto.ec2.spotpricehistory import SpotPriceHistory
+from boto.ec2.spotdatafeedsubscription import SpotDatafeedSubscription
+from boto.ec2.bundleinstance import BundleInstanceTask
+from boto.ec2.placementgroup import PlacementGroup
+from boto.ec2.tag import Tag
+from boto.exception import EC2ResponseError
+
+#boto.set_stream_logger('ec2')
+
+class EC2Connection(AWSQueryConnection):
+
+    APIVersion = boto.config.get('Boto', 'ec2_version', '2010-08-31')
+    DefaultRegionName = boto.config.get('Boto', 'ec2_region_name', 'us-east-1')
+    DefaultRegionEndpoint = boto.config.get('Boto', 'ec2_region_endpoint',
+                                            'ec2.amazonaws.com')
+    ResponseError = EC2ResponseError
+
+    def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
+                 is_secure=True, host=None, port=None, proxy=None, proxy_port=None,
+                 proxy_user=None, proxy_pass=None, debug=0,
+                 https_connection_factory=None, region=None, path='/'):
+        """
+        Init method to create a new connection to EC2.
+
+        B{Note:} The host argument is overridden by the host specified in the
+                 boto configuration file.
+        """
+        if not region:
+            region = RegionInfo(self, self.DefaultRegionName,
+                                self.DefaultRegionEndpoint)
+        self.region = region
+        AWSQueryConnection.__init__(self, aws_access_key_id,
+                                    aws_secret_access_key,
+                                    is_secure, port, proxy, proxy_port,
+                                    proxy_user, proxy_pass,
+                                    self.region.endpoint, debug,
+                                    https_connection_factory, path)
+
+    def _required_auth_capability(self):
+        return ['ec2']
+
+    def get_params(self):
+        """
+        Returns a dictionary containing the value of of all of the keyword
+        arguments passed when constructing this connection.
+        """
+        param_names = ['aws_access_key_id', 'aws_secret_access_key', 'is_secure',
+                       'port', 'proxy', 'proxy_port', 'proxy_user', 'proxy_pass',
+                       'debug', 'https_connection_factory']
+        params = {}
+        for name in param_names:
+            params[name] = getattr(self, name)
+        return params
+
+    def build_filter_params(self, params, filters):
+        i = 1
+        for name in filters:
+            aws_name = name.replace('_', '-')
+            params['Filter.%d.Name' % i] = aws_name
+            value = filters[name]
+            if not isinstance(value, list):
+                value = [value]
+            j = 1
+            for v in value:
+                params['Filter.%d.Value.%d' % (i,j)] = v
+                j += 1
+            i += 1
+
+    # Image methods
+
+    def get_all_images(self, image_ids=None, owners=None,
+                       executable_by=None, filters=None):
+        """
+        Retrieve all the EC2 images available on your account.
+
+        :type image_ids: list
+        :param image_ids: A list of strings with the image IDs wanted
+
+        :type owners: list
+        :param owners: A list of owner IDs
+
+        :type executable_by: list
+        :param executable_by: Returns AMIs for which the specified
+                              user ID has explicit launch permissions
+
+        :type filters: dict
+        :param filters: Optional filters that can be used to limit
+                        the results returned.  Filters are provided
+                        in the form of a dictionary consisting of
+                        filter names as the key and filter values
+                        as the value.  The set of allowable filter
+                        names/values is dependent on the request
+                        being performed.  Check the EC2 API guide
+                        for details.
+
+        :rtype: list
+        :return: A list of :class:`boto.ec2.image.Image`
+        """
+        params = {}
+        if image_ids:
+            self.build_list_params(params, image_ids, 'ImageId')
+        if owners:
+            self.build_list_params(params, owners, 'Owner')
+        if executable_by:
+            self.build_list_params(params, executable_by, 'ExecutableBy')
+        if filters:
+            self.build_filter_params(params, filters)
+        return self.get_list('DescribeImages', params, [('item', Image)], verb='POST')
+
+    def get_all_kernels(self, kernel_ids=None, owners=None):
+        """
+        Retrieve all the EC2 kernels available on your account.
+        Constructs a filter to allow the processing to happen server side.
+
+        :type kernel_ids: list
+        :param kernel_ids: A list of strings with the image IDs wanted
+
+        :type owners: list
+        :param owners: A list of owner IDs
+
+        :rtype: list
+        :return: A list of :class:`boto.ec2.image.Image`
+        """
+        params = {}
+        if kernel_ids:
+            self.build_list_params(params, kernel_ids, 'ImageId')
+        if owners:
+            self.build_list_params(params, owners, 'Owner')
+        filter = {'image-type' : 'kernel'}
+        self.build_filter_params(params, filter)
+        return self.get_list('DescribeImages', params, [('item', Image)], verb='POST')
+
+    def get_all_ramdisks(self, ramdisk_ids=None, owners=None):
+        """
+        Retrieve all the EC2 ramdisks available on your account.
+        Constructs a filter to allow the processing to happen server side.
+
+        :type ramdisk_ids: list
+        :param ramdisk_ids: A list of strings with the image IDs wanted
+
+        :type owners: list
+        :param owners: A list of owner IDs
+
+        :rtype: list
+        :return: A list of :class:`boto.ec2.image.Image`
+        """
+        params = {}
+        if ramdisk_ids:
+            self.build_list_params(params, ramdisk_ids, 'ImageId')
+        if owners:
+            self.build_list_params(params, owners, 'Owner')
+        filter = {'image-type' : 'ramdisk'}
+        self.build_filter_params(params, filter)
+        return self.get_list('DescribeImages', params, [('item', Image)], verb='POST')
+
+    def get_image(self, image_id):
+        """
+        Shortcut method to retrieve a specific image (AMI).
+
+        :type image_id: string
+        :param image_id: the ID of the Image to retrieve
+
+        :rtype: :class:`boto.ec2.image.Image`
+        :return: The EC2 Image specified or None if the image is not found
+        """
+        try:
+            return self.get_all_images(image_ids=[image_id])[0]
+        except IndexError: # None of those images available
+            return None
+
+    def register_image(self, name=None, description=None, image_location=None,
+                       architecture=None, kernel_id=None, ramdisk_id=None,
+                       root_device_name=None, block_device_map=None):
+        """
+        Register an image.
+
+        :type name: string
+        :param name: The name of the AMI.  Valid only for EBS-based images.
+
+        :type description: string
+        :param description: The description of the AMI.
+
+        :type image_location: string
+        :param image_location: Full path to your AMI manifest in Amazon S3 storage.
+                               Only used for S3-based AMI's.
+
+        :type architecture: string
+        :param architecture: The architecture of the AMI.  Valid choices are:
+                             i386 | x86_64
+
+        :type kernel_id: string
+        :param kernel_id: The ID of the kernel with which to launch the instances
+
+        :type root_device_name: string
+        :param root_device_name: The root device name (e.g. /dev/sdh)
+
+        :type block_device_map: :class:`boto.ec2.blockdevicemapping.BlockDeviceMapping`
+        :param block_device_map: A BlockDeviceMapping data structure
+                                 describing the EBS volumes associated
+                                 with the Image.
+
+        :rtype: string
+        :return: The new image id
+        """
+        params = {}
+        if name:
+            params['Name'] = name
+        if description:
+            params['Description'] = description
+        if architecture:
+            params['Architecture'] = architecture
+        if kernel_id:
+            params['KernelId'] = kernel_id
+        if ramdisk_id:
+            params['RamdiskId'] = ramdisk_id
+        if image_location:
+            params['ImageLocation'] = image_location
+        if root_device_name:
+            params['RootDeviceName'] = root_device_name
+        if block_device_map:
+            block_device_map.build_list_params(params)
+        rs = self.get_object('RegisterImage', params, ResultSet, verb='POST')
+        image_id = getattr(rs, 'imageId', None)
+        return image_id
+
+    def deregister_image(self, image_id):
+        """
+        Unregister an AMI.
+
+        :type image_id: string
+        :param image_id: the ID of the Image to unregister
+
+        :rtype: bool
+        :return: True if successful
+        """
+        return self.get_status('DeregisterImage', {'ImageId':image_id}, verb='POST')
+
+    def create_image(self, instance_id, name, description=None, no_reboot=False):
+        """
+        Will create an AMI from the instance in the running or stopped
+        state.
+        
+        :type instance_id: string
+        :param instance_id: the ID of the instance to image.
+
+        :type name: string
+        :param name: The name of the new image
+
+        :type description: string
+        :param description: An optional human-readable string describing
+                            the contents and purpose of the AMI.
+
+        :type no_reboot: bool
+        :param no_reboot: An optional flag indicating that the bundling process
+                          should not attempt to shutdown the instance before
+                          bundling.  If this flag is True, the responsibility
+                          of maintaining file system integrity is left to the
+                          owner of the instance.
+        
+        :rtype: string
+        :return: The new image id
+        """
+        params = {'InstanceId' : instance_id,
+                  'Name' : name}
+        if description:
+            params['Description'] = description
+        if no_reboot:
+            params['NoReboot'] = 'true'
+        img = self.get_object('CreateImage', params, Image, verb='POST')
+        return img.id
+        
+    # ImageAttribute methods
+
+    def get_image_attribute(self, image_id, attribute='launchPermission'):
+        """
+        Gets an attribute from an image.
+
+        :type image_id: string
+        :param image_id: The Amazon image id for which you want info about
+
+        :type attribute: string
+        :param attribute: The attribute you need information about.
+                          Valid choices are:
+                          * launchPermission
+                          * productCodes
+                          * blockDeviceMapping
+
+        :rtype: :class:`boto.ec2.image.ImageAttribute`
+        :return: An ImageAttribute object representing the value of the
+                 attribute requested
+        """
+        params = {'ImageId' : image_id,
+                  'Attribute' : attribute}
+        return self.get_object('DescribeImageAttribute', params, ImageAttribute, verb='POST')
+
+    def modify_image_attribute(self, image_id, attribute='launchPermission',
+                               operation='add', user_ids=None, groups=None,
+                               product_codes=None):
+        """
+        Changes an attribute of an image.
+
+        :type image_id: string
+        :param image_id: The image id you wish to change
+
+        :type attribute: string
+        :param attribute: The attribute you wish to change
+
+        :type operation: string
+        :param operation: Either add or remove (this is required for changing
+                          launchPermissions)
+
+        :type user_ids: list
+        :param user_ids: The Amazon IDs of users to add/remove attributes
+
+        :type groups: list
+        :param groups: The groups to add/remove attributes
+
+        :type product_codes: list
+        :param product_codes: Amazon DevPay product code. Currently only one
+                              product code can be associated with an AMI. Once
+                              set, the product code cannot be changed or reset.
+        """
+        params = {'ImageId' : image_id,
+                  'Attribute' : attribute,
+                  'OperationType' : operation}
+        if user_ids:
+            self.build_list_params(params, user_ids, 'UserId')
+        if groups:
+            self.build_list_params(params, groups, 'UserGroup')
+        if product_codes:
+            self.build_list_params(params, product_codes, 'ProductCode')
+        return self.get_status('ModifyImageAttribute', params, verb='POST')
+
+    def reset_image_attribute(self, image_id, attribute='launchPermission'):
+        """
+        Resets an attribute of an AMI to its default value.
+
+        :type image_id: string
+        :param image_id: ID of the AMI for which an attribute will be described
+
+        :type attribute: string
+        :param attribute: The attribute to reset
+
+        :rtype: bool
+        :return: Whether the operation succeeded or not
+        """
+        params = {'ImageId' : image_id,
+                  'Attribute' : attribute}
+        return self.get_status('ResetImageAttribute', params, verb='POST')
+
+    # Instance methods
+
+    def get_all_instances(self, instance_ids=None, filters=None):
+        """
+        Retrieve all the instances associated with your account.
+
+        :type instance_ids: list
+        :param instance_ids: A list of strings of instance IDs
+
+        :type filters: dict
+        :param filters: Optional filters that can be used to limit
+                        the results returned.  Filters are provided
+                        in the form of a dictionary consisting of
+                        filter names as the key and filter values
+                        as the value.  The set of allowable filter
+                        names/values is dependent on the request
+                        being performed.  Check the EC2 API guide
+                        for details.
+
+        :rtype: list
+        :return: A list of  :class:`boto.ec2.instance.Reservation`
+        """
+        params = {}
+        if instance_ids:
+            self.build_list_params(params, instance_ids, 'InstanceId')
+        if filters:
+            self.build_filter_params(params, filters)
+        return self.get_list('DescribeInstances', params,
+                             [('item', Reservation)], verb='POST')
+
+    def run_instances(self, image_id, min_count=1, max_count=1,
+                      key_name=None, security_groups=None,
+                      user_data=None, addressing_type=None,
+                      instance_type='m1.small', placement=None,
+                      kernel_id=None, ramdisk_id=None,
+                      monitoring_enabled=False, subnet_id=None,
+                      block_device_map=None,
+                      disable_api_termination=False,
+                      instance_initiated_shutdown_behavior=None,
+                      private_ip_address=None,
+                      placement_group=None, client_token=None):
+        """
+        Runs an image on EC2.
+
+        :type image_id: string
+        :param image_id: The ID of the image to run
+
+        :type min_count: int
+        :param min_count: The minimum number of instances to launch
+
+        :type max_count: int
+        :param max_count: The maximum number of instances to launch
+
+        :type key_name: string
+        :param key_name: The name of the key pair with which to launch instances
+
+        :type security_groups: list of strings
+        :param security_groups: The names of the security groups with which to
+                                associate instances
+
+        :type user_data: string
+        :param user_data: The user data passed to the launched instances
+
+        :type instance_type: string
+        :param instance_type: The type of instance to run:
+                              
+                              * m1.small
+                              * m1.large
+                              * m1.xlarge
+                              * c1.medium
+                              * c1.xlarge
+                              * m2.xlarge
+                              * m2.2xlarge
+                              * m2.4xlarge
+                              * cc1.4xlarge
+                              * t1.micro
+
+        :type placement: string
+        :param placement: The availability zone in which to launch the instances
+
+        :type kernel_id: string
+        :param kernel_id: The ID of the kernel with which to launch the
+                          instances
+
+        :type ramdisk_id: string
+        :param ramdisk_id: The ID of the RAM disk with which to launch the
+                           instances
+
+        :type monitoring_enabled: bool
+        :param monitoring_enabled: Enable CloudWatch monitoring on the instance.
+
+        :type subnet_id: string
+        :param subnet_id: The subnet ID within which to launch the instances
+                          for VPC.
+
+        :type private_ip_address: string
+        :param private_ip_address: If you're using VPC, you can optionally use
+                                   this parameter to assign the instance a
+                                   specific available IP address from the
+                                   subnet (e.g., 10.0.0.25).
+
+        :type block_device_map: :class:`boto.ec2.blockdevicemapping.BlockDeviceMapping`
+        :param block_device_map: A BlockDeviceMapping data structure
+                                 describing the EBS volumes associated
+                                 with the Image.
+
+        :type disable_api_termination: bool
+        :param disable_api_termination: If True, the instances will be locked
+                                        and will not be able to be terminated
+                                        via the API.
+
+        :type instance_initiated_shutdown_behavior: string
+        :param instance_initiated_shutdown_behavior: Specifies whether the
+                                                     instance's EBS volumes are
+                                                     stopped (i.e. detached) or
+                                                     terminated (i.e. deleted)
+                                                     when the instance is
+                                                     shutdown by the
+                                                     owner.  Valid values are:
+                                                     
+                                                     * stop
+                                                     * terminate
+
+        :type placement_group: string
+        :param placement_group: If specified, this is the name of the placement
+                                group in which the instance(s) will be launched.
+
+        :type client_token: string
+        :param client_token: Unique, case-sensitive identifier you provide
+                             to ensure idempotency of the request.
+                             Maximum 64 ASCII characters
+
+        :rtype: Reservation
+        :return: The :class:`boto.ec2.instance.Reservation` associated with
+                 the request for machines
+        """
+        params = {'ImageId':image_id,
+                  'MinCount':min_count,
+                  'MaxCount': max_count}
+        if key_name:
+            params['KeyName'] = key_name
+        if security_groups:
+            l = []
+            for group in security_groups:
+                if isinstance(group, SecurityGroup):
+                    l.append(group.name)
+                else:
+                    l.append(group)
+            self.build_list_params(params, l, 'SecurityGroup')
+        if user_data:
+            params['UserData'] = base64.b64encode(user_data)
+        if addressing_type:
+            params['AddressingType'] = addressing_type
+        if instance_type:
+            params['InstanceType'] = instance_type
+        if placement:
+            params['Placement.AvailabilityZone'] = placement
+        if placement_group:
+            params['Placement.GroupName'] = placement_group
+        if kernel_id:
+            params['KernelId'] = kernel_id
+        if ramdisk_id:
+            params['RamdiskId'] = ramdisk_id
+        if monitoring_enabled:
+            params['Monitoring.Enabled'] = 'true'
+        if subnet_id:
+            params['SubnetId'] = subnet_id
+        if private_ip_address:
+            params['PrivateIpAddress'] = private_ip_address
+        if block_device_map:
+            block_device_map.build_list_params(params)
+        if disable_api_termination:
+            params['DisableApiTermination'] = 'true'
+        if instance_initiated_shutdown_behavior:
+            val = instance_initiated_shutdown_behavior
+            params['InstanceInitiatedShutdownBehavior'] = val
+        if client_token:
+            params['ClientToken'] = client_token
+        return self.get_object('RunInstances', params, Reservation, verb='POST')
+
+    def terminate_instances(self, instance_ids=None):
+        """
+        Terminate the instances specified
+
+        :type instance_ids: list
+        :param instance_ids: A list of strings of the Instance IDs to terminate
+
+        :rtype: list
+        :return: A list of the instances terminated
+        """
+        params = {}
+        if instance_ids:
+            self.build_list_params(params, instance_ids, 'InstanceId')
+        return self.get_list('TerminateInstances', params, [('item', Instance)], verb='POST')
+
+    def stop_instances(self, instance_ids=None, force=False):
+        """
+        Stop the instances specified
+        
+        :type instance_ids: list
+        :param instance_ids: A list of strings of the Instance IDs to stop
+
+        :type force: bool
+        :param force: Forces the instance to stop
+        
+        :rtype: list
+        :return: A list of the instances stopped
+        """
+        params = {}
+        if force:
+            params['Force'] = 'true'
+        if instance_ids:
+            self.build_list_params(params, instance_ids, 'InstanceId')
+        return self.get_list('StopInstances', params, [('item', Instance)], verb='POST')
+
+    def start_instances(self, instance_ids=None):
+        """
+        Start the instances specified
+        
+        :type instance_ids: list
+        :param instance_ids: A list of strings of the Instance IDs to start
+        
+        :rtype: list
+        :return: A list of the instances started
+        """
+        params = {}
+        if instance_ids:
+            self.build_list_params(params, instance_ids, 'InstanceId')
+        return self.get_list('StartInstances', params, [('item', Instance)], verb='POST')
+
+    def get_console_output(self, instance_id):
+        """
+        Retrieves the console output for the specified instance.
+
+        :type instance_id: string
+        :param instance_id: The instance ID of a running instance on the cloud.
+
+        :rtype: :class:`boto.ec2.instance.ConsoleOutput`
+        :return: The console output as a ConsoleOutput object
+        """
+        params = {}
+        self.build_list_params(params, [instance_id], 'InstanceId')
+        return self.get_object('GetConsoleOutput', params, ConsoleOutput, verb='POST')
+
+    def reboot_instances(self, instance_ids=None):
+        """
+        Reboot the specified instances.
+
+        :type instance_ids: list
+        :param instance_ids: The instances to terminate and reboot
+        """
+        params = {}
+        if instance_ids:
+            self.build_list_params(params, instance_ids, 'InstanceId')
+        return self.get_status('RebootInstances', params)
+
+    def confirm_product_instance(self, product_code, instance_id):
+        params = {'ProductCode' : product_code,
+                  'InstanceId' : instance_id}
+        rs = self.get_object('ConfirmProductInstance', params, ResultSet, verb='POST')
+        return (rs.status, rs.ownerId)
+
+    # InstanceAttribute methods
+
+    def get_instance_attribute(self, instance_id, attribute):
+        """
+        Gets an attribute from an instance.
+
+        :type instance_id: string
+        :param instance_id: The Amazon id of the instance
+
+        :type attribute: string
+        :param attribute: The attribute you need information about
+                          Valid choices are:
+                          
+                          * instanceType|kernel|ramdisk|userData|
+                          * disableApiTermination|
+                          * instanceInitiatedShutdownBehavior|
+                          * rootDeviceName|blockDeviceMapping
+
+        :rtype: :class:`boto.ec2.image.InstanceAttribute`
+        :return: An InstanceAttribute object representing the value of the
+                 attribute requested
+        """
+        params = {'InstanceId' : instance_id}
+        if attribute:
+            params['Attribute'] = attribute
+        return self.get_object('DescribeInstanceAttribute', params,
+                               InstanceAttribute, verb='POST')
+
+    def modify_instance_attribute(self, instance_id, attribute, value):
+        """
+        Changes an attribute of an instance
+
+        :type instance_id: string
+        :param instance_id: The instance id you wish to change
+
+        :type attribute: string
+        :param attribute: The attribute you wish to change.
+        
+                          * AttributeName - Expected value (default)
+                          * instanceType - A valid instance type (m1.small)
+                          * kernel - Kernel ID (None)
+                          * ramdisk - Ramdisk ID (None)
+                          * userData - Base64 encoded String (None)
+                          * disableApiTermination - Boolean (true)
+                          * instanceInitiatedShutdownBehavior - stop|terminate
+                          * rootDeviceName - device name (None)
+
+        :type value: string
+        :param value: The new value for the attribute
+
+        :rtype: bool
+        :return: Whether the operation succeeded or not
+        """
+        # Allow a bool to be passed in for value of disableApiTermination
+        if attribute == 'disableApiTermination':
+            if isinstance(value, bool):
+                if value:
+                    value = 'true'
+                else:
+                    value = 'false'
+        params = {'InstanceId' : instance_id,
+                  'Attribute' : attribute,
+                  'Value' : value}
+        return self.get_status('ModifyInstanceAttribute', params, verb='POST')
+
+    def reset_instance_attribute(self, instance_id, attribute):
+        """
+        Resets an attribute of an instance to its default value.
+
+        :type instance_id: string
+        :param instance_id: ID of the instance
+
+        :type attribute: string
+        :param attribute: The attribute to reset. Valid values are:
+                          kernel|ramdisk
+
+        :rtype: bool
+        :return: Whether the operation succeeded or not
+        """
+        params = {'InstanceId' : instance_id,
+                  'Attribute' : attribute}
+        return self.get_status('ResetInstanceAttribute', params, verb='POST')
+
+    # Spot Instances
+
+    def get_all_spot_instance_requests(self, request_ids=None,
+                                       filters=None):
+        """
+        Retrieve all the spot instances requests associated with your account.
+        
+        :type request_ids: list
+        :param request_ids: A list of strings of spot instance request IDs
+        
+        :type filters: dict
+        :param filters: Optional filters that can be used to limit
+                        the results returned.  Filters are provided
+                        in the form of a dictionary consisting of
+                        filter names as the key and filter values
+                        as the value.  The set of allowable filter
+                        names/values is dependent on the request
+                        being performed.  Check the EC2 API guide
+                        for details.
+
+        :rtype: list
+        :return: A list of
+                 :class:`boto.ec2.spotinstancerequest.SpotInstanceRequest`
+        """
+        params = {}
+        if request_ids:
+            self.build_list_params(params, request_ids, 'SpotInstanceRequestId')
+        if filters:
+            self.build_filter_params(params, filters)
+        return self.get_list('DescribeSpotInstanceRequests', params,
+                             [('item', SpotInstanceRequest)], verb='POST')
+
+    def get_spot_price_history(self, start_time=None, end_time=None,
+                               instance_type=None, product_description=None):
+        """
+        Retrieve the recent history of spot instances pricing.
+        
+        :type start_time: str
+        :param start_time: An indication of how far back to provide price
+                           changes for. An ISO8601 DateTime string.
+        
+        :type end_time: str
+        :param end_time: An indication of how far forward to provide price
+                         changes for.  An ISO8601 DateTime string.
+        
+        :type instance_type: str
+        :param instance_type: Filter responses to a particular instance type.
+        
+        :type product_description: str
+        :param product_descripton: Filter responses to a particular platform.
+                                   Valid values are currently: Linux
+        
+        :rtype: list
+        :return: A list tuples containing price and timestamp.
+        """
+        params = {}
+        if start_time:
+            params['StartTime'] = start_time
+        if end_time:
+            params['EndTime'] = end_time
+        if instance_type:
+            params['InstanceType'] = instance_type
+        if product_description:
+            params['ProductDescription'] = product_description
+        return self.get_list('DescribeSpotPriceHistory', params,
+                             [('item', SpotPriceHistory)], verb='POST')
+
+    def request_spot_instances(self, price, image_id, count=1, type='one-time',
+                               valid_from=None, valid_until=None,
+                               launch_group=None, availability_zone_group=None,
+                               key_name=None, security_groups=None,
+                               user_data=None, addressing_type=None,
+                               instance_type='m1.small', placement=None,
+                               kernel_id=None, ramdisk_id=None,
+                               monitoring_enabled=False, subnet_id=None,
+                               block_device_map=None):
+        """
+        Request instances on the spot market at a particular price.
+
+        :type price: str
+        :param price: The maximum price of your bid
+        
+        :type image_id: string
+        :param image_id: The ID of the image to run
+
+        :type count: int
+        :param count: The of instances to requested
+        
+        :type type: str
+        :param type: Type of request. Can be 'one-time' or 'persistent'.
+                     Default is one-time.
+
+        :type valid_from: str
+        :param valid_from: Start date of the request. An ISO8601 time string.
+
+        :type valid_until: str
+        :param valid_until: End date of the request.  An ISO8601 time string.
+
+        :type launch_group: str
+        :param launch_group: If supplied, all requests will be fulfilled
+                             as a group.
+                             
+        :type availability_zone_group: str
+        :param availability_zone_group: If supplied, all requests will be
+                                        fulfilled within a single
+                                        availability zone.
+                             
+        :type key_name: string
+        :param key_name: The name of the key pair with which to launch instances
+
+        :type security_groups: list of strings
+        :param security_groups: The names of the security groups with which to
+                                associate instances
+
+        :type user_data: string
+        :param user_data: The user data passed to the launched instances
+
+        :type instance_type: string
+        :param instance_type: The type of instance to run:
+                              
+                              * m1.small
+                              * m1.large
+                              * m1.xlarge
+                              * c1.medium
+                              * c1.xlarge
+                              * m2.xlarge
+                              * m2.2xlarge
+                              * m2.4xlarge
+                              * cc1.4xlarge
+                              * t1.micro
+
+        :type placement: string
+        :param placement: The availability zone in which to launch the instances
+
+        :type kernel_id: string
+        :param kernel_id: The ID of the kernel with which to launch the
+                          instances
+
+        :type ramdisk_id: string
+        :param ramdisk_id: The ID of the RAM disk with which to launch the
+                           instances
+
+        :type monitoring_enabled: bool
+        :param monitoring_enabled: Enable CloudWatch monitoring on the instance.
+
+        :type subnet_id: string
+        :param subnet_id: The subnet ID within which to launch the instances
+                          for VPC.
+
+        :type block_device_map: :class:`boto.ec2.blockdevicemapping.BlockDeviceMapping`
+        :param block_device_map: A BlockDeviceMapping data structure
+                                 describing the EBS volumes associated
+                                 with the Image.
+
+        :rtype: Reservation
+        :return: The :class:`boto.ec2.spotinstancerequest.SpotInstanceRequest`
+                 associated with the request for machines
+        """
+        params = {'LaunchSpecification.ImageId':image_id,
+                  'Type' : type,
+                  'SpotPrice' : price}
+        if count:
+            params['InstanceCount'] = count
+        if valid_from:
+            params['ValidFrom'] = valid_from
+        if valid_until:
+            params['ValidUntil'] = valid_until
+        if launch_group:
+            params['LaunchGroup'] = launch_group
+        if availability_zone_group:
+            params['AvailabilityZoneGroup'] = availability_zone_group
+        if key_name:
+            params['LaunchSpecification.KeyName'] = key_name
+        if security_groups:
+            l = []
+            for group in security_groups:
+                if isinstance(group, SecurityGroup):
+                    l.append(group.name)
+                else:
+                    l.append(group)
+            self.build_list_params(params, l,
+                                   'LaunchSpecification.SecurityGroup')
+        if user_data:
+            params['LaunchSpecification.UserData'] = base64.b64encode(user_data)
+        if addressing_type:
+            params['LaunchSpecification.AddressingType'] = addressing_type
+        if instance_type:
+            params['LaunchSpecification.InstanceType'] = instance_type
+        if placement:
+            params['LaunchSpecification.Placement.AvailabilityZone'] = placement
+        if kernel_id:
+            params['LaunchSpecification.KernelId'] = kernel_id
+        if ramdisk_id:
+            params['LaunchSpecification.RamdiskId'] = ramdisk_id
+        if monitoring_enabled:
+            params['LaunchSpecification.Monitoring.Enabled'] = 'true'
+        if subnet_id:
+            params['LaunchSpecification.SubnetId'] = subnet_id
+        if block_device_map:
+            block_device_map.build_list_params(params, 'LaunchSpecification.')
+        return self.get_list('RequestSpotInstances', params,
+                             [('item', SpotInstanceRequest)],
+                             verb='POST')
+
+        
+    def cancel_spot_instance_requests(self, request_ids):
+        """
+        Cancel the specified Spot Instance Requests.
+        
+        :type request_ids: list
+        :param request_ids: A list of strings of the Request IDs to terminate
+        
+        :rtype: list
+        :return: A list of the instances terminated
+        """
+        params = {}
+        if request_ids:
+            self.build_list_params(params, request_ids, 'SpotInstanceRequestId')
+        return self.get_list('CancelSpotInstanceRequests', params,
+                             [('item', Instance)], verb='POST')
+
+    def get_spot_datafeed_subscription(self):
+        """
+        Return the current spot instance data feed subscription
+        associated with this account, if any.
+        
+        :rtype: :class:`boto.ec2.spotdatafeedsubscription.SpotDatafeedSubscription`
+        :return: The datafeed subscription object or None
+        """
+        return self.get_object('DescribeSpotDatafeedSubscription',
+                               None, SpotDatafeedSubscription, verb='POST')
+
+    def create_spot_datafeed_subscription(self, bucket, prefix):
+        """
+        Create a spot instance datafeed subscription for this account.
+
+        :type bucket: str or unicode
+        :param bucket: The name of the bucket where spot instance data
+                       will be written.  The account issuing this request
+                       must have FULL_CONTROL access to the bucket
+                       specified in the request.
+
+        :type prefix: str or unicode
+        :param prefix: An optional prefix that will be pre-pended to all
+                       data files written to the bucket.
+                       
+        :rtype: :class:`boto.ec2.spotdatafeedsubscription.SpotDatafeedSubscription`
+        :return: The datafeed subscription object or None
+        """
+        params = {'Bucket' : bucket}
+        if prefix:
+            params['Prefix'] = prefix
+        return self.get_object('CreateSpotDatafeedSubscription',
+                               params, SpotDatafeedSubscription, verb='POST')
+
+    def delete_spot_datafeed_subscription(self):
+        """
+        Delete the current spot instance data feed subscription
+        associated with this account
+        
+        :rtype: bool
+        :return: True if successful
+        """
+        return self.get_status('DeleteSpotDatafeedSubscription', None, verb='POST')
+
+    # Zone methods
+
+    def get_all_zones(self, zones=None, filters=None):
+        """
+        Get all Availability Zones associated with the current region.
+
+        :type zones: list
+        :param zones: Optional list of zones.  If this list is present,
+                      only the Zones associated with these zone names
+                      will be returned.
+
+        :type filters: dict
+        :param filters: Optional filters that can be used to limit
+                        the results returned.  Filters are provided
+                        in the form of a dictionary consisting of
+                        filter names as the key and filter values
+                        as the value.  The set of allowable filter
+                        names/values is dependent on the request
+                        being performed.  Check the EC2 API guide
+                        for details.
+
+        :rtype: list of :class:`boto.ec2.zone.Zone`
+        :return: The requested Zone objects
+        """
+        params = {}
+        if zones:
+            self.build_list_params(params, zones, 'ZoneName')
+        if filters:
+            self.build_filter_params(params, filters)
+        return self.get_list('DescribeAvailabilityZones', params, [('item', Zone)], verb='POST')
+
+    # Address methods
+
+    def get_all_addresses(self, addresses=None, filters=None):
+        """
+        Get all EIP's associated with the current credentials.
+
+        :type addresses: list
+        :param addresses: Optional list of addresses.  If this list is present,
+                           only the Addresses associated with these addresses
+                           will be returned.
+
+        :type filters: dict
+        :param filters: Optional filters that can be used to limit
+                        the results returned.  Filters are provided
+                        in the form of a dictionary consisting of
+                        filter names as the key and filter values
+                        as the value.  The set of allowable filter
+                        names/values is dependent on the request
+                        being performed.  Check the EC2 API guide
+                        for details.
+
+        :rtype: list of :class:`boto.ec2.address.Address`
+        :return: The requested Address objects
+        """
+        params = {}
+        if addresses:
+            self.build_list_params(params, addresses, 'PublicIp')
+        if filters:
+            self.build_filter_params(params, filters)
+        return self.get_list('DescribeAddresses', params, [('item', Address)], verb='POST')
+
+    def allocate_address(self):
+        """
+        Allocate a new Elastic IP address and associate it with your account.
+
+        :rtype: :class:`boto.ec2.address.Address`
+        :return: The newly allocated Address
+        """
+        return self.get_object('AllocateAddress', {}, Address, verb='POST')
+
+    def associate_address(self, instance_id, public_ip):
+        """
+        Associate an Elastic IP address with a currently running instance.
+
+        :type instance_id: string
+        :param instance_id: The ID of the instance
+
+        :type public_ip: string
+        :param public_ip: The public IP address
+
+        :rtype: bool
+        :return: True if successful
+        """
+        params = {'InstanceId' : instance_id, 'PublicIp' : public_ip}
+        return self.get_status('AssociateAddress', params, verb='POST')
+
+    def disassociate_address(self, public_ip):
+        """
+        Disassociate an Elastic IP address from a currently running instance.
+
+        :type public_ip: string
+        :param public_ip: The public IP address
+
+        :rtype: bool
+        :return: True if successful
+        """
+        params = {'PublicIp' : public_ip}
+        return self.get_status('DisassociateAddress', params, verb='POST')
+
+    def release_address(self, public_ip):
+        """
+        Free up an Elastic IP address
+
+        :type public_ip: string
+        :param public_ip: The public IP address
+
+        :rtype: bool
+        :return: True if successful
+        """
+        params = {'PublicIp' : public_ip}
+        return self.get_status('ReleaseAddress', params, verb='POST')
+
+    # Volume methods
+
+    def get_all_volumes(self, volume_ids=None, filters=None):
+        """
+        Get all Volumes associated with the current credentials.
+
+        :type volume_ids: list
+        :param volume_ids: Optional list of volume ids.  If this list is present,
+                           only the volumes associated with these volume ids
+                           will be returned.
+
+        :type filters: dict
+        :param filters: Optional filters that can be used to limit
+                        the results returned.  Filters are provided
+                        in the form of a dictionary consisting of
+                        filter names as the key and filter values
+                        as the value.  The set of allowable filter
+                        names/values is dependent on the request
+                        being performed.  Check the EC2 API guide
+                        for details.
+
+        :rtype: list of :class:`boto.ec2.volume.Volume`
+        :return: The requested Volume objects
+        """
+        params = {}
+        if volume_ids:
+            self.build_list_params(params, volume_ids, 'VolumeId')
+        if filters:
+            self.build_filter_params(params, filters)
+        return self.get_list('DescribeVolumes', params, [('item', Volume)], verb='POST')
+
+    def create_volume(self, size, zone, snapshot=None):
+        """
+        Create a new EBS Volume.
+
+        :type size: int
+        :param size: The size of the new volume, in GiB
+
+        :type zone: string or :class:`boto.ec2.zone.Zone`
+        :param zone: The availability zone in which the Volume will be created.
+
+        :type snapshot: string or :class:`boto.ec2.snapshot.Snapshot`
+        :param snapshot: The snapshot from which the new Volume will be created.
+        """
+        if isinstance(zone, Zone):
+            zone = zone.name
+        params = {'AvailabilityZone' : zone}
+        if size:
+            params['Size'] = size
+        if snapshot:
+            if isinstance(snapshot, Snapshot):
+                snapshot = snapshot.id
+            params['SnapshotId'] = snapshot
+        return self.get_object('CreateVolume', params, Volume, verb='POST')
+
+    def delete_volume(self, volume_id):
+        """
+        Delete an EBS volume.
+
+        :type volume_id: str
+        :param volume_id: The ID of the volume to be delete.
+
+        :rtype: bool
+        :return: True if successful
+        """
+        params = {'VolumeId': volume_id}
+        return self.get_status('DeleteVolume', params, verb='POST')
+
+    def attach_volume(self, volume_id, instance_id, device):
+        """
+        Attach an EBS volume to an EC2 instance.
+
+        :type volume_id: str
+        :param volume_id: The ID of the EBS volume to be attached.
+
+        :type instance_id: str
+        :param instance_id: The ID of the EC2 instance to which it will
+                            be attached.
+
+        :type device: str
+        :param device: The device on the instance through which the
+                       volume will be exposted (e.g. /dev/sdh)
+
+        :rtype: bool
+        :return: True if successful
+        """
+        params = {'InstanceId' : instance_id,
+                  'VolumeId' : volume_id,
+                  'Device' : device}
+        return self.get_status('AttachVolume', params, verb='POST')
+
+    def detach_volume(self, volume_id, instance_id=None,
+                      device=None, force=False):
+        """
+        Detach an EBS volume from an EC2 instance.
+
+        :type volume_id: str
+        :param volume_id: The ID of the EBS volume to be attached.
+
+        :type instance_id: str
+        :param instance_id: The ID of the EC2 instance from which it will
+                            be detached.
+
+        :type device: str
+        :param device: The device on the instance through which the
+                       volume is exposted (e.g. /dev/sdh)
+
+        :type force: bool
+        :param force: Forces detachment if the previous detachment attempt did
+                      not occur cleanly.  This option can lead to data loss or
+                      a corrupted file system. Use this option only as a last
+                      resort to detach a volume from a failed instance. The
+                      instance will not have an opportunity to flush file system
+                      caches nor file system meta data. If you use this option,
+                      you must perform file system check and repair procedures.
+
+        :rtype: bool
+        :return: True if successful
+        """
+        params = {'VolumeId' : volume_id}
+        if instance_id:
+            params['InstanceId'] = instance_id
+        if device:
+            params['Device'] = device
+        if force:
+            params['Force'] = 'true'
+        return self.get_status('DetachVolume', params, verb='POST')
+
+    # Snapshot methods
+
+    def get_all_snapshots(self, snapshot_ids=None,
+                          owner=None, restorable_by=None,
+                          filters=None):
+        """
+        Get all EBS Snapshots associated with the current credentials.
+
+        :type snapshot_ids: list
+        :param snapshot_ids: Optional list of snapshot ids.  If this list is
+                             present, only the Snapshots associated with
+                             these snapshot ids will be returned.
+
+        :type owner: str
+        :param owner: If present, only the snapshots owned by the specified user
+                      will be returned.  Valid values are:
+                      
+                      * self
+                      * amazon
+                      * AWS Account ID
+
+        :type restorable_by: str
+        :param restorable_by: If present, only the snapshots that are restorable
+                              by the specified account id will be returned.
+
+        :type filters: dict
+        :param filters: Optional filters that can be used to limit
+                        the results returned.  Filters are provided
+                        in the form of a dictionary consisting of
+                        filter names as the key and filter values
+                        as the value.  The set of allowable filter
+                        names/values is dependent on the request
+                        being performed.  Check the EC2 API guide
+                        for details.
+
+        :rtype: list of :class:`boto.ec2.snapshot.Snapshot`
+        :return: The requested Snapshot objects
+        """
+        params = {}
+        if snapshot_ids:
+            self.build_list_params(params, snapshot_ids, 'SnapshotId')
+        if owner:
+            params['Owner'] = owner
+        if restorable_by:
+            params['RestorableBy'] = restorable_by
+        if filters:
+            self.build_filter_params(params, filters)
+        return self.get_list('DescribeSnapshots', params, [('item', Snapshot)], verb='POST')
+
+    def create_snapshot(self, volume_id, description=None):
+        """
+        Create a snapshot of an existing EBS Volume.
+
+        :type volume_id: str
+        :param volume_id: The ID of the volume to be snapshot'ed
+
+        :type description: str
+        :param description: A description of the snapshot.
+                            Limited to 255 characters.
+
+        :rtype: bool
+        :return: True if successful
+        """
+        params = {'VolumeId' : volume_id}
+        if description:
+            params['Description'] = description[0:255]
+        snapshot = self.get_object('CreateSnapshot', params, Snapshot, verb='POST')
+        volume = self.get_all_volumes([volume_id])[0]
+        volume_name = volume.tags.get('Name')
+        if volume_name:
+            snapshot.add_tag('Name', volume_name)
+        return snapshot
+
+    def delete_snapshot(self, snapshot_id):
+        params = {'SnapshotId': snapshot_id}
+        return self.get_status('DeleteSnapshot', params, verb='POST')
+
+    def trim_snapshots(self, hourly_backups = 8, daily_backups = 7, weekly_backups = 4):
+        """
+        Trim excess snapshots, based on when they were taken. More current snapshots are 
+        retained, with the number retained decreasing as you move back in time.
+
+        If ebs volumes have a 'Name' tag with a value, their snapshots will be assigned the same 
+        tag when they are created. The values of the 'Name' tags for snapshots are used by this
+        function to group snapshots taken from the same volume (or from a series of like-named
+        volumes over time) for trimming.
+
+        For every group of like-named snapshots, this function retains the newest and oldest 
+        snapshots, as well as, by default,  the first snapshots taken in each of the last eight 
+        hours, the first snapshots taken in each of the last seven days, the first snapshots 
+        taken in the last 4 weeks (counting Midnight Sunday morning as the start of the week), 
+        and the first snapshot from the first Sunday of each month forever.
+
+        :type hourly_backups: int
+        :param hourly_backups: How many recent hourly backups should be saved.
+
+        :type daily_backups: int
+        :param daily_backups: How many recent daily backups should be saved.
+
+        :type weekly_backups: int
+        :param weekly_backups: How many recent weekly backups should be saved.
+        """
+
+        # This function first builds up an ordered list of target times that snapshots should be saved for 
+        # (last 8 hours, last 7 days, etc.). Then a map of snapshots is constructed, with the keys being
+        # the snapshot / volume names and the values being arrays of chornologically sorted snapshots.
+        # Finally, for each array in the map, we go through the snapshot array and the target time array
+        # in an interleaved fashion, deleting snapshots whose start_times don't immediately follow a
+        # target time (we delete a snapshot if there's another snapshot that was made closer to the
+        # preceding target time).
+
+        now = datetime.utcnow() # work with UTC time, which is what the snapshot start time is reported in
+        last_hour = datetime(now.year, now.month, now.day, now.hour)
+        last_midnight = datetime(now.year, now.month, now.day)
+        last_sunday = datetime(now.year, now.month, now.day) - timedelta(days = (now.weekday() + 1) % 7)
+        start_of_month = datetime(now.year, now.month, 1)
+
+        target_backup_times = []
+
+        oldest_snapshot_date = datetime(2007, 1, 1) # there are no snapshots older than 1/1/2007
+
+        for hour in range(0, hourly_backups):
+            target_backup_times.append(last_hour - timedelta(hours = hour))
+
+        for day in range(0, daily_backups):
+            target_backup_times.append(last_midnight - timedelta(days = day))
+
+        for week in range(0, weekly_backups):
+            target_backup_times.append(last_sunday - timedelta(weeks = week))
+
+        one_day = timedelta(days = 1)
+        while start_of_month > oldest_snapshot_date:
+            # append the start of the month to the list of snapshot dates to save:
+            target_backup_times.append(start_of_month)
+            # there's no timedelta setting for one month, so instead:
+            # decrement the day by one, so we go to the final day of the previous month...
+            start_of_month -= one_day
+            # ... and then go to the first day of that previous month:
+            start_of_month = datetime(start_of_month.year, start_of_month.month, 1)
+
+        temp = []
+
+        for t in target_backup_times:
+            if temp.__contains__(t) == False:
+                temp.append(t)
+
+        target_backup_times = temp
+        target_backup_times.reverse() # make the oldest date first
+
+        # get all the snapshots, sort them by date and time, and organize them into one array for each volume:
+        all_snapshots = self.get_all_snapshots(owner = 'self')
+        all_snapshots.sort(cmp = lambda x, y: cmp(x.start_time, y.start_time)) # oldest first
+        snaps_for_each_volume = {}
+        for snap in all_snapshots:
+            # the snapshot name and the volume name are the same. The snapshot name is set from the volume
+            # name at the time the snapshot is taken
+            volume_name = snap.tags.get('Name')
+            if volume_name:
+                # only examine snapshots that have a volume name
+                snaps_for_volume = snaps_for_each_volume.get(volume_name)
+                if not snaps_for_volume:
+                    snaps_for_volume = []
+                    snaps_for_each_volume[volume_name] = snaps_for_volume
+                snaps_for_volume.append(snap)
+
+        # Do a running comparison of snapshot dates to desired time periods, keeping the oldest snapshot in each
+        # time period and deleting the rest:
+        for volume_name in snaps_for_each_volume:
+            snaps = snaps_for_each_volume[volume_name]
+            snaps = snaps[:-1] # never delete the newest snapshot, so remove it from consideration
+            time_period_number = 0
+            snap_found_for_this_time_period = False
+            for snap in snaps:
+                check_this_snap = True
+                while check_this_snap and time_period_number < target_backup_times.__len__():
+                    snap_date = datetime.strptime(snap.start_time, '%Y-%m-%dT%H:%M:%S.000Z')
+                    if snap_date < target_backup_times[time_period_number]:
+                        # the snap date is before the cutoff date. Figure out if it's the first snap in this
+                        # date range and act accordingly (since both date the date ranges and the snapshots
+                        # are sorted chronologically, we know this snapshot isn't in an earlier date range):
+                        if snap_found_for_this_time_period == True:
+                            if not snap.tags.get('preserve_snapshot'):
+                                # as long as the snapshot wasn't marked with the 'preserve_snapshot' tag, delete it:
+                                self.delete_snapshot(snap.id)
+                                boto.log.info('Trimmed snapshot %s (%s)' % (snap.tags['Name'], snap.start_time))
+                            # go on and look at the next snapshot, leaving the time period alone
+                        else:
+                            # this was the first snapshot found for this time period. Leave it alone and look at the 
+                            # next snapshot:
+                            snap_found_for_this_time_period = True
+                        check_this_snap = False
+                    else:
+                        # the snap is after the cutoff date. Check it against the next cutoff date
+                        time_period_number += 1
+                        snap_found_for_this_time_period = False
+
+
+    def get_snapshot_attribute(self, snapshot_id,
+                               attribute='createVolumePermission'):
+        """
+        Get information about an attribute of a snapshot.  Only one attribute
+        can be specified per call.
+
+        :type snapshot_id: str
+        :param snapshot_id: The ID of the snapshot.
+
+        :type attribute: str
+        :param attribute: The requested attribute.  Valid values are:
+        
+                          * createVolumePermission
+
+        :rtype: list of :class:`boto.ec2.snapshotattribute.SnapshotAttribute`
+        :return: The requested Snapshot attribute
+        """
+        params = {'Attribute' : attribute}
+        if snapshot_id:
+            params['SnapshotId'] = snapshot_id
+        return self.get_object('DescribeSnapshotAttribute', params,
+                               SnapshotAttribute, verb='POST')
+
+    def modify_snapshot_attribute(self, snapshot_id,
+                                  attribute='createVolumePermission',
+                                  operation='add', user_ids=None, groups=None):
+        """
+        Changes an attribute of an image.
+
+        :type snapshot_id: string
+        :param snapshot_id: The snapshot id you wish to change
+
+        :type attribute: string
+        :param attribute: The attribute you wish to change.  Valid values are:
+                          createVolumePermission
+
+        :type operation: string
+        :param operation: Either add or remove (this is required for changing
+                          snapshot ermissions)
+
+        :type user_ids: list
+        :param user_ids: The Amazon IDs of users to add/remove attributes
+
+        :type groups: list
+        :param groups: The groups to add/remove attributes.  The only valid
+                       value at this time is 'all'.
+
+        """
+        params = {'SnapshotId' : snapshot_id,
+                  'Attribute' : attribute,
+                  'OperationType' : operation}
+        if user_ids:
+            self.build_list_params(params, user_ids, 'UserId')
+        if groups:
+            self.build_list_params(params, groups, 'UserGroup')
+        return self.get_status('ModifySnapshotAttribute', params, verb='POST')
+
+    def reset_snapshot_attribute(self, snapshot_id,
+                                 attribute='createVolumePermission'):
+        """
+        Resets an attribute of a snapshot to its default value.
+
+        :type snapshot_id: string
+        :param snapshot_id: ID of the snapshot
+
+        :type attribute: string
+        :param attribute: The attribute to reset
+
+        :rtype: bool
+        :return: Whether the operation succeeded or not
+        """
+        params = {'SnapshotId' : snapshot_id,
+                  'Attribute' : attribute}
+        return self.get_status('ResetSnapshotAttribute', params, verb='POST')
+
+    # Keypair methods
+
+    def get_all_key_pairs(self, keynames=None, filters=None):
+        """
+        Get all key pairs associated with your account.
+
+        :type keynames: list
+        :param keynames: A list of the names of keypairs to retrieve.
+                         If not provided, all key pairs will be returned.
+
+        :type filters: dict
+        :param filters: Optional filters that can be used to limit
+                        the results returned.  Filters are provided
+                        in the form of a dictionary consisting of
+                        filter names as the key and filter values
+                        as the value.  The set of allowable filter
+                        names/values is dependent on the request
+                        being performed.  Check the EC2 API guide
+                        for details.
+
+        :rtype: list
+        :return: A list of :class:`boto.ec2.keypair.KeyPair`
+        """
+        params = {}
+        if keynames:
+            self.build_list_params(params, keynames, 'KeyName')
+        if filters:
+            self.build_filter_params(params, filters)
+        return self.get_list('DescribeKeyPairs', params, [('item', KeyPair)], verb='POST')
+
+    def get_key_pair(self, keyname):
+        """
+        Convenience method to retrieve a specific keypair (KeyPair).
+
+        :type image_id: string
+        :param image_id: the ID of the Image to retrieve
+
+        :rtype: :class:`boto.ec2.keypair.KeyPair`
+        :return: The KeyPair specified or None if it is not found
+        """
+        try:
+            return self.get_all_key_pairs(keynames=[keyname])[0]
+        except IndexError: # None of those key pairs available
+            return None
+
+    def create_key_pair(self, key_name):
+        """
+        Create a new key pair for your account.
+        This will create the key pair within the region you
+        are currently connected to.
+
+        :type key_name: string
+        :param key_name: The name of the new keypair
+
+        :rtype: :class:`boto.ec2.keypair.KeyPair`
+        :return: The newly created :class:`boto.ec2.keypair.KeyPair`.
+                 The material attribute of the new KeyPair object
+                 will contain the the unencrypted PEM encoded RSA private key.
+        """
+        params = {'KeyName':key_name}
+        return self.get_object('CreateKeyPair', params, KeyPair, verb='POST')
+
+    def delete_key_pair(self, key_name):
+        """
+        Delete a key pair from your account.
+
+        :type key_name: string
+        :param key_name: The name of the keypair to delete
+        """
+        params = {'KeyName':key_name}
+        return self.get_status('DeleteKeyPair', params, verb='POST')
+
+    def import_key_pair(self, key_name, public_key_material):
+        """
+        mports the public key from an RSA key pair that you created
+        with a third-party tool.
+
+        Supported formats:
+
+        * OpenSSH public key format (e.g., the format
+          in ~/.ssh/authorized_keys)
+
+        * Base64 encoded DER format
+
+        * SSH public key file format as specified in RFC4716
+
+        DSA keys are not supported. Make sure your key generator is
+        set up to create RSA keys.
+
+        Supported lengths: 1024, 2048, and 4096.
+
+        :type key_name: string
+        :param key_name: The name of the new keypair
+
+        :type public_key_material: string
+        :param public_key_material: The public key. You must base64 encode
+                                    the public key material before sending
+                                    it to AWS.
+
+        :rtype: :class:`boto.ec2.keypair.KeyPair`
+        :return: The newly created :class:`boto.ec2.keypair.KeyPair`.
+                 The material attribute of the new KeyPair object
+                 will contain the the unencrypted PEM encoded RSA private key.
+        """
+        params = {'KeyName' : key_name,
+                  'PublicKeyMaterial' : public_key_material}
+        return self.get_object('ImportKeyPair', params, KeyPair, verb='POST')
+
+    # SecurityGroup methods
+
+    def get_all_security_groups(self, groupnames=None, filters=None):
+        """
+        Get all security groups associated with your account in a region.
+
+        :type groupnames: list
+        :param groupnames: A list of the names of security groups to retrieve.
+                           If not provided, all security groups will be
+                           returned.
+
+        :type filters: dict
+        :param filters: Optional filters that can be used to limit
+                        the results returned.  Filters are provided
+                        in the form of a dictionary consisting of
+                        filter names as the key and filter values
+                        as the value.  The set of allowable filter
+                        names/values is dependent on the request
+                        being performed.  Check the EC2 API guide
+                        for details.
+
+        :rtype: list
+        :return: A list of :class:`boto.ec2.securitygroup.SecurityGroup`
+        """
+        params = {}
+        if groupnames:
+            self.build_list_params(params, groupnames, 'GroupName')
+        if filters:
+            self.build_filter_params(params, filters)
+        return self.get_list('DescribeSecurityGroups', params,
+                             [('item', SecurityGroup)], verb='POST')
+
+    def create_security_group(self, name, description):
+        """
+        Create a new security group for your account.
+        This will create the security group within the region you
+        are currently connected to.
+
+        :type name: string
+        :param name: The name of the new security group
+
+        :type description: string
+        :param description: The description of the new security group
+
+        :rtype: :class:`boto.ec2.securitygroup.SecurityGroup`
+        :return: The newly created :class:`boto.ec2.keypair.KeyPair`.
+        """
+        params = {'GroupName':name, 'GroupDescription':description}
+        group = self.get_object('CreateSecurityGroup', params, SecurityGroup, verb='POST')
+        group.name = name
+        group.description = description
+        return group
+
+    def delete_security_group(self, name):
+        """
+        Delete a security group from your account.
+
+        :type key_name: string
+        :param key_name: The name of the keypair to delete
+        """
+        params = {'GroupName':name}
+        return self.get_status('DeleteSecurityGroup', params, verb='POST')
+
+    def _authorize_deprecated(self, group_name, src_security_group_name=None,
+                              src_security_group_owner_id=None):
+        """
+        This method is called only when someone tries to authorize a group
+        without specifying a from_port or to_port.  Until recently, that was
+        the only way to do group authorization but the EC2 API has been
+        changed to now require a from_port and to_port when specifying a
+        group.  This is a much better approach but I don't want to break
+        existing boto applications that depend on the old behavior, hence
+        this kludge.
+
+        :type group_name: string
+        :param group_name: The name of the security group you are adding
+                           the rule to.
+
+        :type src_security_group_name: string
+        :param src_security_group_name: The name of the security group you are
+                                        granting access to.
+
+        :type src_security_group_owner_id: string
+        :param src_security_group_owner_id: The ID of the owner of the security
+                                            group you are granting access to.
+
+        :rtype: bool
+        :return: True if successful.
+        """
+        warnings.warn('FromPort and ToPort now required for group authorization',
+                      DeprecationWarning)
+        params = {'GroupName':group_name}
+        if src_security_group_name:
+            params['SourceSecurityGroupName'] = src_security_group_name
+        if src_security_group_owner_id:
+            params['SourceSecurityGroupOwnerId'] = src_security_group_owner_id
+        return self.get_status('AuthorizeSecurityGroupIngress', params, verb='POST')
+
+    def authorize_security_group(self, group_name, src_security_group_name=None,
+                                 src_security_group_owner_id=None,
+                                 ip_protocol=None, from_port=None, to_port=None,
+                                 cidr_ip=None):
+        """
+        Add a new rule to an existing security group.
+        You need to pass in either src_security_group_name and
+        src_security_group_owner_id OR ip_protocol, from_port, to_port,
+        and cidr_ip.  In other words, either you are authorizing another
+        group or you are authorizing some ip-based rule.
+
+        :type group_name: string
+        :param group_name: The name of the security group you are adding
+                           the rule to.
+
+        :type src_security_group_name: string
+        :param src_security_group_name: The name of the security group you are
+                                        granting access to.
+
+        :type src_security_group_owner_id: string
+        :param src_security_group_owner_id: The ID of the owner of the security
+                                            group you are granting access to.
+
+        :type ip_protocol: string
+        :param ip_protocol: Either tcp | udp | icmp
+
+        :type from_port: int
+        :param from_port: The beginning port number you are enabling
+
+        :type to_port: int
+        :param to_port: The ending port number you are enabling
+
+        :type cidr_ip: string
+        :param cidr_ip: The CIDR block you are providing access to.
+                        See http://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing
+
+        :rtype: bool
+        :return: True if successful.
+        """
+        if src_security_group_name:
+            if from_port is None and to_port is None and ip_protocol is None:
+                return self._authorize_deprecated(group_name,
+                                                  src_security_group_name,
+                                                  src_security_group_owner_id)
+        params = {'GroupName':group_name}
+        if src_security_group_name:
+            params['IpPermissions.1.Groups.1.GroupName'] = src_security_group_name
+        if src_security_group_owner_id:
+            params['IpPermissions.1.Groups.1.UserId'] = src_security_group_owner_id
+        if ip_protocol:
+            params['IpPermissions.1.IpProtocol'] = ip_protocol
+        if from_port:
+            params['IpPermissions.1.FromPort'] = from_port
+        if to_port:
+            params['IpPermissions.1.ToPort'] = to_port
+        if cidr_ip:
+            params['IpPermissions.1.IpRanges.1.CidrIp'] = cidr_ip
+        return self.get_status('AuthorizeSecurityGroupIngress', params, verb='POST')
+
+    def _revoke_deprecated(self, group_name, src_security_group_name=None,
+                           src_security_group_owner_id=None):
+        """
+        This method is called only when someone tries to revoke a group
+        without specifying a from_port or to_port.  Until recently, that was
+        the only way to do group revocation but the EC2 API has been
+        changed to now require a from_port and to_port when specifying a
+        group.  This is a much better approach but I don't want to break
+        existing boto applications that depend on the old behavior, hence
+        this kludge.
+
+        :type group_name: string
+        :param group_name: The name of the security group you are adding
+                           the rule to.
+
+        :type src_security_group_name: string
+        :param src_security_group_name: The name of the security group you are
+                                        granting access to.
+
+        :type src_security_group_owner_id: string
+        :param src_security_group_owner_id: The ID of the owner of the security
+                                            group you are granting access to.
+
+        :rtype: bool
+        :return: True if successful.
+        """
+        warnings.warn('FromPort and ToPort now required for group authorization',
+                      DeprecationWarning)
+        params = {'GroupName':group_name}
+        if src_security_group_name:
+            params['SourceSecurityGroupName'] = src_security_group_name
+        if src_security_group_owner_id:
+            params['SourceSecurityGroupOwnerId'] = src_security_group_owner_id
+        return self.get_status('RevokeSecurityGroupIngress', params, verb='POST')
+
+    def revoke_security_group(self, group_name, src_security_group_name=None,
+                              src_security_group_owner_id=None,
+                              ip_protocol=None, from_port=None, to_port=None,
+                              cidr_ip=None):
+        """
+        Remove an existing rule from an existing security group.
+        You need to pass in either src_security_group_name and
+        src_security_group_owner_id OR ip_protocol, from_port, to_port,
+        and cidr_ip.  In other words, either you are revoking another
+        group or you are revoking some ip-based rule.
+
+        :type group_name: string
+        :param group_name: The name of the security group you are removing
+                           the rule from.
+
+        :type src_security_group_name: string
+        :param src_security_group_name: The name of the security group you are
+                                        revoking access to.
+
+        :type src_security_group_owner_id: string
+        :param src_security_group_owner_id: The ID of the owner of the security
+                                            group you are revoking access to.
+
+        :type ip_protocol: string
+        :param ip_protocol: Either tcp | udp | icmp
+
+        :type from_port: int
+        :param from_port: The beginning port number you are disabling
+
+        :type to_port: int
+        :param to_port: The ending port number you are disabling
+
+        :type cidr_ip: string
+        :param cidr_ip: The CIDR block you are revoking access to.
+                        See http://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing
+
+        :rtype: bool
+        :return: True if successful.
+        """
+        if src_security_group_name:
+            if from_port is None and to_port is None and ip_protocol is None:
+                return self._revoke_deprecated(group_name,
+                                               src_security_group_name,
+                                               src_security_group_owner_id)
+        params = {'GroupName':group_name}
+        if src_security_group_name:
+            params['IpPermissions.1.Groups.1.GroupName'] = src_security_group_name
+        if src_security_group_owner_id:
+            params['IpPermissions.1.Groups.1.UserId'] = src_security_group_owner_id
+        if ip_protocol:
+            params['IpPermissions.1.IpProtocol'] = ip_protocol
+        if from_port:
+            params['IpPermissions.1.FromPort'] = from_port
+        if to_port:
+            params['IpPermissions.1.ToPort'] = to_port
+        if cidr_ip:
+            params['IpPermissions.1.IpRanges.1.CidrIp'] = cidr_ip
+        return self.get_status('RevokeSecurityGroupIngress', params, verb='POST')
+
+    #
+    # Regions
+    #
+
+    def get_all_regions(self, region_names=None, filters=None):
+        """
+        Get all available regions for the EC2 service.
+
+        :type region_names: list of str
+        :param region_names: Names of regions to limit output
+
+        :type filters: dict
+        :param filters: Optional filters that can be used to limit
+                        the results returned.  Filters are provided
+                        in the form of a dictionary consisting of
+                        filter names as the key and filter values
+                        as the value.  The set of allowable filter
+                        names/values is dependent on the request
+                        being performed.  Check the EC2 API guide
+                        for details.
+
+        :rtype: list
+        :return: A list of :class:`boto.ec2.regioninfo.RegionInfo`
+        """
+        params = {}
+        if region_names:
+            self.build_list_params(params, region_names, 'RegionName')
+        if filters:
+            self.build_filter_params(params, filters)
+        regions =  self.get_list('DescribeRegions', params, [('item', RegionInfo)], verb='POST')
+        for region in regions:
+            region.connection_cls = EC2Connection
+        return regions
+
+    #
+    # Reservation methods
+    #
+
+    def get_all_reserved_instances_offerings(self, reserved_instances_id=None,
+                                             instance_type=None,
+                                             availability_zone=None,
+                                             product_description=None,
+                                             filters=None):
+        """
+        Describes Reserved Instance offerings that are available for purchase.
+
+        :type reserved_instances_id: str
+        :param reserved_instances_id: Displays Reserved Instances with the
+                                      specified offering IDs.
+
+        :type instance_type: str
+        :param instance_type: Displays Reserved Instances of the specified
+                              instance type.
+
+        :type availability_zone: str
+        :param availability_zone: Displays Reserved Instances within the
+                                  specified Availability Zone.
+
+        :type product_description: str
+        :param product_description: Displays Reserved Instances with the
+                                    specified product description.
+
+        :type filters: dict
+        :param filters: Optional filters that can be used to limit
+                        the results returned.  Filters are provided
+                        in the form of a dictionary consisting of
+                        filter names as the key and filter values
+                        as the value.  The set of allowable filter
+                        names/values is dependent on the request
+                        being performed.  Check the EC2 API guide
+                        for details.
+
+        :rtype: list
+        :return: A list of :class:`boto.ec2.reservedinstance.ReservedInstancesOffering`
+        """
+        params = {}
+        if reserved_instances_id:
+            params['ReservedInstancesId'] = reserved_instances_id
+        if instance_type:
+            params['InstanceType'] = instance_type
+        if availability_zone:
+            params['AvailabilityZone'] = availability_zone
+        if product_description:
+            params['ProductDescription'] = product_description
+        if filters:
+            self.build_filter_params(params, filters)
+
+        return self.get_list('DescribeReservedInstancesOfferings',
+                             params, [('item', ReservedInstancesOffering)], verb='POST')
+
+    def get_all_reserved_instances(self, reserved_instances_id=None,
+                                   filters=None):
+        """
+        Describes Reserved Instance offerings that are available for purchase.
+
+        :type reserved_instance_ids: list
+        :param reserved_instance_ids: A list of the reserved instance ids that
+                                      will be returned. If not provided, all
+                                      reserved instances will be returned.
+
+        :type filters: dict
+        :param filters: Optional filters that can be used to limit
+                        the results returned.  Filters are provided
+                        in the form of a dictionary consisting of
+                        filter names as the key and filter values
+                        as the value.  The set of allowable filter
+                        names/values is dependent on the request
+                        being performed.  Check the EC2 API guide
+                        for details.
+
+        :rtype: list
+        :return: A list of :class:`boto.ec2.reservedinstance.ReservedInstance`
+        """
+        params = {}
+        if reserved_instances_id:
+            self.build_list_params(params, reserved_instances_id,
+                                   'ReservedInstancesId')
+        if filters:
+            self.build_filter_params(params, filters)
+        return self.get_list('DescribeReservedInstances',
+                             params, [('item', ReservedInstance)], verb='POST')
+
+    def purchase_reserved_instance_offering(self, reserved_instances_offering_id,
+                                            instance_count=1):
+        """
+        Purchase a Reserved Instance for use with your account.
+        ** CAUTION **
+        This request can result in large amounts of money being charged to your
+        AWS account.  Use with caution!
+
+        :type reserved_instances_offering_id: string
+        :param reserved_instances_offering_id: The offering ID of the Reserved
+                                               Instance to purchase
+
+        :type instance_count: int
+        :param instance_count: The number of Reserved Instances to purchase.
+                               Default value is 1.
+
+        :rtype: :class:`boto.ec2.reservedinstance.ReservedInstance`
+        :return: The newly created Reserved Instance
+        """
+        params = {'ReservedInstancesOfferingId' : reserved_instances_offering_id,
+                  'InstanceCount' : instance_count}
+        return self.get_object('PurchaseReservedInstancesOffering', params,
+                               ReservedInstance, verb='POST')
+
+    #
+    # Monitoring
+    #
+
+    def monitor_instance(self, instance_id):
+        """
+        Enable CloudWatch monitoring for the supplied instance.
+
+        :type instance_id: string
+        :param instance_id: The instance id
+
+        :rtype: list
+        :return: A list of :class:`boto.ec2.instanceinfo.InstanceInfo`
+        """
+        params = {'InstanceId' : instance_id}
+        return self.get_list('MonitorInstances', params,
+                             [('item', InstanceInfo)], verb='POST')
+
+    def unmonitor_instance(self, instance_id):
+        """
+        Disable CloudWatch monitoring for the supplied instance.
+
+        :type instance_id: string
+        :param instance_id: The instance id
+
+        :rtype: list
+        :return: A list of :class:`boto.ec2.instanceinfo.InstanceInfo`
+        """
+        params = {'InstanceId' : instance_id}
+        return self.get_list('UnmonitorInstances', params,
+                             [('item', InstanceInfo)], verb='POST')
+
+    # 
+    # Bundle Windows Instances
+    #
+
+    def bundle_instance(self, instance_id,
+                        s3_bucket, 
+                        s3_prefix,
+                        s3_upload_policy):
+        """
+        Bundle Windows instance.
+
+        :type instance_id: string
+        :param instance_id: The instance id
+
+        :type s3_bucket: string
+        :param s3_bucket: The bucket in which the AMI should be stored.
+
+        :type s3_prefix: string
+        :param s3_prefix: The beginning of the file name for the AMI.
+
+        :type s3_upload_policy: string
+        :param s3_upload_policy: Base64 encoded policy that specifies condition
+                                 and permissions for Amazon EC2 to upload the
+                                 user's image into Amazon S3.
+        """
+
+        params = {'InstanceId' : instance_id,
+                  'Storage.S3.Bucket' : s3_bucket,
+                  'Storage.S3.Prefix' : s3_prefix,
+                  'Storage.S3.UploadPolicy' : s3_upload_policy}
+        s3auth = boto.auth.get_auth_handler(None, boto.config, self.provider, ['s3'])
+        params['Storage.S3.AWSAccessKeyId'] = self.aws_access_key_id
+        signature = s3auth.sign_string(s3_upload_policy)
+        params['Storage.S3.UploadPolicySignature'] = signature
+        return self.get_object('BundleInstance', params, BundleInstanceTask, verb='POST') 
+
+    def get_all_bundle_tasks(self, bundle_ids=None, filters=None):
+        """
+        Retrieve current bundling tasks. If no bundle id is specified, all
+        tasks are retrieved.
+
+        :type bundle_ids: list
+        :param bundle_ids: A list of strings containing identifiers for 
+                           previously created bundling tasks.
+                           
+        :type filters: dict
+        :param filters: Optional filters that can be used to limit
+                        the results returned.  Filters are provided
+                        in the form of a dictionary consisting of
+                        filter names as the key and filter values
+                        as the value.  The set of allowable filter
+                        names/values is dependent on the request
+                        being performed.  Check the EC2 API guide
+                        for details.
+
+        """
+ 
+        params = {}
+        if bundle_ids:
+            self.build_list_params(params, bundle_ids, 'BundleId')
+        if filters:
+            self.build_filter_params(params, filters)
+        return self.get_list('DescribeBundleTasks', params,
+                             [('item', BundleInstanceTask)], verb='POST')
+
+    def cancel_bundle_task(self, bundle_id):
+        """
+        Cancel a previously submitted bundle task
+ 
+        :type bundle_id: string
+        :param bundle_id: The identifier of the bundle task to cancel.
+        """                        
+
+        params = {'BundleId' : bundle_id}
+        return self.get_object('CancelBundleTask', params, BundleInstanceTask, verb='POST')
+
+    def get_password_data(self, instance_id):
+        """
+        Get encrypted administrator password for a Windows instance.
+
+        :type instance_id: string
+        :param instance_id: The identifier of the instance to retrieve the
+                            password for.
+        """
+
+        params = {'InstanceId' : instance_id}
+        rs = self.get_object('GetPasswordData', params, ResultSet, verb='POST')
+        return rs.passwordData
+
+    # 
+    # Cluster Placement Groups
+    #
+
+    def get_all_placement_groups(self, groupnames=None, filters=None):
+        """
+        Get all placement groups associated with your account in a region.
+
+        :type groupnames: list
+        :param groupnames: A list of the names of placement groups to retrieve.
+                           If not provided, all placement groups will be
+                           returned.
+
+        :type filters: dict
+        :param filters: Optional filters that can be used to limit
+                        the results returned.  Filters are provided
+                        in the form of a dictionary consisting of
+                        filter names as the key and filter values
+                        as the value.  The set of allowable filter
+                        names/values is dependent on the request
+                        being performed.  Check the EC2 API guide
+                        for details.
+
+        :rtype: list
+        :return: A list of :class:`boto.ec2.placementgroup.PlacementGroup`
+        """
+        params = {}
+        if groupnames:
+            self.build_list_params(params, groupnames, 'GroupName')
+        if filters:
+            self.build_filter_params(params, filters)
+        return self.get_list('DescribePlacementGroups', params,
+                             [('item', PlacementGroup)], verb='POST')
+
+    def create_placement_group(self, name, strategy='cluster'):
+        """
+        Create a new placement group for your account.
+        This will create the placement group within the region you
+        are currently connected to.
+
+        :type name: string
+        :param name: The name of the new placement group
+
+        :type strategy: string
+        :param strategy: The placement strategy of the new placement group.
+                         Currently, the only acceptable value is "cluster".
+
+        :rtype: :class:`boto.ec2.placementgroup.PlacementGroup`
+        :return: The newly created :class:`boto.ec2.keypair.KeyPair`.
+        """
+        params = {'GroupName':name, 'Strategy':strategy}
+        group = self.get_status('CreatePlacementGroup', params, verb='POST')
+        return group
+
+    def delete_placement_group(self, name):
+        """
+        Delete a placement group from your account.
+
+        :type key_name: string
+        :param key_name: The name of the keypair to delete
+        """
+        params = {'GroupName':name}
+        return self.get_status('DeletePlacementGroup', params, verb='POST')
+
+    # Tag methods
+
+    def build_tag_param_list(self, params, tags):
+        keys = tags.keys()
+        keys.sort()
+        i = 1
+        for key in keys:
+            value = tags[key]
+            params['Tag.%d.Key'%i] = key
+            if value is not None:
+                params['Tag.%d.Value'%i] = value
+            i += 1
+        
+    def get_all_tags(self, tags=None, filters=None):
+        """
+        Retrieve all the metadata tags associated with your account.
+
+        :type tags: list
+        :param tags: A list of mumble
+
+        :type filters: dict
+        :param filters: Optional filters that can be used to limit
+                        the results returned.  Filters are provided
+                        in the form of a dictionary consisting of
+                        filter names as the key and filter values
+                        as the value.  The set of allowable filter
+                        names/values is dependent on the request
+                        being performed.  Check the EC2 API guide
+                        for details.
+
+        :rtype: dict
+        :return: A dictionary containing metadata tags
+        """
+        params = {}
+        if tags:
+            self.build_list_params(params, instance_ids, 'InstanceId')
+        if filters:
+            self.build_filter_params(params, filters)
+        return self.get_list('DescribeTags', params, [('item', Tag)], verb='POST')
+
+    def create_tags(self, resource_ids, tags):
+        """
+        Create new metadata tags for the specified resource ids.
+
+        :type resource_ids: list
+        :param resource_ids: List of strings
+
+        :type tags: dict
+        :param tags: A dictionary containing the name/value pairs
+
+        """
+        params = {}
+        self.build_list_params(params, resource_ids, 'ResourceId')
+        self.build_tag_param_list(params, tags)
+        return self.get_status('CreateTags', params, verb='POST')
+
+    def delete_tags(self, resource_ids, tags):
+        """
+        Delete metadata tags for the specified resource ids.
+
+        :type resource_ids: list
+        :param resource_ids: List of strings
+
+        :type tags: dict or list
+        :param tags: Either a dictionary containing name/value pairs
+                     or a list containing just tag names.
+                     If you pass in a dictionary, the values must
+                     match the actual tag values or the tag will
+                     not be deleted.
+
+        """
+        if isinstance(tags, list):
+            tags = {}.fromkeys(tags, None)
+        params = {}
+        self.build_list_params(params, resource_ids, 'ResourceId')
+        self.build_tag_param_list(params, tags)
+        return self.get_status('DeleteTags', params, verb='POST')
+
diff --git a/boto/ec2/ec2object.py b/boto/ec2/ec2object.py
new file mode 100644
index 0000000..6e37596
--- /dev/null
+++ b/boto/ec2/ec2object.py
@@ -0,0 +1,102 @@
+# Copyright (c) 2006-2010 Mitch Garnaat http://garnaat.org/
+# Copyright (c) 2010, Eucalyptus Systems, Inc.
+#
+# 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.
+
+"""
+Represents an EC2 Object
+"""
+from boto.ec2.tag import TagSet
+
+class EC2Object(object):
+
+    def __init__(self, connection=None):
+        self.connection = connection
+        if self.connection and hasattr(self.connection, 'region'):
+            self.region = connection.region
+        else:
+            self.region = None
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        setattr(self, name, value)
+
+    
+class TaggedEC2Object(EC2Object):
+    """
+    Any EC2 resource that can be tagged should be represented
+    by a Python object that subclasses this class.  This class
+    has the mechanism in place to handle the tagSet element in
+    the Describe* responses.  If tags are found, it will create
+    a TagSet object and allow it to parse and collect the tags
+    into a dict that is stored in the "tags" attribute of the
+    object.
+    """
+
+    def __init__(self, connection=None):
+        EC2Object.__init__(self, connection)
+        self.tags = TagSet()
+
+    def startElement(self, name, attrs, connection):
+        if name == 'tagSet':
+            return self.tags
+        else:
+            return None
+
+    def add_tag(self, key, value=None):
+        """
+        Add a tag to this object.  Tag's are stored by AWS and can be used
+        to organize and filter resources.  Adding a tag involves a round-trip
+        to the EC2 service.
+
+        :type key: str
+        :param key: The key or name of the tag being stored.
+
+        :type value: str
+        :param value: An optional value that can be stored with the tag.
+        """
+        status = self.connection.create_tags([self.id], {key : value})
+        if self.tags is None:
+            self.tags = TagSet()
+        self.tags[key] = value
+
+    def remove_tag(self, key, value=None):
+        """
+        Remove a tag from this object.  Removing a tag involves a round-trip
+        to the EC2 service.
+
+        :type key: str
+        :param key: The key or name of the tag being stored.
+
+        :type value: str
+        :param value: An optional value that can be stored with the tag.
+                      If a value is provided, it must match the value
+                      currently stored in EC2.  If not, the tag will not
+                      be removed.
+        """
+        if value:
+            tags = {key : value}
+        else:
+            tags = [key]
+        status = self.connection.delete_tags([self.id], tags)
+        if key in self.tags:
+            del self.tags[key]
diff --git a/boto/ec2/elb/__init__.py b/boto/ec2/elb/__init__.py
new file mode 100644
index 0000000..f4061d3
--- /dev/null
+++ b/boto/ec2/elb/__init__.py
@@ -0,0 +1,427 @@
+# Copyright (c) 2006-2009 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.
+#
+"""
+This module provides an interface to the Elastic Compute Cloud (EC2)
+load balancing service from AWS.
+"""
+from boto.connection import AWSQueryConnection
+from boto.ec2.instanceinfo import InstanceInfo
+from boto.ec2.elb.loadbalancer import LoadBalancer
+from boto.ec2.elb.instancestate import InstanceState
+from boto.ec2.elb.healthcheck import HealthCheck
+from boto.regioninfo import RegionInfo
+import boto
+
+RegionData = {
+    'us-east-1' : 'elasticloadbalancing.us-east-1.amazonaws.com',
+    'us-west-1' : 'elasticloadbalancing.us-west-1.amazonaws.com',
+    'eu-west-1' : 'elasticloadbalancing.eu-west-1.amazonaws.com',
+    'ap-southeast-1' : 'elasticloadbalancing.ap-southeast-1.amazonaws.com'}
+
+def regions():
+    """
+    Get all available regions for the SDB service.
+
+    :rtype: list
+    :return: A list of :class:`boto.RegionInfo` instances
+    """
+    regions = []
+    for region_name in RegionData:
+        region = RegionInfo(name=region_name,
+                            endpoint=RegionData[region_name],
+                            connection_cls=ELBConnection)
+        regions.append(region)
+    return regions
+
+def connect_to_region(region_name, **kw_params):
+    """
+    Given a valid region name, return a
+    :class:`boto.ec2.elb.ELBConnection`.
+
+    :param str region_name: The name of the region to connect to.
+
+    :rtype: :class:`boto.ec2.ELBConnection` or ``None``
+    :return: A connection to the given region, or None if an invalid region
+        name is given
+    """
+    for region in regions():
+        if region.name == region_name:
+            return region.connect(**kw_params)
+    return None
+
+class ELBConnection(AWSQueryConnection):
+
+    APIVersion = boto.config.get('Boto', 'elb_version', '2010-07-01')
+    DefaultRegionName = boto.config.get('Boto', 'elb_region_name', 'us-east-1')
+    DefaultRegionEndpoint = boto.config.get('Boto', 'elb_region_endpoint',
+                                            'elasticloadbalancing.amazonaws.com')
+
+    def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
+                 is_secure=False, port=None, proxy=None, proxy_port=None,
+                 proxy_user=None, proxy_pass=None, debug=0,
+                 https_connection_factory=None, region=None, path='/'):
+        """
+        Init method to create a new connection to EC2 Load Balancing Service.
+
+        B{Note:} The region argument is overridden by the region specified in
+        the boto configuration file.
+        """
+        if not region:
+            region = RegionInfo(self, self.DefaultRegionName,
+                                self.DefaultRegionEndpoint)
+        self.region = region
+        AWSQueryConnection.__init__(self, aws_access_key_id,
+                                    aws_secret_access_key,
+                                    is_secure, port, proxy, proxy_port,
+                                    proxy_user, proxy_pass,
+                                    self.region.endpoint, debug,
+                                    https_connection_factory, path)
+
+    def _required_auth_capability(self):
+        return ['ec2']
+
+    def build_list_params(self, params, items, label):
+        if isinstance(items, str):
+            items = [items]
+        for i in range(1, len(items)+1):
+            params[label % i] = items[i-1]
+
+    def get_all_load_balancers(self, load_balancer_names=None):
+        """
+        Retrieve all load balancers associated with your account.
+
+        :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_names:
+            self.build_list_params(params, load_balancer_names, 'LoadBalancerNames.member.%d')
+        return self.get_list('DescribeLoadBalancers', params, [('member', LoadBalancer)])
+
+
+    def create_load_balancer(self, name, zones, listeners):
+        """
+        Create a new load balancer for your account.
+
+        :type name: string
+        :param name: The mnemonic name associated with the new load balancer
+
+        :type zones: List of strings
+        :param zones: The names of the availability zone(s) to add.
+
+        :type listeners: List of tuples
+        :param listeners: Each tuple contains three or four values,
+                          (LoadBalancerPortNumber, InstancePortNumber, Protocol,
+                          [SSLCertificateId])
+                          where LoadBalancerPortNumber and InstancePortNumber are
+                          integer values between 1 and 65535, Protocol is a
+                          string containing either 'TCP', 'HTTP' or 'HTTPS';
+                          SSLCertificateID is the ARN of a AWS AIM certificate,
+                          and must be specified when doing HTTPS.
+
+        :rtype: :class:`boto.ec2.elb.loadbalancer.LoadBalancer`
+        :return: The newly created :class:`boto.ec2.elb.loadbalancer.LoadBalancer`
+        """
+        params = {'LoadBalancerName' : name}
+        for i in range(0, len(listeners)):
+            params['Listeners.member.%d.LoadBalancerPort' % (i+1)] = listeners[i][0]
+            params['Listeners.member.%d.InstancePort' % (i+1)] = listeners[i][1]
+            params['Listeners.member.%d.Protocol' % (i+1)] = listeners[i][2]
+            if listeners[i][2]=='HTTPS':
+                params['Listeners.member.%d.SSLCertificateId' % (i+1)] = listeners[i][3]
+        self.build_list_params(params, zones, 'AvailabilityZones.member.%d')
+        load_balancer = self.get_object('CreateLoadBalancer', params, LoadBalancer)
+        load_balancer.name = name
+        load_balancer.listeners = listeners
+        load_balancer.availability_zones = zones
+        return load_balancer
+
+    def create_load_balancer_listeners(self, name, listeners):
+        """
+        Creates a Listener (or group of listeners) for an existing Load Balancer
+
+        :type name: string
+        :param name: The name of the load balancer to create the listeners for
+
+        :type listeners: List of tuples
+        :param listeners: Each tuple contains three values,
+                          (LoadBalancerPortNumber, InstancePortNumber, Protocol,
+                          [SSLCertificateId])
+                          where LoadBalancerPortNumber and InstancePortNumber are
+                          integer values between 1 and 65535, Protocol is a
+                          string containing either 'TCP', 'HTTP' or 'HTTPS';
+                          SSLCertificateID is the ARN of a AWS AIM certificate,
+                          and must be specified when doing HTTPS.
+
+        :return: The status of the request
+        """
+        params = {'LoadBalancerName' : name}
+        for i in range(0, len(listeners)):
+            params['Listeners.member.%d.LoadBalancerPort' % (i+1)] = listeners[i][0]
+            params['Listeners.member.%d.InstancePort' % (i+1)] = listeners[i][1]
+            params['Listeners.member.%d.Protocol' % (i+1)] = listeners[i][2]
+            if listeners[i][2]=='HTTPS':
+                params['Listeners.member.%d.SSLCertificateId' % (i+1)] = listeners[i][3]
+        return self.get_status('CreateLoadBalancerListeners', params)
+
+
+    def delete_load_balancer(self, name):
+        """
+        Delete a Load Balancer from your account.
+
+        :type name: string
+        :param name: The name of the Load Balancer to delete
+        """
+        params = {'LoadBalancerName': name}
+        return self.get_status('DeleteLoadBalancer', params)
+
+    def delete_load_balancer_listeners(self, name, ports):
+        """
+        Deletes a load balancer listener (or group of listeners)
+
+        :type name: string
+        :param name: The name of the load balancer to create the listeners for
+
+        :type ports: List int
+        :param ports: Each int represents the port on the ELB to be removed
+
+        :return: The status of the request
+        """
+        params = {'LoadBalancerName' : name}
+        for i in range(0, len(ports)):
+            params['LoadBalancerPorts.member.%d' % (i+1)] = ports[i]
+        return self.get_status('DeleteLoadBalancerListeners', params)
+
+
+
+    def enable_availability_zones(self, load_balancer_name, zones_to_add):
+        """
+        Add availability zones to an existing Load Balancer
+        All zones must be in the same region as the Load Balancer
+        Adding zones that are already registered with the Load Balancer
+        has no effect.
+
+        :type load_balancer_name: string
+        :param load_balancer_name: The name of the Load Balancer
+
+        :type zones: List of strings
+        :param zones: The name of the zone(s) to add.
+
+        :rtype: List of strings
+        :return: An updated list of zones for this Load Balancer.
+
+        """
+        params = {'LoadBalancerName' : load_balancer_name}
+        self.build_list_params(params, zones_to_add, 'AvailabilityZones.member.%d')
+        return self.get_list('EnableAvailabilityZonesForLoadBalancer', params, None)
+
+    def disable_availability_zones(self, load_balancer_name, zones_to_remove):
+        """
+        Remove availability zones from an existing Load Balancer.
+        All zones must be in the same region as the Load Balancer.
+        Removing zones that are not registered with the Load Balancer
+        has no effect.
+        You cannot remove all zones from an Load Balancer.
+
+        :type load_balancer_name: string
+        :param load_balancer_name: The name of the Load Balancer
+
+        :type zones: List of strings
+        :param zones: The name of the zone(s) to remove.
+
+        :rtype: List of strings
+        :return: An updated list of zones for this Load Balancer.
+
+        """
+        params = {'LoadBalancerName' : load_balancer_name}
+        self.build_list_params(params, zones_to_remove, 'AvailabilityZones.member.%d')
+        return self.get_list('DisableAvailabilityZonesForLoadBalancer', params, None)
+
+    def register_instances(self, load_balancer_name, instances):
+        """
+        Add new Instances to an existing Load Balancer.
+
+        :type load_balancer_name: string
+        :param load_balancer_name: The name of the Load Balancer
+
+        :type instances: List of strings
+        :param instances: The instance ID's of the EC2 instances to add.
+
+        :rtype: List of strings
+        :return: An updated list of instances for this Load Balancer.
+
+        """
+        params = {'LoadBalancerName' : load_balancer_name}
+        self.build_list_params(params, instances, 'Instances.member.%d.InstanceId')
+        return self.get_list('RegisterInstancesWithLoadBalancer', params, [('member', InstanceInfo)])
+
+    def deregister_instances(self, load_balancer_name, instances):
+        """
+        Remove Instances from an existing Load Balancer.
+
+        :type load_balancer_name: string
+        :param load_balancer_name: The name of the Load Balancer
+
+        :type instances: List of strings
+        :param instances: The instance ID's of the EC2 instances to remove.
+
+        :rtype: List of strings
+        :return: An updated list of instances for this Load Balancer.
+
+        """
+        params = {'LoadBalancerName' : load_balancer_name}
+        self.build_list_params(params, instances, 'Instances.member.%d.InstanceId')
+        return self.get_list('DeregisterInstancesFromLoadBalancer', params, [('member', InstanceInfo)])
+
+    def describe_instance_health(self, load_balancer_name, instances=None):
+        """
+        Get current state of all Instances registered to an Load Balancer.
+
+        :type load_balancer_name: string
+        :param load_balancer_name: The name of the Load Balancer
+
+        :type instances: List of strings
+        :param instances: The instance ID's of the EC2 instances
+                          to return status for.  If not provided,
+                          the state of all instances will be returned.
+
+        :rtype: List of :class:`boto.ec2.elb.instancestate.InstanceState`
+        :return: list of state info for instances in this Load Balancer.
+
+        """
+        params = {'LoadBalancerName' : load_balancer_name}
+        if instances:
+            self.build_list_params(params, instances, 'Instances.member.%d.InstanceId')
+        return self.get_list('DescribeInstanceHealth', params, [('member', InstanceState)])
+
+    def configure_health_check(self, name, health_check):
+        """
+        Define a health check for the EndPoints.
+
+        :type name: string
+        :param name: The mnemonic name associated with the new access point
+
+        :type health_check: :class:`boto.ec2.elb.healthcheck.HealthCheck`
+        :param health_check: A HealthCheck object populated with the desired
+                             values.
+
+        :rtype: :class:`boto.ec2.elb.healthcheck.HealthCheck`
+        :return: The updated :class:`boto.ec2.elb.healthcheck.HealthCheck`
+        """
+        params = {'LoadBalancerName' : name,
+                  'HealthCheck.Timeout' : health_check.timeout,
+                  'HealthCheck.Target' : health_check.target,
+                  'HealthCheck.Interval' : health_check.interval,
+                  'HealthCheck.UnhealthyThreshold' : health_check.unhealthy_threshold,
+                  'HealthCheck.HealthyThreshold' : health_check.healthy_threshold}
+        return self.get_object('ConfigureHealthCheck', params, HealthCheck)
+
+    def set_lb_listener_SSL_certificate(self, lb_name, lb_port, ssl_certificate_id):
+        """
+        Sets the certificate that terminates the specified listener's SSL
+        connections. The specified certificate replaces any prior certificate
+        that was used on the same LoadBalancer and port.
+        """
+        params = {
+                    'LoadBalancerName'          :   lb_name,
+                    'LoadBalancerPort'          :   lb_port,
+                    'SSLCertificateId'          :   ssl_certificate_id,
+                 }
+        return self.get_status('SetLoadBalancerListenerSSLCertificate', params)
+
+    def create_app_cookie_stickiness_policy(self, name, lb_name, policy_name):
+        """
+        Generates a stickiness policy with sticky session lifetimes that follow
+        that of an application-generated cookie. This policy can only be
+        associated with HTTP listeners.
+
+        This policy is similar to the policy created by
+        CreateLBCookieStickinessPolicy, except that the lifetime of the special
+        Elastic Load Balancing cookie follows the lifetime of the
+        application-generated cookie specified in the policy configuration. The
+        load balancer only inserts a new stickiness cookie when the application
+        response includes a new application cookie.
+
+        If the application cookie is explicitly removed or expires, the session
+        stops being sticky until a new application cookie is issued.
+        """
+        params = {
+                    'CookieName'        :   name,
+                    'LoadBalancerName'  :   lb_name,
+                    'PolicyName'        :   policy_name,
+                 }
+        return self.get_status('CreateAppCookieStickinessPolicy', params)
+
+    def create_lb_cookie_stickiness_policy(self, cookie_expiration_period, lb_name, policy_name):
+        """
+        Generates a stickiness policy with sticky session lifetimes controlled
+        by the lifetime of the browser (user-agent) or a specified expiration
+        period. This policy can only be associated only with HTTP listeners.
+
+        When a load balancer implements this policy, the load balancer uses a
+        special cookie to track the backend server instance for each request.
+        When the load balancer receives a request, it first checks to see if
+        this cookie is present in the request. If so, the load balancer sends
+        the request to the application server specified in the cookie. If not,
+        the load balancer sends the request to a server that is chosen based on
+        the existing load balancing algorithm.
+
+        A cookie is inserted into the response for binding subsequent requests
+        from the same user to that server. The validity of the cookie is based
+        on the cookie expiration time, which is specified in the policy
+        configuration.
+        """
+        params = {
+                    'CookieExpirationPeriod'    :   cookie_expiration_period,
+                    'LoadBalancerName'          :   lb_name,
+                    'PolicyName'                :   policy_name,
+                 }
+        return self.get_status('CreateLBCookieStickinessPolicy', params)
+
+    def delete_lb_policy(self, lb_name, policy_name):
+        """
+        Deletes a policy from the LoadBalancer. The specified policy must not
+        be enabled for any listeners.
+        """
+        params = {
+                    'LoadBalancerName'          : lb_name,
+                    'PolicyName'                : policy_name,
+                 }
+        return self.get_status('DeleteLoadBalancerPolicy', params)
+
+    def set_lb_policies_of_listener(self, lb_name, lb_port, policies):
+        """
+        Associates, updates, or disables a policy with a listener on the load
+        balancer. Currently only zero (0) or one (1) policy can be associated
+        with a listener.
+        """
+        params = {
+                    'LoadBalancerName'          : lb_name,
+                    'LoadBalancerPort'          : lb_port,
+                 }
+        self.build_list_params(params, policies, 'PolicyNames.member.%d')
+        return self.get_status('SetLoadBalancerPoliciesOfListener', params)
+
+
diff --git a/boto/ec2/elb/healthcheck.py b/boto/ec2/elb/healthcheck.py
new file mode 100644
index 0000000..5a3edbc
--- /dev/null
+++ b/boto/ec2/elb/healthcheck.py
@@ -0,0 +1,68 @@
+# Copyright (c) 2006-2009 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.
+
+class HealthCheck(object):
+    """
+    Represents an EC2 Access Point Health Check
+    """
+
+    def __init__(self, access_point=None, interval=30, target=None,
+                 healthy_threshold=3, timeout=5, unhealthy_threshold=5):
+        self.access_point = access_point
+        self.interval = interval
+        self.target = target
+        self.healthy_threshold = healthy_threshold
+        self.timeout = timeout
+        self.unhealthy_threshold = unhealthy_threshold
+
+    def __repr__(self):
+        return 'HealthCheck:%s' % self.target
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'Interval':
+            self.interval = int(value)
+        elif name == 'Target':
+            self.target = value
+        elif name == 'HealthyThreshold':
+            self.healthy_threshold = int(value)
+        elif name == 'Timeout':
+            self.timeout = int(value)
+        elif name == 'UnhealthyThreshold':
+            self.unhealthy_threshold = int(value)
+        else:
+            setattr(self, name, value)
+
+    def update(self):
+        if not self.access_point:
+            return
+
+        new_hc = self.connection.configure_health_check(self.access_point,
+                                                        self)
+        self.interval = new_hc.interval
+        self.target = new_hc.target
+        self.healthy_threshold = new_hc.healthy_threshold
+        self.unhealthy_threshold = new_hc.unhealthy_threshold
+        self.timeout = new_hc.timeout
+
+
diff --git a/boto/ec2/elb/instancestate.py b/boto/ec2/elb/instancestate.py
new file mode 100644
index 0000000..4a9b0d4
--- /dev/null
+++ b/boto/ec2/elb/instancestate.py
@@ -0,0 +1,54 @@
+# Copyright (c) 2006-2009 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.
+
+class InstanceState(object):
+    """
+    Represents the state of an EC2 Load Balancer Instance
+    """
+
+    def __init__(self, load_balancer=None, description=None,
+                 state=None, instance_id=None, reason_code=None):
+        self.load_balancer = load_balancer
+        self.description = description
+        self.state = state
+        self.instance_id = instance_id
+        self.reason_code = reason_code
+
+    def __repr__(self):
+        return 'InstanceState:(%s,%s)' % (self.instance_id, self.state)
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'Description':
+            self.description = value
+        elif name == 'State':
+            self.state = value
+        elif name == 'InstanceId':
+            self.instance_id = value
+        elif name == 'ReasonCode':
+            self.reason_code = value
+        else:
+            setattr(self, name, value)
+
+
+
diff --git a/boto/ec2/elb/listelement.py b/boto/ec2/elb/listelement.py
new file mode 100644
index 0000000..5be4599
--- /dev/null
+++ b/boto/ec2/elb/listelement.py
@@ -0,0 +1,31 @@
+# Copyright (c) 2006-2009 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.
+
+class ListElement(list):
+
+    def startElement(self, name, attrs, connection):
+        pass
+
+    def endElement(self, name, value, connection):
+        if name == 'member':
+            self.append(value)
+    
+    
diff --git a/boto/ec2/elb/listener.py b/boto/ec2/elb/listener.py
new file mode 100644
index 0000000..a8807c0
--- /dev/null
+++ b/boto/ec2/elb/listener.py
@@ -0,0 +1,71 @@
+# Copyright (c) 2006-2009 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.
+
+class Listener(object):
+    """
+    Represents an EC2 Load Balancer Listener tuple
+    """
+
+    def __init__(self, load_balancer=None, load_balancer_port=0,
+                 instance_port=0, protocol='', ssl_certificate_id=None):
+        self.load_balancer = load_balancer
+        self.load_balancer_port = load_balancer_port
+        self.instance_port = instance_port
+        self.protocol = protocol
+        self.ssl_certificate_id = ssl_certificate_id
+
+    def __repr__(self):
+        r = "(%d, %d, '%s'" % (self.load_balancer_port, self.instance_port, self.protocol)
+        if self.ssl_certificate_id:
+            r += ', %s' % (self.ssl_certificate_id)
+        r += ')'
+        return r
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'LoadBalancerPort':
+            self.load_balancer_port = int(value)
+        elif name == 'InstancePort':
+            self.instance_port = int(value)
+        elif name == 'Protocol':
+            self.protocol = value
+        elif name == 'SSLCertificateId':
+            self.ssl_certificate_id = value
+        else:
+            setattr(self, name, value)
+
+    def get_tuple(self):
+        return self.load_balancer_port, self.instance_port, self.protocol
+
+    def __getitem__(self, key):
+        if key == 0:
+            return self.load_balancer_port
+        if key == 1:
+            return self.instance_port
+        if key == 2:
+            return self.protocol
+        raise KeyError
+
+
+
+
diff --git a/boto/ec2/elb/loadbalancer.py b/boto/ec2/elb/loadbalancer.py
new file mode 100644
index 0000000..9759952
--- /dev/null
+++ b/boto/ec2/elb/loadbalancer.py
@@ -0,0 +1,182 @@
+# Copyright (c) 2006-2009 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.ec2.elb.healthcheck import HealthCheck
+from boto.ec2.elb.listener import Listener
+from boto.ec2.elb.listelement import ListElement
+from boto.ec2.elb.policies import Policies
+from boto.ec2.instanceinfo import InstanceInfo
+from boto.resultset import ResultSet
+
+class LoadBalancer(object):
+    """
+    Represents an EC2 Load Balancer
+    """
+
+    def __init__(self, connection=None, name=None, endpoints=None):
+        self.connection = connection
+        self.name = name
+        self.listeners = None
+        self.health_check = None
+        self.policies = None
+        self.dns_name = None
+        self.created_time = None
+        self.instances = None
+        self.availability_zones = ListElement()
+
+    def __repr__(self):
+        return 'LoadBalancer:%s' % self.name
+
+    def startElement(self, name, attrs, connection):
+        if name == 'HealthCheck':
+            self.health_check = HealthCheck(self)
+            return self.health_check
+        elif name == 'ListenerDescriptions':
+            self.listeners = ResultSet([('member', Listener)])
+            return self.listeners
+        elif name == 'AvailabilityZones':
+            return self.availability_zones
+        elif name == 'Instances':
+            self.instances = ResultSet([('member', InstanceInfo)])
+            return self.instances
+        elif name == 'Policies':
+            self.policies = Policies(self)
+            return self.policies
+        else:
+            return None
+
+    def endElement(self, name, value, connection):
+        if name == 'LoadBalancerName':
+            self.name = value
+        elif name == 'DNSName':
+            self.dns_name = value
+        elif name == 'CreatedTime':
+            self.created_time = value
+        elif name == 'InstanceId':
+            self.instances.append(value)
+        else:
+            setattr(self, name, value)
+
+    def enable_zones(self, zones):
+        """
+        Enable availability zones to this Access Point.
+        All zones must be in the same region as the Access Point.
+
+        :type zones: string or List of strings
+        :param zones: The name of the zone(s) to add.
+
+        """
+        if isinstance(zones, str) or isinstance(zones, unicode):
+            zones = [zones]
+        new_zones = self.connection.enable_availability_zones(self.name, zones)
+        self.availability_zones = new_zones
+
+    def disable_zones(self, zones):
+        """
+        Disable availability zones from this Access Point.
+
+        :type zones: string or List of strings
+        :param zones: The name of the zone(s) to add.
+
+        """
+        if isinstance(zones, str) or isinstance(zones, unicode):
+            zones = [zones]
+        new_zones = self.connection.disable_availability_zones(self.name, zones)
+        self.availability_zones = new_zones
+
+    def register_instances(self, instances):
+        """
+        Add instances to this Load Balancer
+        All instances must be in the same region as the Load Balancer.
+        Adding endpoints that are already registered with the Load Balancer
+        has no effect.
+
+        :type zones: string or List of instance id's
+        :param zones: The name of the endpoint(s) to add.
+
+        """
+        if isinstance(instances, str) or isinstance(instances, unicode):
+            instances = [instances]
+        new_instances = self.connection.register_instances(self.name, instances)
+        self.instances = new_instances
+
+    def deregister_instances(self, instances):
+        """
+        Remove instances from this Load Balancer.
+        Removing instances that are not registered with the Load Balancer
+        has no effect.
+
+        :type zones: string or List of instance id's
+        :param zones: The name of the endpoint(s) to add.
+
+        """
+        if isinstance(instances, str) or isinstance(instances, unicode):
+            instances = [instances]
+        new_instances = self.connection.deregister_instances(self.name, instances)
+        self.instances = new_instances
+
+    def delete(self):
+        """
+        Delete this load balancer
+        """
+        return self.connection.delete_load_balancer(self.name)
+
+    def configure_health_check(self, health_check):
+        return self.connection.configure_health_check(self.name, health_check)
+
+    def get_instance_health(self, instances=None):
+        return self.connection.describe_instance_health(self.name, instances)
+
+    def create_listeners(self, listeners):
+        return self.connection.create_load_balancer_listeners(self.name, listeners)
+
+    def create_listener(self, inPort, outPort=None, proto="tcp"):
+        if outPort == None:
+            outPort = inPort
+        return self.create_listeners([(inPort, outPort, proto)])
+
+    def delete_listeners(self, listeners):
+        return self.connection.delete_load_balancer_listeners(self.name, listeners)
+
+    def delete_listener(self, inPort, outPort=None, proto="tcp"):
+        if outPort == None:
+            outPort = inPort
+        return self.delete_listeners([(inPort, outPort, proto)])
+
+    def delete_policy(self, policy_name):
+        """
+        Deletes a policy from the LoadBalancer. The specified policy must not
+        be enabled for any listeners.
+        """
+        return self.connection.delete_lb_policy(self.name, policy_name)
+
+    def set_policies_of_listener(self, lb_port, policies):
+        return self.connection.set_lb_policies_of_listener(self.name, lb_port, policies)
+
+    def create_cookie_stickiness_policy(self, cookie_expiration_period, policy_name):
+        return self.connection.create_lb_cookie_stickiness_policy(cookie_expiration_period, self.name, policy_name)
+
+    def create_app_cookie_stickiness_policy(self, name, policy_name):
+        return self.connection.create_app_cookie_stickiness_policy(name, self.name, policy_name)
+
+    def set_listener_SSL_certificate(self, lb_port, ssl_certificate_id):
+        return self.connection.set_lb_listener_SSL_certificate(self.name, lb_port, ssl_certificate_id)
+
diff --git a/boto/ec2/elb/policies.py b/boto/ec2/elb/policies.py
new file mode 100644
index 0000000..428ce72
--- /dev/null
+++ b/boto/ec2/elb/policies.py
@@ -0,0 +1,82 @@
+# Copyright (c) 2010 Reza Lotun http://reza.lotun.name
+#
+# 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.resultset import ResultSet
+
+
+class AppCookieStickinessPolicy(object):
+    def __init__(self, connection=None):
+        self.cookie_name = None
+        self.policy_name = None
+
+    def __repr__(self):
+        return 'AppCookieStickiness(%s, %s)' % (self.policy_name, self.cookie_name)
+
+    def startElement(self, name, attrs, connection):
+        pass
+
+    def endElement(self, name, value, connection):
+        if name == 'CookieName':
+            self.cookie_name = value
+        elif name == 'PolicyName':
+            self.policy_name = value
+
+
+class LBCookieStickinessPolicy(object):
+    def __init__(self, connection=None):
+        self.policy_name = None
+        self.cookie_expiration_period = None
+
+    def __repr__(self):
+        return 'LBCookieStickiness(%s, %s)' % (self.policy_name, self.cookie_expiration_period)
+
+    def startElement(self, name, attrs, connection):
+        pass
+
+    def endElement(self, name, value, connection):
+        if name == 'CookieExpirationPeriod':
+            self.cookie_expiration_period = value
+        elif name == 'PolicyName':
+            self.policy_name = value
+
+
+class Policies(object):
+    """
+    ELB Policies
+    """
+    def __init__(self, connection=None):
+        self.connection = connection
+        self.app_cookie_stickiness_policies = None
+        self.lb_cookie_stickiness_policies = None
+
+    def __repr__(self):
+        return 'Policies(AppCookieStickiness%s, LBCookieStickiness%s)' % (self.app_cookie_stickiness_policies,
+                                                                           self.lb_cookie_stickiness_policies)
+
+    def startElement(self, name, attrs, connection):
+        if name == 'AppCookieStickinessPolicies':
+            self.app_cookie_stickiness_policies = ResultSet([('member', AppCookieStickinessPolicy)])
+        elif name == 'LBCookieStickinessPolicies':
+            self.lb_cookie_stickiness_policies = ResultSet([('member', LBCookieStickinessPolicy)])
+
+    def endElement(self, name, value, connection):
+        return
+
diff --git a/boto/ec2/image.py b/boto/ec2/image.py
new file mode 100644
index 0000000..a85fba0
--- /dev/null
+++ b/boto/ec2/image.py
@@ -0,0 +1,322 @@
+# Copyright (c) 2006-2010 Mitch Garnaat http://garnaat.org/
+# Copyright (c) 2010, Eucalyptus Systems, Inc.
+#
+# 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.ec2.ec2object import EC2Object, TaggedEC2Object
+from boto.ec2.blockdevicemapping import BlockDeviceMapping
+
+class ProductCodes(list):
+
+    def startElement(self, name, attrs, connection):
+        pass
+
+    def endElement(self, name, value, connection):
+        if name == 'productCode':
+            self.append(value)
+    
+class Image(TaggedEC2Object):
+    """
+    Represents an EC2 Image
+    """
+    
+    def __init__(self, connection=None):
+        TaggedEC2Object.__init__(self, connection)
+        self.id = None
+        self.location = None
+        self.state = None
+        self.ownerId = None  # for backwards compatibility
+        self.owner_id = None
+        self.owner_alias = None
+        self.is_public = False
+        self.architecture = None
+        self.platform = None
+        self.type = None
+        self.kernel_id = None
+        self.ramdisk_id = None
+        self.name = None
+        self.description = None
+        self.product_codes = ProductCodes()
+        self.block_device_mapping = None
+        self.root_device_type = None
+        self.root_device_name = None
+        self.virtualization_type = None
+        self.hypervisor = None
+        self.instance_lifecycle = None
+
+    def __repr__(self):
+        return 'Image:%s' % self.id
+
+    def startElement(self, name, attrs, connection):
+        retval = TaggedEC2Object.startElement(self, name, attrs, connection)
+        if retval is not None:
+            return retval
+        if name == 'blockDeviceMapping':
+            self.block_device_mapping = BlockDeviceMapping()
+            return self.block_device_mapping
+        elif name == 'productCodes':
+            return self.product_codes
+        else:
+            return None
+
+    def endElement(self, name, value, connection):
+        if name == 'imageId':
+            self.id = value
+        elif name == 'imageLocation':
+            self.location = value
+        elif name == 'imageState':
+            self.state = value
+        elif name == 'imageOwnerId':
+            self.ownerId = value # for backwards compatibility
+            self.owner_id = value
+        elif name == 'isPublic':
+            if value == 'false':
+                self.is_public = False
+            elif value == 'true':
+                self.is_public = True
+            else:
+                raise Exception(
+                    'Unexpected value of isPublic %s for image %s'%(
+                        value, 
+                        self.id
+                    )
+                )
+        elif name == 'architecture':
+            self.architecture = value
+        elif name == 'imageType':
+            self.type = value
+        elif name == 'kernelId':
+            self.kernel_id = value
+        elif name == 'ramdiskId':
+            self.ramdisk_id = value
+        elif name == 'imageOwnerAlias':
+            self.owner_alias = value
+        elif name == 'platform':
+            self.platform = value
+        elif name == 'name':
+            self.name = value
+        elif name == 'description':
+            self.description = value
+        elif name == 'rootDeviceType':
+            self.root_device_type = value
+        elif name == 'rootDeviceName':
+            self.root_device_name = value
+        elif name == 'virtualizationType':
+            self.virtualization_type = value
+        elif name == 'hypervisor':
+            self.hypervisor = value
+        elif name == 'instanceLifecycle':
+            self.instance_lifecycle = value
+        else:
+            setattr(self, name, value)
+
+    def _update(self, updated):
+        self.__dict__.update(updated.__dict__)
+
+    def update(self, validate=False):
+        """
+        Update the image's state information by making a call to fetch
+        the current image attributes from the service.
+
+        :type validate: bool
+        :param validate: By default, if EC2 returns no data about the
+                         image the update method returns quietly.  If
+                         the validate param is True, however, it will
+                         raise a ValueError exception if no data is
+                         returned from EC2.
+        """
+        rs = self.connection.get_all_images([self.id])
+        if len(rs) > 0:
+            img = rs[0]
+            if img.id == self.id:
+                self._update(img)
+        elif validate:
+            raise ValueError('%s is not a valid Image ID' % self.id)
+        return self.state
+
+    def run(self, min_count=1, max_count=1, key_name=None,
+            security_groups=None, user_data=None,
+            addressing_type=None, instance_type='m1.small', placement=None,
+            kernel_id=None, ramdisk_id=None,
+            monitoring_enabled=False, subnet_id=None,
+            block_device_map=None,
+            disable_api_termination=False,
+            instance_initiated_shutdown_behavior=None,
+            private_ip_address=None,
+            placement_group=None):
+        """
+        Runs this instance.
+        
+        :type min_count: int
+        :param min_count: The minimum number of instances to start
+        
+        :type max_count: int
+        :param max_count: The maximum number of instances to start
+        
+        :type key_name: string
+        :param key_name: The name of the keypair to run this instance with.
+        
+        :type security_groups: 
+        :param security_groups:
+        
+        :type user_data: 
+        :param user_data:
+        
+        :type addressing_type: 
+        :param daddressing_type:
+        
+        :type instance_type: string
+        :param instance_type: The type of instance to run.  Current choices are:
+                              m1.small | m1.large | m1.xlarge | c1.medium |
+                              c1.xlarge | m2.xlarge | m2.2xlarge |
+                              m2.4xlarge | cc1.4xlarge
+        
+        :type placement: string
+        :param placement: The availability zone in which to launch the instances
+
+        :type kernel_id: string
+        :param kernel_id: The ID of the kernel with which to launch the instances
+        
+        :type ramdisk_id: string
+        :param ramdisk_id: The ID of the RAM disk with which to launch the instances
+        
+        :type monitoring_enabled: bool
+        :param monitoring_enabled: Enable CloudWatch monitoring on the instance.
+        
+        :type subnet_id: string
+        :param subnet_id: The subnet ID within which to launch the instances for VPC.
+        
+        :type private_ip_address: string
+        :param private_ip_address: If you're using VPC, you can optionally use
+                                   this parameter to assign the instance a
+                                   specific available IP address from the
+                                   subnet (e.g., 10.0.0.25).
+
+        :type block_device_map: :class:`boto.ec2.blockdevicemapping.BlockDeviceMapping`
+        :param block_device_map: A BlockDeviceMapping data structure
+                                 describing the EBS volumes associated
+                                 with the Image.
+
+        :type disable_api_termination: bool
+        :param disable_api_termination: If True, the instances will be locked
+                                        and will not be able to be terminated
+                                        via the API.
+
+        :type instance_initiated_shutdown_behavior: string
+        :param instance_initiated_shutdown_behavior: Specifies whether the instance's
+                                                     EBS volumes are stopped (i.e. detached)
+                                                     or terminated (i.e. deleted) when
+                                                     the instance is shutdown by the
+                                                     owner.  Valid values are:
+                                                     stop | terminate
+
+        :type placement_group: string
+        :param placement_group: If specified, this is the name of the placement
+                                group in which the instance(s) will be launched.
+
+        :rtype: Reservation
+        :return: The :class:`boto.ec2.instance.Reservation` associated with the request for machines
+        """
+        return self.connection.run_instances(self.id, min_count, max_count,
+                                             key_name, security_groups,
+                                             user_data, addressing_type,
+                                             instance_type, placement,
+                                             kernel_id, ramdisk_id,
+                                             monitoring_enabled, subnet_id,
+                                             block_device_map, disable_api_termination,
+                                             instance_initiated_shutdown_behavior,
+                                             private_ip_address,
+                                             placement_group)
+
+    def deregister(self):
+        return self.connection.deregister_image(self.id)
+
+    def get_launch_permissions(self):
+        img_attrs = self.connection.get_image_attribute(self.id,
+                                                        'launchPermission')
+        return img_attrs.attrs
+
+    def set_launch_permissions(self, user_ids=None, group_names=None):
+        return self.connection.modify_image_attribute(self.id,
+                                                      'launchPermission',
+                                                      'add',
+                                                      user_ids,
+                                                      group_names)
+
+    def remove_launch_permissions(self, user_ids=None, group_names=None):
+        return self.connection.modify_image_attribute(self.id,
+                                                      'launchPermission',
+                                                      'remove',
+                                                      user_ids,
+                                                      group_names)
+
+    def reset_launch_attributes(self):
+        return self.connection.reset_image_attribute(self.id,
+                                                     'launchPermission')
+
+    def get_kernel(self):
+        img_attrs =self.connection.get_image_attribute(self.id, 'kernel')
+        return img_attrs.kernel
+
+    def get_ramdisk(self):
+        img_attrs = self.connection.get_image_attribute(self.id, 'ramdisk')
+        return img_attrs.ramdisk
+
+class ImageAttribute:
+
+    def __init__(self, parent=None):
+        self.name = None
+        self.kernel = None
+        self.ramdisk = None
+        self.attrs = {}
+
+    def startElement(self, name, attrs, connection):
+        if name == 'blockDeviceMapping':
+            self.attrs['block_device_mapping'] = BlockDeviceMapping()
+            return self.attrs['block_device_mapping']
+        else:
+            return None
+
+    def endElement(self, name, value, connection):
+        if name == 'launchPermission':
+            self.name = 'launch_permission'
+        elif name == 'group':
+            if self.attrs.has_key('groups'):
+                self.attrs['groups'].append(value)
+            else:
+                self.attrs['groups'] = [value]
+        elif name == 'userId':
+            if self.attrs.has_key('user_ids'):
+                self.attrs['user_ids'].append(value)
+            else:
+                self.attrs['user_ids'] = [value]
+        elif name == 'productCode':
+            if self.attrs.has_key('product_codes'):
+                self.attrs['product_codes'].append(value)
+            else:
+                self.attrs['product_codes'] = [value]
+        elif name == 'imageId':
+            self.image_id = value
+        elif name == 'kernel':
+            self.kernel = value
+        elif name == 'ramdisk':
+            self.ramdisk = value
+        else:
+            setattr(self, name, value)
diff --git a/boto/ec2/instance.py b/boto/ec2/instance.py
new file mode 100644
index 0000000..9e8aacf
--- /dev/null
+++ b/boto/ec2/instance.py
@@ -0,0 +1,394 @@
+# Copyright (c) 2006-2010 Mitch Garnaat http://garnaat.org/
+# Copyright (c) 2010, Eucalyptus Systems, Inc.
+#
+# 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.
+
+"""
+Represents an EC2 Instance
+"""
+import boto
+from boto.ec2.ec2object import EC2Object, TaggedEC2Object
+from boto.resultset import ResultSet
+from boto.ec2.address import Address
+from boto.ec2.blockdevicemapping import BlockDeviceMapping
+from boto.ec2.image import ProductCodes
+import base64
+
+class Reservation(EC2Object):
+    
+    def __init__(self, connection=None):
+        EC2Object.__init__(self, connection)
+        self.id = None
+        self.owner_id = None
+        self.groups = []
+        self.instances = []
+
+    def __repr__(self):
+        return 'Reservation:%s' % self.id
+
+    def startElement(self, name, attrs, connection):
+        if name == 'instancesSet':
+            self.instances = ResultSet([('item', Instance)])
+            return self.instances
+        elif name == 'groupSet':
+            self.groups = ResultSet([('item', Group)])
+            return self.groups
+        else:
+            return None
+
+    def endElement(self, name, value, connection):
+        if name == 'reservationId':
+            self.id = value
+        elif name == 'ownerId':
+            self.owner_id = value
+        else:
+            setattr(self, name, value)
+
+    def stop_all(self):
+        for instance in self.instances:
+            instance.stop()
+            
+class Instance(TaggedEC2Object):
+    
+    def __init__(self, connection=None):
+        TaggedEC2Object.__init__(self, connection)
+        self.id = None
+        self.dns_name = None
+        self.public_dns_name = None
+        self.private_dns_name = None
+        self.state = None
+        self.state_code = None
+        self.key_name = None
+        self.shutdown_state = None
+        self.previous_state = None
+        self.instance_type = None
+        self.instance_class = None
+        self.launch_time = None
+        self.image_id = None
+        self.placement = None
+        self.kernel = None
+        self.ramdisk = None
+        self.product_codes = ProductCodes()
+        self.ami_launch_index = None
+        self.monitored = False
+        self.instance_class = None
+        self.spot_instance_request_id = None
+        self.subnet_id = None
+        self.vpc_id = None
+        self.private_ip_address = None
+        self.ip_address = None
+        self.requester_id = None
+        self._in_monitoring_element = False
+        self.persistent = False
+        self.root_device_name = None
+        self.root_device_type = None
+        self.block_device_mapping = None
+        self.state_reason = None
+        self.group_name = None
+        self.client_token = None
+
+    def __repr__(self):
+        return 'Instance:%s' % self.id
+
+    def startElement(self, name, attrs, connection):
+        retval = TaggedEC2Object.startElement(self, name, attrs, connection)
+        if retval is not None:
+            return retval
+        if name == 'monitoring':
+            self._in_monitoring_element = True
+        elif name == 'blockDeviceMapping':
+            self.block_device_mapping = BlockDeviceMapping()
+            return self.block_device_mapping
+        elif name == 'productCodes':
+            return self.product_codes
+        elif name == 'stateReason':
+            self.state_reason = StateReason()
+            return self.state_reason
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'instanceId':
+            self.id = value
+        elif name == 'imageId':
+            self.image_id = value
+        elif name == 'dnsName' or name == 'publicDnsName':
+            self.dns_name = value           # backwards compatibility
+            self.public_dns_name = value
+        elif name == 'privateDnsName':
+            self.private_dns_name = value
+        elif name == 'keyName':
+            self.key_name = value
+        elif name == 'amiLaunchIndex':
+            self.ami_launch_index = value
+        elif name == 'shutdownState':
+            self.shutdown_state = value
+        elif name == 'previousState':
+            self.previous_state = value
+        elif name == 'name':
+            self.state = value
+        elif name == 'code':
+            try:
+                self.state_code = int(value)
+            except ValueError:
+                boto.log.warning('Error converting code (%s) to int' % value)
+                self.state_code = value
+        elif name == 'instanceType':
+            self.instance_type = value
+        elif name == 'instanceClass':
+            self.instance_class = value
+        elif name == 'rootDeviceName':
+            self.root_device_name = value
+        elif name == 'rootDeviceType':
+            self.root_device_type = value
+        elif name == 'launchTime':
+            self.launch_time = value
+        elif name == 'availabilityZone':
+            self.placement = value
+        elif name == 'placement':
+            pass
+        elif name == 'kernelId':
+            self.kernel = value
+        elif name == 'ramdiskId':
+            self.ramdisk = value
+        elif name == 'state':
+            if self._in_monitoring_element:
+                if value == 'enabled':
+                    self.monitored = True
+                self._in_monitoring_element = False
+        elif name == 'instanceClass':
+            self.instance_class = value
+        elif name == 'spotInstanceRequestId':
+            self.spot_instance_request_id = value
+        elif name == 'subnetId':
+            self.subnet_id = value
+        elif name == 'vpcId':
+            self.vpc_id = value
+        elif name == 'privateIpAddress':
+            self.private_ip_address = value
+        elif name == 'ipAddress':
+            self.ip_address = value
+        elif name == 'requesterId':
+            self.requester_id = value
+        elif name == 'persistent':
+            if value == 'true':
+                self.persistent = True
+            else:
+                self.persistent = False
+        elif name == 'groupName':
+            if self._in_monitoring_element:
+                self.group_name = value
+        elif name == 'clientToken':
+            self.client_token = value
+        else:
+            setattr(self, name, value)
+
+    def _update(self, updated):
+        self.__dict__.update(updated.__dict__)
+
+    def update(self, validate=False):
+        """
+        Update the instance's state information by making a call to fetch
+        the current instance attributes from the service.
+
+        :type validate: bool
+        :param validate: By default, if EC2 returns no data about the
+                         instance the update method returns quietly.  If
+                         the validate param is True, however, it will
+                         raise a ValueError exception if no data is
+                         returned from EC2.
+        """
+        rs = self.connection.get_all_instances([self.id])
+        if len(rs) > 0:
+            r = rs[0]
+            for i in r.instances:
+                if i.id == self.id:
+                    self._update(i)
+        elif validate:
+            raise ValueError('%s is not a valid Instance ID' % self.id)
+        return self.state
+
+    def terminate(self):
+        """
+        Terminate the instance
+        """
+        rs = self.connection.terminate_instances([self.id])
+        self._update(rs[0])
+
+    def stop(self, force=False):
+        """
+        Stop the instance
+
+        :type force: bool
+        :param force: Forces the instance to stop
+        
+        :rtype: list
+        :return: A list of the instances stopped
+        """
+        rs = self.connection.stop_instances([self.id])
+        self._update(rs[0])
+
+    def start(self):
+        """
+        Start the instance.
+        """
+        rs = self.connection.start_instances([self.id])
+        self._update(rs[0])
+
+    def reboot(self):
+        return self.connection.reboot_instances([self.id])
+
+    def get_console_output(self):
+        """
+        Retrieves the console output for the instance.
+
+        :rtype: :class:`boto.ec2.instance.ConsoleOutput`
+        :return: The console output as a ConsoleOutput object
+        """
+        return self.connection.get_console_output(self.id)
+
+    def confirm_product(self, product_code):
+        return self.connection.confirm_product_instance(self.id, product_code)
+
+    def use_ip(self, ip_address):
+        if isinstance(ip_address, Address):
+            ip_address = ip_address.public_ip
+        return self.connection.associate_address(self.id, ip_address)
+
+    def monitor(self):
+        return self.connection.monitor_instance(self.id)
+
+    def unmonitor(self):
+        return self.connection.unmonitor_instance(self.id)
+
+    def get_attribute(self, attribute):
+        """
+        Gets an attribute from this instance.
+
+        :type attribute: string
+        :param attribute: The attribute you need information about
+                          Valid choices are:
+                          instanceType|kernel|ramdisk|userData|
+                          disableApiTermination|
+                          instanceInitiatedShutdownBehavior|
+                          rootDeviceName|blockDeviceMapping
+
+        :rtype: :class:`boto.ec2.image.InstanceAttribute`
+        :return: An InstanceAttribute object representing the value of the
+                 attribute requested
+        """
+        return self.connection.get_instance_attribute(self.id, attribute)
+
+    def modify_attribute(self, attribute, value):
+        """
+        Changes an attribute of this instance
+
+        :type attribute: string
+        :param attribute: The attribute you wish to change.
+                          AttributeName - Expected value (default)
+                          instanceType - A valid instance type (m1.small)
+                          kernel - Kernel ID (None)
+                          ramdisk - Ramdisk ID (None)
+                          userData - Base64 encoded String (None)
+                          disableApiTermination - Boolean (true)
+                          instanceInitiatedShutdownBehavior - stop|terminate
+                          rootDeviceName - device name (None)
+
+        :type value: string
+        :param value: The new value for the attribute
+
+        :rtype: bool
+        :return: Whether the operation succeeded or not
+        """
+        return self.connection.modify_instance_attribute(self.id, attribute,
+                                                         value)
+
+    def reset_attribute(self, attribute):
+        """
+        Resets an attribute of this instance to its default value.
+
+        :type attribute: string
+        :param attribute: The attribute to reset. Valid values are:
+                          kernel|ramdisk
+
+        :rtype: bool
+        :return: Whether the operation succeeded or not
+        """
+        return self.connection.reset_instance_attribute(self.id, attribute)
+
+class Group:
+
+    def __init__(self, parent=None):
+        self.id = None
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'groupId':
+            self.id = value
+        else:
+            setattr(self, name, value)
+    
+class ConsoleOutput:
+
+    def __init__(self, parent=None):
+        self.parent = parent
+        self.instance_id = None
+        self.timestamp = None
+        self.comment = None
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'instanceId':
+            self.instance_id = value
+        elif name == 'output':
+            self.output = base64.b64decode(value)
+        else:
+            setattr(self, name, value)
+
+class InstanceAttribute(dict):
+
+    def __init__(self, parent=None):
+        dict.__init__(self)
+        self._current_value = None
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'value':
+            self._current_value = value
+        else:
+            self[name] = self._current_value
+
+class StateReason(dict):
+
+    def __init__(self, parent=None):
+        dict.__init__(self)
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name != 'stateReason':
+            self[name] = value
+            
diff --git a/boto/ec2/instanceinfo.py b/boto/ec2/instanceinfo.py
new file mode 100644
index 0000000..6efbaed
--- /dev/null
+++ b/boto/ec2/instanceinfo.py
@@ -0,0 +1,47 @@
+# Copyright (c) 2006-2008 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.
+
+class InstanceInfo(object):
+    """
+    Represents an EC2 Instance status response from CloudWatch
+    """
+    
+    def __init__(self, connection=None, id=None, state=None):
+        self.connection = connection
+        self.id = id
+        self.state = state
+
+    def __repr__(self):
+        return 'InstanceInfo:%s' % self.id
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'instanceId' or name == 'InstanceId':
+            self.id = value
+        elif name == 'state':
+            self.state = value
+        else:
+            setattr(self, name, value)
+
+            
+
diff --git a/boto/ec2/keypair.py b/boto/ec2/keypair.py
new file mode 100644
index 0000000..d08e5ce
--- /dev/null
+++ b/boto/ec2/keypair.py
@@ -0,0 +1,111 @@
+# Copyright (c) 2006,2007 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.
+
+"""
+Represents an EC2 Keypair
+"""
+
+import os
+from boto.ec2.ec2object import EC2Object
+from boto.exception import BotoClientError
+
+class KeyPair(EC2Object):
+    
+    def __init__(self, connection=None):
+        EC2Object.__init__(self, connection)
+        self.name = None
+        self.fingerprint = None
+        self.material = None
+
+    def __repr__(self):
+        return 'KeyPair:%s' % self.name
+
+    def endElement(self, name, value, connection):
+        if name == 'keyName':
+            self.name = value
+        elif name == 'keyFingerprint':
+            self.fingerprint = value
+        elif name == 'keyMaterial':
+            self.material = value
+        else:
+            setattr(self, name, value)
+
+    def delete(self):
+        """
+        Delete the KeyPair.
+        
+        :rtype: bool
+        :return: True if successful, otherwise False.
+        """
+        return self.connection.delete_key_pair(self.name)
+
+    def save(self, directory_path):
+        """
+        Save the material (the unencrypted PEM encoded RSA private key)
+        of a newly created KeyPair to a local file.
+        
+        :type directory_path: string
+        :param directory_path: The fully qualified path to the directory
+                               in which the keypair will be saved.  The
+                               keypair file will be named using the name
+                               of the keypair as the base name and .pem
+                               for the file extension.  If a file of that
+                               name already exists in the directory, an
+                               exception will be raised and the old file
+                               will not be overwritten.
+        
+        :rtype: bool
+        :return: True if successful.
+        """
+        if self.material:
+            file_path = os.path.join(directory_path, '%s.pem' % self.name)
+            if os.path.exists(file_path):
+                raise BotoClientError('%s already exists, it will not be overwritten' % file_path)
+            fp = open(file_path, 'wb')
+            fp.write(self.material)
+            fp.close()
+            return True
+        else:
+            raise BotoClientError('KeyPair contains no material')
+
+    def copy_to_region(self, region):
+        """
+        Create a new key pair of the same new in another region.
+        Note that the new key pair will use a different ssh
+        cert than the this key pair.  After doing the copy,
+        you will need to save the material associated with the
+        new key pair (use the save method) to a local file.
+
+        :type region: :class:`boto.ec2.regioninfo.RegionInfo`
+        :param region: The region to which this security group will be copied.
+
+        :rtype: :class:`boto.ec2.keypair.KeyPair`
+        :return: The new key pair
+        """
+        if region.name == self.region:
+            raise BotoClientError('Unable to copy to the same Region')
+        conn_params = self.connection.get_params()
+        rconn = region.connect(**conn_params)
+        kp = rconn.create_key_pair(self.name)
+        return kp
+
+
+
diff --git a/boto/ec2/launchspecification.py b/boto/ec2/launchspecification.py
new file mode 100644
index 0000000..a574a38
--- /dev/null
+++ b/boto/ec2/launchspecification.py
@@ -0,0 +1,96 @@
+# Copyright (c) 2006-2009 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.
+
+"""
+Represents a launch specification for Spot instances.
+"""
+
+from boto.ec2.ec2object import EC2Object
+from boto.resultset import ResultSet
+from boto.ec2.blockdevicemapping import BlockDeviceMapping
+from boto.ec2.instance import Group
+
+class GroupList(list):
+
+    def startElement(self, name, attrs, connection):
+        pass
+
+    def endElement(self, name, value, connection):
+        if name == 'groupId':
+            self.append(value)
+            
+class LaunchSpecification(EC2Object):
+    
+    def __init__(self, connection=None):
+        EC2Object.__init__(self, connection)
+        self.key_name = None
+        self.instance_type = None
+        self.image_id = None
+        self.groups = []
+        self.placement = None
+        self.kernel = None
+        self.ramdisk = None
+        self.monitored = False
+        self.subnet_id = None
+        self._in_monitoring_element = False
+        self.block_device_mapping = None
+
+    def __repr__(self):
+        return 'LaunchSpecification(%s)' % self.image_id
+
+    def startElement(self, name, attrs, connection):
+        if name == 'groupSet':
+            self.groups = ResultSet([('item', Group)])
+            return self.groups
+        elif name == 'monitoring':
+            self._in_monitoring_element = True
+        elif name == 'blockDeviceMapping':
+            self.block_device_mapping = BlockDeviceMapping()
+            return self.block_device_mapping
+        else:
+            return None
+
+    def endElement(self, name, value, connection):
+        if name == 'imageId':
+            self.image_id = value
+        elif name == 'keyName':
+            self.key_name = value
+        elif name == 'instanceType':
+            self.instance_type = value
+        elif name == 'availabilityZone':
+            self.placement = value
+        elif name == 'placement':
+            pass
+        elif name == 'kernelId':
+            self.kernel = value
+        elif name == 'ramdiskId':
+            self.ramdisk = value
+        elif name == 'subnetId':
+            self.subnet_id = value
+        elif name == 'state':
+            if self._in_monitoring_element:
+                if value == 'enabled':
+                    self.monitored = True
+                self._in_monitoring_element = False
+        else:
+            setattr(self, name, value)
+
+
diff --git a/boto/ec2/placementgroup.py b/boto/ec2/placementgroup.py
new file mode 100644
index 0000000..e1bbea6
--- /dev/null
+++ b/boto/ec2/placementgroup.py
@@ -0,0 +1,51 @@
+# 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.
+"""
+Represents an EC2 Placement Group
+"""
+from boto.ec2.ec2object import EC2Object
+from boto.exception import BotoClientError
+
+class PlacementGroup(EC2Object):
+    
+    def __init__(self, connection=None, name=None, strategy=None, state=None):
+        EC2Object.__init__(self, connection)
+        self.name = name
+        self.strategy = strategy
+        self.state = state
+
+    def __repr__(self):
+        return 'PlacementGroup:%s' % self.name
+
+    def endElement(self, name, value, connection):
+        if name == 'groupName':
+            self.name = value
+        elif name == 'strategy':
+            self.strategy = value
+        elif name == 'state':
+            self.state = value
+        else:
+            setattr(self, name, value)
+
+    def delete(self):
+        return self.connection.delete_placement_group(self.name)
+
+
diff --git a/boto/ec2/regioninfo.py b/boto/ec2/regioninfo.py
new file mode 100644
index 0000000..0b37b0e
--- /dev/null
+++ b/boto/ec2/regioninfo.py
@@ -0,0 +1,34 @@
+# Copyright (c) 2006-2010 Mitch Garnaat http://garnaat.org/
+# Copyright (c) 2010, Eucalyptus Systems, Inc.
+# 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.
+
+from boto.regioninfo import RegionInfo
+
+class EC2RegionInfo(RegionInfo):
+    """
+    Represents an EC2 Region
+    """
+    
+    def __init__(self, connection=None, name=None, endpoint=None):
+        from boto.ec2.connection import EC2Connection
+        RegionInfo.__init__(self, connection, name, endpoint,
+                            EC2Connection)
diff --git a/boto/ec2/reservedinstance.py b/boto/ec2/reservedinstance.py
new file mode 100644
index 0000000..1d35c1d
--- /dev/null
+++ b/boto/ec2/reservedinstance.py
@@ -0,0 +1,97 @@
+# Copyright (c) 2006-2009 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.ec2.ec2object import EC2Object
+
+class ReservedInstancesOffering(EC2Object):
+    
+    def __init__(self, connection=None, id=None, instance_type=None,
+                 availability_zone=None, duration=None, fixed_price=None,
+                 usage_price=None, description=None):
+        EC2Object.__init__(self, connection)
+        self.id = id
+        self.instance_type = instance_type
+        self.availability_zone = availability_zone
+        self.duration = duration
+        self.fixed_price = fixed_price
+        self.usage_price = usage_price
+        self.description = description
+
+    def __repr__(self):
+        return 'ReservedInstanceOffering:%s' % self.id
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'reservedInstancesOfferingId':
+            self.id = value
+        elif name == 'instanceType':
+            self.instance_type = value
+        elif name == 'availabilityZone':
+            self.availability_zone = value
+        elif name == 'duration':
+            self.duration = value
+        elif name == 'fixedPrice':
+            self.fixed_price = value
+        elif name == 'usagePrice':
+            self.usage_price = value
+        elif name == 'productDescription':
+            self.description = value
+        else:
+            setattr(self, name, value)
+
+    def describe(self):
+        print 'ID=%s' % self.id
+        print '\tInstance Type=%s' % self.instance_type
+        print '\tZone=%s' % self.availability_zone
+        print '\tDuration=%s' % self.duration
+        print '\tFixed Price=%s' % self.fixed_price
+        print '\tUsage Price=%s' % self.usage_price
+        print '\tDescription=%s' % self.description
+
+    def purchase(self, instance_count=1):
+        return self.connection.purchase_reserved_instance_offering(self.id, instance_count)
+
+class ReservedInstance(ReservedInstancesOffering):
+
+    def __init__(self, connection=None, id=None, instance_type=None,
+                 availability_zone=None, duration=None, fixed_price=None,
+                 usage_price=None, description=None,
+                 instance_count=None, state=None):
+        ReservedInstancesOffering.__init__(self, connection, id, instance_type,
+                                           availability_zone, duration, fixed_price,
+                                           usage_price, description)
+        self.instance_count = instance_count
+        self.state = state
+
+    def __repr__(self):
+        return 'ReservedInstance:%s' % self.id
+
+    def endElement(self, name, value, connection):
+        if name == 'reservedInstancesId':
+            self.id = value
+        if name == 'instanceCount':
+            self.instance_count = int(value)
+        elif name == 'state':
+            self.state = value
+        else:
+            ReservedInstancesOffering.endElement(self, name, value, connection)
diff --git a/boto/ec2/securitygroup.py b/boto/ec2/securitygroup.py
new file mode 100644
index 0000000..24e08c3
--- /dev/null
+++ b/boto/ec2/securitygroup.py
@@ -0,0 +1,286 @@
+# Copyright (c) 2006,2007 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.
+
+"""
+Represents an EC2 Security Group
+"""
+from boto.ec2.ec2object import EC2Object
+from boto.exception import BotoClientError
+
+class SecurityGroup(EC2Object):
+    
+    def __init__(self, connection=None, owner_id=None,
+                 name=None, description=None):
+        EC2Object.__init__(self, connection)
+        self.owner_id = owner_id
+        self.name = name
+        self.description = description
+        self.rules = []
+
+    def __repr__(self):
+        return 'SecurityGroup:%s' % self.name
+
+    def startElement(self, name, attrs, connection):
+        if name == 'item':
+            self.rules.append(IPPermissions(self))
+            return self.rules[-1]
+        else:
+            return None
+
+    def endElement(self, name, value, connection):
+        if name == 'ownerId':
+            self.owner_id = value
+        elif name == 'groupName':
+            self.name = value
+        elif name == 'groupDescription':
+            self.description = value
+        elif name == 'ipRanges':
+            pass
+        elif name == 'return':
+            if value == 'false':
+                self.status = False
+            elif value == 'true':
+                self.status = True
+            else:
+                raise Exception(
+                    'Unexpected value of status %s for group %s'%(
+                        value, 
+                        self.name
+                    )
+                )
+        else:
+            setattr(self, name, value)
+
+    def delete(self):
+        return self.connection.delete_security_group(self.name)
+
+    def add_rule(self, ip_protocol, from_port, to_port,
+                 src_group_name, src_group_owner_id, cidr_ip):
+        """
+        Add a rule to the SecurityGroup object.  Note that this method
+        only changes the local version of the object.  No information
+        is sent to EC2.
+        """
+        rule = IPPermissions(self)
+        rule.ip_protocol = ip_protocol
+        rule.from_port = from_port
+        rule.to_port = to_port
+        self.rules.append(rule)
+        rule.add_grant(src_group_name, src_group_owner_id, cidr_ip)
+
+    def remove_rule(self, ip_protocol, from_port, to_port,
+                    src_group_name, src_group_owner_id, cidr_ip):
+        """
+        Remove a rule to the SecurityGroup object.  Note that this method
+        only changes the local version of the object.  No information
+        is sent to EC2.
+        """
+        target_rule = None
+        for rule in self.rules:
+            if rule.ip_protocol == ip_protocol:
+                if rule.from_port == from_port:
+                    if rule.to_port == to_port:
+                        target_rule = rule
+                        target_grant = None
+                        for grant in rule.grants:
+                            if grant.name == src_group_name:
+                                if grant.owner_id == src_group_owner_id:
+                                    if grant.cidr_ip == cidr_ip:
+                                        target_grant = grant
+                        if target_grant:
+                            rule.grants.remove(target_grant)
+        if len(rule.grants) == 0:
+            self.rules.remove(target_rule)
+
+    def authorize(self, ip_protocol=None, from_port=None, to_port=None,
+                  cidr_ip=None, src_group=None):
+        """
+        Add a new rule to this security group.
+        You need to pass in either src_group_name
+        OR ip_protocol, from_port, to_port,
+        and cidr_ip.  In other words, either you are authorizing another
+        group or you are authorizing some ip-based rule.
+        
+        :type ip_protocol: string
+        :param ip_protocol: Either tcp | udp | icmp
+
+        :type from_port: int
+        :param from_port: The beginning port number you are enabling
+
+        :type to_port: int
+        :param to_port: The ending port number you are enabling
+
+        :type to_port: string
+        :param to_port: The CIDR block you are providing access to.
+                        See http://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing
+
+        :type src_group: :class:`boto.ec2.securitygroup.SecurityGroup` or
+                         :class:`boto.ec2.securitygroup.GroupOrCIDR`
+                         
+        :rtype: bool
+        :return: True if successful.
+        """
+        if src_group:
+            cidr_ip = None
+            src_group_name = src_group.name
+            src_group_owner_id = src_group.owner_id
+        else:
+            src_group_name = None
+            src_group_owner_id = None
+        status = self.connection.authorize_security_group(self.name,
+                                                          src_group_name,
+                                                          src_group_owner_id,
+                                                          ip_protocol,
+                                                          from_port,
+                                                          to_port,
+                                                          cidr_ip)
+        if status:
+            self.add_rule(ip_protocol, from_port, to_port, src_group_name,
+                          src_group_owner_id, cidr_ip)
+        return status
+
+    def revoke(self, ip_protocol=None, from_port=None, to_port=None,
+               cidr_ip=None, src_group=None):
+        if src_group:
+            cidr_ip=None
+            src_group_name = src_group.name
+            src_group_owner_id = src_group.owner_id
+        else:
+            src_group_name = None
+            src_group_owner_id = None
+        status = self.connection.revoke_security_group(self.name,
+                                                       src_group_name,
+                                                       src_group_owner_id,
+                                                       ip_protocol,
+                                                       from_port,
+                                                       to_port,
+                                                       cidr_ip)
+        if status:
+            self.remove_rule(ip_protocol, from_port, to_port, src_group_name,
+                             src_group_owner_id, cidr_ip)
+        return status
+
+    def copy_to_region(self, region, name=None):
+        """
+        Create a copy of this security group in another region.
+        Note that the new security group will be a separate entity
+        and will not stay in sync automatically after the copy
+        operation.
+
+        :type region: :class:`boto.ec2.regioninfo.RegionInfo`
+        :param region: The region to which this security group will be copied.
+
+        :type name: string
+        :param name: The name of the copy.  If not supplied, the copy
+                     will have the same name as this security group.
+        
+        :rtype: :class:`boto.ec2.securitygroup.SecurityGroup`
+        :return: The new security group.
+        """
+        if region.name == self.region:
+            raise BotoClientError('Unable to copy to the same Region')
+        conn_params = self.connection.get_params()
+        rconn = region.connect(**conn_params)
+        sg = rconn.create_security_group(name or self.name, self.description)
+        source_groups = []
+        for rule in self.rules:
+            grant = rule.grants[0]
+            if grant.name:
+                if grant.name not in source_groups:
+                    source_groups.append(grant.name)
+                    sg.authorize(None, None, None, None, grant)
+            else:
+                sg.authorize(rule.ip_protocol, rule.from_port, rule.to_port,
+                             grant.cidr_ip)
+        return sg
+
+    def instances(self):
+        instances = []
+        rs = self.connection.get_all_instances()
+        for reservation in rs:
+            uses_group = [g.id for g in reservation.groups if g.id == self.name]
+            if uses_group:
+                instances.extend(reservation.instances)
+        return instances
+
+class IPPermissions:
+
+    def __init__(self, parent=None):
+        self.parent = parent
+        self.ip_protocol = None
+        self.from_port = None
+        self.to_port = None
+        self.grants = []
+
+    def __repr__(self):
+        return 'IPPermissions:%s(%s-%s)' % (self.ip_protocol,
+                                            self.from_port, self.to_port)
+
+    def startElement(self, name, attrs, connection):
+        if name == 'item':
+            self.grants.append(GroupOrCIDR(self))
+            return self.grants[-1]
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'ipProtocol':
+            self.ip_protocol = value
+        elif name == 'fromPort':
+            self.from_port = value
+        elif name == 'toPort':
+            self.to_port = value
+        else:
+            setattr(self, name, value)
+
+    def add_grant(self, name=None, owner_id=None, cidr_ip=None):
+        grant = GroupOrCIDR(self)
+        grant.owner_id = owner_id
+        grant.name = name
+        grant.cidr_ip = cidr_ip
+        self.grants.append(grant)
+        return grant
+
+class GroupOrCIDR:
+
+    def __init__(self, parent=None):
+        self.owner_id = None
+        self.name = None
+        self.cidr_ip = None
+
+    def __repr__(self):
+        if self.cidr_ip:
+            return '%s' % self.cidr_ip
+        else:
+            return '%s-%s' % (self.name, self.owner_id)
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'userId':
+            self.owner_id = value
+        elif name == 'groupName':
+            self.name = value
+        if name == 'cidrIp':
+            self.cidr_ip = value
+        else:
+            setattr(self, name, value)
+
diff --git a/boto/ec2/snapshot.py b/boto/ec2/snapshot.py
new file mode 100644
index 0000000..bbe8ad4
--- /dev/null
+++ b/boto/ec2/snapshot.py
@@ -0,0 +1,140 @@
+# Copyright (c) 2006-2010 Mitch Garnaat http://garnaat.org/
+# Copyright (c) 2010, Eucalyptus Systems, Inc.
+#
+# 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.
+
+"""
+Represents an EC2 Elastic IP Snapshot
+"""
+from boto.ec2.ec2object import TaggedEC2Object
+
+class Snapshot(TaggedEC2Object):
+    
+    def __init__(self, connection=None):
+        TaggedEC2Object.__init__(self, connection)
+        self.id = None
+        self.volume_id = None
+        self.status = None
+        self.progress = None
+        self.start_time = None
+        self.owner_id = None
+        self.volume_size = None
+        self.description = None
+
+    def __repr__(self):
+        return 'Snapshot:%s' % self.id
+
+    def endElement(self, name, value, connection):
+        if name == 'snapshotId':
+            self.id = value
+        elif name == 'volumeId':
+            self.volume_id = value
+        elif name == 'status':
+            self.status = value
+        elif name == 'startTime':
+            self.start_time = value
+        elif name == 'ownerId':
+            self.owner_id = value
+        elif name == 'volumeSize':
+            try:
+                self.volume_size = int(value)
+            except:
+                self.volume_size = value
+        elif name == 'description':
+            self.description = value
+        else:
+            setattr(self, name, value)
+
+    def _update(self, updated):
+        self.progress = updated.progress
+        self.status = updated.status
+
+    def update(self, validate=False):
+        """
+        Update the data associated with this snapshot by querying EC2.
+
+        :type validate: bool
+        :param validate: By default, if EC2 returns no data about the
+                         snapshot the update method returns quietly.  If
+                         the validate param is True, however, it will
+                         raise a ValueError exception if no data is
+                         returned from EC2.
+        """
+        rs = self.connection.get_all_snapshots([self.id])
+        if len(rs) > 0:
+            self._update(rs[0])
+        elif validate:
+            raise ValueError('%s is not a valid Snapshot ID' % self.id)
+        return self.progress
+    
+    def delete(self):
+        return self.connection.delete_snapshot(self.id)
+
+    def get_permissions(self):
+        attrs = self.connection.get_snapshot_attribute(self.id,
+                                                       attribute='createVolumePermission')
+        return attrs.attrs
+
+    def share(self, user_ids=None, groups=None):
+        return self.connection.modify_snapshot_attribute(self.id,
+                                                         'createVolumePermission',
+                                                         'add',
+                                                         user_ids,
+                                                         groups)
+
+    def unshare(self, user_ids=None, groups=None):
+        return self.connection.modify_snapshot_attribute(self.id,
+                                                         'createVolumePermission',
+                                                         'remove',
+                                                         user_ids,
+                                                         groups)
+
+    def reset_permissions(self):
+        return self.connection.reset_snapshot_attribute(self.id, 'createVolumePermission')
+
+class SnapshotAttribute:
+
+    def __init__(self, parent=None):
+        self.snapshot_id = None
+        self.attrs = {}
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'createVolumePermission':
+            self.name = 'create_volume_permission'
+        elif name == 'group':
+            if self.attrs.has_key('groups'):
+                self.attrs['groups'].append(value)
+            else:
+                self.attrs['groups'] = [value]
+        elif name == 'userId':
+            if self.attrs.has_key('user_ids'):
+                self.attrs['user_ids'].append(value)
+            else:
+                self.attrs['user_ids'] = [value]
+        elif name == 'snapshotId':
+            self.snapshot_id = value
+        else:
+            setattr(self, name, value)
+
+
+            
diff --git a/boto/ec2/spotdatafeedsubscription.py b/boto/ec2/spotdatafeedsubscription.py
new file mode 100644
index 0000000..9b820a3
--- /dev/null
+++ b/boto/ec2/spotdatafeedsubscription.py
@@ -0,0 +1,63 @@
+# Copyright (c) 2006-2009 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.
+
+"""
+Represents an EC2 Spot Instance Datafeed Subscription
+"""
+from boto.ec2.ec2object import EC2Object
+from boto.ec2.spotinstancerequest import SpotInstanceStateFault
+
+class SpotDatafeedSubscription(EC2Object):
+    
+    def __init__(self, connection=None, owner_id=None,
+                 bucket=None, prefix=None, state=None,fault=None):
+        EC2Object.__init__(self, connection)
+        self.owner_id = owner_id
+        self.bucket = bucket
+        self.prefix = prefix
+        self.state = state
+        self.fault = fault
+
+    def __repr__(self):
+        return 'SpotDatafeedSubscription:%s' % self.bucket
+
+    def startElement(self, name, attrs, connection):
+        if name == 'fault':
+            self.fault = SpotInstanceStateFault()
+            return self.fault
+        else:
+            return None
+        
+    def endElement(self, name, value, connection):
+        if name == 'ownerId':
+            self.owner_id = value
+        elif name == 'bucket':
+            self.bucket = value
+        elif name == 'prefix':
+            self.prefix = value
+        elif name == 'state':
+            self.state = value
+        else:
+            setattr(self, name, value)
+
+    def delete(self):
+        return self.connection.delete_spot_datafeed_subscription()
+
diff --git a/boto/ec2/spotinstancerequest.py b/boto/ec2/spotinstancerequest.py
new file mode 100644
index 0000000..06acb0f
--- /dev/null
+++ b/boto/ec2/spotinstancerequest.py
@@ -0,0 +1,113 @@
+# Copyright (c) 2006-2010 Mitch Garnaat http://garnaat.org/
+# Copyright (c) 2010, Eucalyptus Systems, Inc.
+#
+# 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.
+
+"""
+Represents an EC2 Spot Instance Request
+"""
+
+from boto.ec2.ec2object import TaggedEC2Object
+from boto.ec2.launchspecification import LaunchSpecification
+
+class SpotInstanceStateFault(object):
+
+    def __init__(self, code=None, message=None):
+        self.code = code
+        self.message = message
+
+    def __repr__(self):
+        return '(%s, %s)' % (self.code, self.message)
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'code':
+            self.code = value
+        elif name == 'message':
+            self.message = value
+        setattr(self, name, value)
+
+class SpotInstanceRequest(TaggedEC2Object):
+    
+    def __init__(self, connection=None):
+        TaggedEC2Object.__init__(self, connection)
+        self.id = None
+        self.price = None
+        self.type = None
+        self.state = None
+        self.fault = None
+        self.valid_from = None
+        self.valid_until = None
+        self.launch_group = None
+        self.product_description = None
+        self.availability_zone_group = None
+        self.create_time = None
+        self.launch_specification = None
+        self.instance_id = None
+
+    def __repr__(self):
+        return 'SpotInstanceRequest:%s' % self.id
+
+    def startElement(self, name, attrs, connection):
+        retval = TaggedEC2Object.startElement(self, name, attrs, connection)
+        if retval is not None:
+            return retval
+        if name == 'launchSpecification':
+            self.launch_specification = LaunchSpecification(connection)
+            return self.launch_specification
+        elif name == 'fault':
+            self.fault = SpotInstanceStateFault()
+            return self.fault
+        else:
+            return None
+
+    def endElement(self, name, value, connection):
+        if name == 'spotInstanceRequestId':
+            self.id = value
+        elif name == 'spotPrice':
+            self.price = float(value)
+        elif name == 'type':
+            self.type = value
+        elif name == 'state':
+            self.state = value
+        elif name == 'productDescription':
+            self.product_description = value
+        elif name == 'validFrom':
+            self.valid_from = value
+        elif name == 'validUntil':
+            self.valid_until = value
+        elif name == 'launchGroup':
+            self.launch_group = value
+        elif name == 'availabilityZoneGroup':
+            self.availability_zone_group = value
+        elif name == 'createTime':
+            self.create_time = value
+        elif name == 'instanceId':
+            self.instance_id = value
+        else:
+            setattr(self, name, value)
+
+    def cancel(self):
+        self.connection.cancel_spot_instance_requests([self.id])
+
+
+    
diff --git a/boto/ec2/spotpricehistory.py b/boto/ec2/spotpricehistory.py
new file mode 100644
index 0000000..d4e1711
--- /dev/null
+++ b/boto/ec2/spotpricehistory.py
@@ -0,0 +1,52 @@
+# Copyright (c) 2006-2009 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.
+
+"""
+Represents an EC2 Spot Instance Request
+"""
+
+from boto.ec2.ec2object import EC2Object
+
+class SpotPriceHistory(EC2Object):
+    
+    def __init__(self, connection=None):
+        EC2Object.__init__(self, connection)
+        self.price = 0.0
+        self.instance_type = None
+        self.product_description = None
+        self.timestamp = None
+
+    def __repr__(self):
+        return 'SpotPriceHistory(%s):%2f' % (self.instance_type, self.price)
+
+    def endElement(self, name, value, connection):
+        if name == 'instanceType':
+            self.instance_type = value
+        elif name == 'spotPrice':
+            self.price = float(value)
+        elif name == 'productDescription':
+            self.product_description = value
+        elif name == 'timestamp':
+            self.timestamp = value
+        else:
+            setattr(self, name, value)
+
+
diff --git a/boto/ec2/tag.py b/boto/ec2/tag.py
new file mode 100644
index 0000000..8032e6f
--- /dev/null
+++ b/boto/ec2/tag.py
@@ -0,0 +1,87 @@
+# Copyright (c) 2010 Mitch Garnaat http://garnaat.org/
+# Copyright (c) 2010, Eucalyptus Systems, Inc.
+#
+# 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.
+
+class TagSet(dict):
+    """
+    A TagSet is used to collect the tags associated with a particular
+    EC2 resource.  Not all resources can be tagged but for those that
+    can, this dict object will be used to collect those values.  See
+    :class:`boto.ec2.ec2object.TaggedEC2Object` for more details.
+    """
+    
+    def __init__(self, connection=None):
+        self.connection = connection
+        self._current_key = None
+        self._current_value = None
+
+    def startElement(self, name, attrs, connection):
+        if name == 'item':
+            self._current_key = None
+            self._current_value = None
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'key':
+            self._current_key = value
+        elif name == 'value':
+            self._current_value = value
+        elif name == 'item':
+            self[self._current_key] = self._current_value
+
+
+class Tag(object):
+    """
+    A Tag is used when creating or listing all tags related to
+    an AWS account.  It records not only the key and value but
+    also the ID of the resource to which the tag is attached
+    as well as the type of the resource.
+    """
+    
+    def __init__(self, connection=None, res_id=None, res_type=None,
+                 name=None, value=None):
+        self.connection = connection
+        self.res_id = res_id
+        self.res_type = res_type
+        self.name = name
+        self.value = value
+
+    def __repr__(self):
+        return 'Tag:%s' % self.name
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'resourceId':
+            self.res_id = value
+        elif name == 'resourceType':
+            self.res_type = value
+        elif name == 'key':
+            self.name = value
+        elif name == 'value':
+            self.value = value
+        else:
+            setattr(self, name, value)
+
+
+            
+
diff --git a/boto/ec2/volume.py b/boto/ec2/volume.py
new file mode 100644
index 0000000..45345fa
--- /dev/null
+++ b/boto/ec2/volume.py
@@ -0,0 +1,227 @@
+# Copyright (c) 2006-2010 Mitch Garnaat http://garnaat.org/
+# Copyright (c) 2010, Eucalyptus Systems, Inc.
+#
+# 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.
+
+"""
+Represents an EC2 Elastic Block Storage Volume
+"""
+from boto.ec2.ec2object import TaggedEC2Object
+
+class Volume(TaggedEC2Object):
+    
+    def __init__(self, connection=None):
+        TaggedEC2Object.__init__(self, connection)
+        self.id = None
+        self.create_time = None
+        self.status = None
+        self.size = None
+        self.snapshot_id = None
+        self.attach_data = None
+        self.zone = None
+
+    def __repr__(self):
+        return 'Volume:%s' % self.id
+
+    def startElement(self, name, attrs, connection):
+        retval = TaggedEC2Object.startElement(self, name, attrs, connection)
+        if retval is not None:
+            return retval
+        if name == 'attachmentSet':
+            self.attach_data = AttachmentSet()
+            return self.attach_data
+        elif name == 'tagSet':
+            self.tags = boto.resultset.ResultSet([('item', Tag)])
+            return self.tags
+        else:
+            return None
+
+    def endElement(self, name, value, connection):
+        if name == 'volumeId':
+            self.id = value
+        elif name == 'createTime':
+            self.create_time = value
+        elif name == 'status':
+            if value != '':
+                self.status = value
+        elif name == 'size':
+            self.size = int(value)
+        elif name == 'snapshotId':
+            self.snapshot_id = value
+        elif name == 'availabilityZone':
+            self.zone = value
+        else:
+            setattr(self, name, value)
+
+    def _update(self, updated):
+        self.__dict__.update(updated.__dict__)
+
+    def update(self, validate=False):
+        """
+        Update the data associated with this volume by querying EC2.
+
+        :type validate: bool
+        :param validate: By default, if EC2 returns no data about the
+                         volume the update method returns quietly.  If
+                         the validate param is True, however, it will
+                         raise a ValueError exception if no data is
+                         returned from EC2.
+        """
+        rs = self.connection.get_all_volumes([self.id])
+        if len(rs) > 0:
+            self._update(rs[0])
+        elif validate:
+            raise ValueError('%s is not a valid Volume ID' % self.id)
+        return self.status
+
+    def delete(self):
+        """
+        Delete this EBS volume.
+
+        :rtype: bool
+        :return: True if successful
+        """
+        return self.connection.delete_volume(self.id)
+
+    def attach(self, instance_id, device):
+        """
+        Attach this EBS volume to an EC2 instance.
+
+        :type instance_id: str
+        :param instance_id: The ID of the EC2 instance to which it will
+                            be attached.
+
+        :type device: str
+        :param device: The device on the instance through which the
+                       volume will be exposted (e.g. /dev/sdh)
+
+        :rtype: bool
+        :return: True if successful
+        """
+        return self.connection.attach_volume(self.id, instance_id, device)
+
+    def detach(self, force=False):
+        """
+        Detach this EBS volume from an EC2 instance.
+
+        :type force: bool
+        :param force: Forces detachment if the previous detachment attempt did
+                      not occur cleanly.  This option can lead to data loss or
+                      a corrupted file system. Use this option only as a last
+                      resort to detach a volume from a failed instance. The
+                      instance will not have an opportunity to flush file system
+                      caches nor file system meta data. If you use this option,
+                      you must perform file system check and repair procedures.
+
+        :rtype: bool
+        :return: True if successful
+        """
+        instance_id = None
+        if self.attach_data:
+            instance_id = self.attach_data.instance_id
+        device = None
+        if self.attach_data:
+            device = self.attach_data.device
+        return self.connection.detach_volume(self.id, instance_id, device, force)
+
+    def create_snapshot(self, description=None):
+        """
+        Create a snapshot of this EBS Volume.
+
+        :type description: str
+        :param description: A description of the snapshot.  Limited to 256 characters.
+        
+        :rtype: bool
+        :return: True if successful
+        """
+        return self.connection.create_snapshot(self.id, description)
+
+    def volume_state(self):
+        """
+        Returns the state of the volume.  Same value as the status attribute.
+        """
+        return self.status
+
+    def attachment_state(self):
+        """
+        Get the attachment state.
+        """
+        state = None
+        if self.attach_data:
+            state = self.attach_data.status
+        return state
+
+    def snapshots(self, owner=None, restorable_by=None):
+        """
+        Get all snapshots related to this volume.  Note that this requires
+        that all available snapshots for the account be retrieved from EC2
+        first and then the list is filtered client-side to contain only
+        those for this volume.
+
+        :type owner: str
+        :param owner: If present, only the snapshots owned by the specified user
+                      will be returned.  Valid values are:
+                      self | amazon | AWS Account ID
+
+        :type restorable_by: str
+        :param restorable_by: If present, only the snapshots that are restorable
+                              by the specified account id will be returned.
+
+        :rtype: list of L{boto.ec2.snapshot.Snapshot}
+        :return: The requested Snapshot objects
+        
+        """
+        rs = self.connection.get_all_snapshots(owner=owner,
+                                               restorable_by=restorable_by)
+        mine = []
+        for snap in rs:
+            if snap.volume_id == self.id:
+                mine.append(snap)
+        return mine
+
+class AttachmentSet(object):
+    
+    def __init__(self):
+        self.id = None
+        self.instance_id = None
+        self.status = None
+        self.attach_time = None
+        self.device = None
+
+    def __repr__(self):
+        return 'AttachmentSet:%s' % self.id
+
+    def startElement(self, name, attrs, connection):
+        pass
+    
+    def endElement(self, name, value, connection):
+        if name == 'volumeId':
+            self.id = value
+        elif name == 'instanceId':
+            self.instance_id = value
+        elif name == 'status':
+            self.status = value
+        elif name == 'attachTime':
+            self.attach_time = value
+        elif name == 'device':
+            self.device = value
+        else:
+            setattr(self, name, value)
+
diff --git a/boto/ec2/zone.py b/boto/ec2/zone.py
new file mode 100644
index 0000000..aec79b2
--- /dev/null
+++ b/boto/ec2/zone.py
@@ -0,0 +1,47 @@
+# Copyright (c) 2006-2008 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.
+
+"""
+Represents an EC2 Availability Zone
+"""
+from boto.ec2.ec2object import EC2Object
+
+class Zone(EC2Object):
+    
+    def __init__(self, connection=None):
+        EC2Object.__init__(self, connection)
+        self.name = None
+        self.state = None
+
+    def __repr__(self):
+        return 'Zone:%s' % self.name
+
+    def endElement(self, name, value, connection):
+        if name == 'zoneName':
+            self.name = value
+        elif name == 'zoneState':
+            self.state = value
+        else:
+            setattr(self, name, value)
+
+
+
+
diff --git a/boto/ecs/__init__.py b/boto/ecs/__init__.py
new file mode 100644
index 0000000..db86dd5
--- /dev/null
+++ b/boto/ecs/__init__.py
@@ -0,0 +1,84 @@
+# 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-11-01'
+
+    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 _required_auth_capability(self):
+        return ['ecs']
+
+    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(None, 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 0000000..29588b8
--- /dev/null
+++ b/boto/ecs/item.py
@@ -0,0 +1,153 @@
+# 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
+        self.total_results = 0
+        self.total_pages = 0
+
+    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/emr/__init__.py b/boto/emr/__init__.py
new file mode 100644
index 0000000..3c33f9a
--- /dev/null
+++ b/boto/emr/__init__.py
@@ -0,0 +1,30 @@
+# Copyright (c) 2010 Spotify AB
+#
+# 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.
+
+"""
+This module provies an interface to the Elastic MapReduce (EMR)
+service from AWS.
+"""
+from connection import EmrConnection
+from step import Step, StreamingStep, JarStep
+from bootstrap_action import BootstrapAction
+
+
diff --git a/boto/emr/bootstrap_action.py b/boto/emr/bootstrap_action.py
new file mode 100644
index 0000000..c1c9038
--- /dev/null
+++ b/boto/emr/bootstrap_action.py
@@ -0,0 +1,43 @@
+# Copyright (c) 2010 Spotify AB
+#
+# 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.
+
+class BootstrapAction(object):
+    def __init__(self, name, path, bootstrap_action_args):
+        self.name = name
+        self.path = path
+
+        if isinstance(bootstrap_action_args, basestring):
+            bootstrap_action_args = [bootstrap_action_args]
+
+        self.bootstrap_action_args = bootstrap_action_args
+
+    def args(self):
+        args = []
+
+        if self.bootstrap_action_args:
+            args.extend(self.bootstrap_action_args)
+
+        return args
+
+    def __repr__(self):
+        return '%s.%s(name=%r, path=%r, bootstrap_action_args=%r)' % (
+            self.__class__.__module__, self.__class__.__name__,
+            self.name, self.path, self.bootstrap_action_args)
diff --git a/boto/emr/connection.py b/boto/emr/connection.py
new file mode 100644
index 0000000..2bfd368
--- /dev/null
+++ b/boto/emr/connection.py
@@ -0,0 +1,280 @@
+# Copyright (c) 2010 Spotify AB
+#
+# 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.
+
+"""
+Represents a connection to the EMR service
+"""
+import types
+
+import boto
+from boto.ec2.regioninfo import RegionInfo
+from boto.emr.emrobject import JobFlow, RunJobFlowResponse
+from boto.emr.step import JarStep
+from boto.connection import AWSQueryConnection
+from boto.exception import EmrResponseError
+
+class EmrConnection(AWSQueryConnection):
+
+    APIVersion = boto.config.get('Boto', 'emr_version', '2009-03-31')
+    DefaultRegionName = boto.config.get('Boto', 'emr_region_name', 'us-east-1')
+    DefaultRegionEndpoint = boto.config.get('Boto', 'emr_region_endpoint',
+                                            'elasticmapreduce.amazonaws.com')
+    ResponseError = EmrResponseError
+
+    # Constants for AWS Console debugging
+    DebuggingJar = 's3n://us-east-1.elasticmapreduce/libs/script-runner/script-runner.jar'
+    DebuggingArgs = 's3n://us-east-1.elasticmapreduce/libs/state-pusher/0.1/fetch'
+
+    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, debug=0,
+                 https_connection_factory=None, region=None, path='/'):
+        if not region:
+            region = RegionInfo(self, self.DefaultRegionName, self.DefaultRegionEndpoint)
+        self.region = region
+        AWSQueryConnection.__init__(self, aws_access_key_id,
+                                    aws_secret_access_key,
+                                    is_secure, port, proxy, proxy_port,
+                                    proxy_user, proxy_pass,
+                                    self.region.endpoint, debug,
+                                    https_connection_factory, path)
+
+    def _required_auth_capability(self):
+        return ['emr']
+
+    def describe_jobflow(self, jobflow_id):
+        """
+        Describes a single Elastic MapReduce job flow
+
+        :type jobflow_id: str
+        :param jobflow_id: The job flow id of interest
+        """
+        jobflows = self.describe_jobflows(jobflow_ids=[jobflow_id])
+        if jobflows:
+            return jobflows[0]
+
+    def describe_jobflows(self, states=None, jobflow_ids=None,
+                           created_after=None, created_before=None):
+        """
+        Retrieve all the Elastic MapReduce job flows on your account
+
+        :type states: list
+        :param states: A list of strings with job flow states wanted
+
+        :type jobflow_ids: list
+        :param jobflow_ids: A list of job flow IDs
+        :type created_after: datetime
+        :param created_after: Bound on job flow creation time
+
+        :type created_before: datetime
+        :param created_before: Bound on job flow creation time
+        """
+        params = {}
+
+        if states:
+            self.build_list_params(params, states, 'JobFlowStates.member')
+        if jobflow_ids:
+            self.build_list_params(params, jobflow_ids, 'JobFlowIds.member')
+        if created_after:
+            params['CreatedAfter'] = created_after.strftime('%Y-%m-%dT%H:%M:%S')
+        if created_before:
+            params['CreatedBefore'] = created_before.strftime('%Y-%m-%dT%H:%M:%S')
+
+        return self.get_list('DescribeJobFlows', params, [('member', JobFlow)])
+
+    def terminate_jobflow(self, jobflow_id):
+        """
+        Terminate an Elastic MapReduce job flow
+
+        :type jobflow_id: str
+        :param jobflow_id: A jobflow id 
+        """
+        self.terminate_jobflows([jobflow_id]) 
+
+    def terminate_jobflows(self, jobflow_ids):
+        """
+        Terminate an Elastic MapReduce job flow
+
+        :type jobflow_ids: list
+        :param jobflow_ids: A list of job flow IDs
+        """
+        params = {}
+        self.build_list_params(params, jobflow_ids, 'JobFlowIds.member')
+        return self.get_status('TerminateJobFlows', params)
+
+    def add_jobflow_steps(self, jobflow_id, steps):
+        """
+        Adds steps to a jobflow
+
+        :type jobflow_id: str
+        :param jobflow_id: The job flow id
+        :type steps: list(boto.emr.Step)
+        :param steps: A list of steps to add to the job
+        """
+        if type(steps) != types.ListType:
+            steps = [steps]
+        params = {}
+        params['JobFlowId'] = jobflow_id
+
+        # Step args
+        step_args = [self._build_step_args(step) for step in steps]
+        params.update(self._build_step_list(step_args))
+
+        return self.get_object('AddJobFlowSteps', params, RunJobFlowResponse)
+
+    def run_jobflow(self, name, log_uri, ec2_keyname=None, availability_zone=None,
+                    master_instance_type='m1.small',
+                    slave_instance_type='m1.small', num_instances=1,
+                    action_on_failure='TERMINATE_JOB_FLOW', keep_alive=False,
+                    enable_debugging=False,
+                    hadoop_version='0.18',
+                    steps=[],
+                    bootstrap_actions=[]):
+        """
+        Runs a job flow
+
+        :type name: str
+        :param name: Name of the job flow
+        :type log_uri: str
+        :param log_uri: URI of the S3 bucket to place logs
+        :type ec2_keyname: str
+        :param ec2_keyname: EC2 key used for the instances
+        :type availability_zone: str
+        :param availability_zone: EC2 availability zone of the cluster
+        :type master_instance_type: str
+        :param master_instance_type: EC2 instance type of the master
+        :type slave_instance_type: str
+        :param slave_instance_type: EC2 instance type of the slave nodes
+        :type num_instances: int
+        :param num_instances: Number of instances in the Hadoop cluster
+        :type action_on_failure: str
+        :param action_on_failure: Action to take if a step terminates
+        :type keep_alive: bool
+        :param keep_alive: Denotes whether the cluster should stay alive upon completion
+        :type enable_debugging: bool
+        :param enable_debugging: Denotes whether AWS console debugging should be enabled.
+        :type steps: list(boto.emr.Step)
+        :param steps: List of steps to add with the job
+
+        :rtype: str
+        :return: The jobflow id
+        """
+        params = {}
+        if action_on_failure:
+            params['ActionOnFailure'] = action_on_failure
+        params['Name'] = name
+        params['LogUri'] = log_uri
+
+        # Instance args
+        instance_params = self._build_instance_args(ec2_keyname, availability_zone,
+                                                    master_instance_type, slave_instance_type,
+                                                    num_instances, keep_alive, hadoop_version)
+        params.update(instance_params)
+
+        # Debugging step from EMR API docs
+        if enable_debugging:
+            debugging_step = JarStep(name='Setup Hadoop Debugging',
+                                     action_on_failure='TERMINATE_JOB_FLOW',
+                                     main_class=None,
+                                     jar=self.DebuggingJar,
+                                     step_args=self.DebuggingArgs)
+            steps.insert(0, debugging_step)
+
+        # Step args
+        if steps:
+            step_args = [self._build_step_args(step) for step in steps]
+            params.update(self._build_step_list(step_args))
+
+        if bootstrap_actions:
+            bootstrap_action_args = [self._build_bootstrap_action_args(bootstrap_action) for bootstrap_action in bootstrap_actions]
+            params.update(self._build_bootstrap_action_list(bootstrap_action_args))
+
+        response = self.get_object('RunJobFlow', params, RunJobFlowResponse)
+        return response.jobflowid
+
+    def _build_bootstrap_action_args(self, bootstrap_action):
+        bootstrap_action_params = {}
+        bootstrap_action_params['ScriptBootstrapAction.Path'] = bootstrap_action.path
+
+        try:
+            bootstrap_action_params['Name'] = bootstrap_action.name
+        except AttributeError:
+            pass
+
+        args = bootstrap_action.args()
+        if args:
+            self.build_list_params(bootstrap_action_params, args, 'ScriptBootstrapAction.Args.member')
+
+        return bootstrap_action_params
+
+    def _build_step_args(self, step):
+        step_params = {}
+        step_params['ActionOnFailure'] = step.action_on_failure
+        step_params['HadoopJarStep.Jar'] = step.jar()
+
+        main_class = step.main_class()
+        if main_class:
+            step_params['HadoopJarStep.MainClass'] = main_class
+
+        args = step.args()
+        if args:
+            self.build_list_params(step_params, args, 'HadoopJarStep.Args.member')
+
+        step_params['Name'] = step.name
+        return step_params
+
+    def _build_bootstrap_action_list(self, bootstrap_actions):
+        if type(bootstrap_actions) != types.ListType:
+            bootstrap_actions = [bootstrap_actions]
+
+        params = {}
+        for i, bootstrap_action in enumerate(bootstrap_actions):
+            for key, value in bootstrap_action.iteritems():
+                params['BootstrapActions.memeber.%s.%s' % (i + 1, key)] = value
+        return params
+
+    def _build_step_list(self, steps):
+        if type(steps) != types.ListType:
+            steps = [steps]
+
+        params = {}
+        for i, step in enumerate(steps):
+            for key, value in step.iteritems():
+                params['Steps.memeber.%s.%s' % (i+1, key)] = value
+        return params
+
+    def _build_instance_args(self, ec2_keyname, availability_zone, master_instance_type,
+                             slave_instance_type, num_instances, keep_alive, hadoop_version):
+        params = {
+            'Instances.MasterInstanceType' : master_instance_type,
+            'Instances.SlaveInstanceType' : slave_instance_type,
+            'Instances.InstanceCount' : num_instances,
+            'Instances.KeepJobFlowAliveWhenNoSteps' : str(keep_alive).lower(),
+            'Instances.HadoopVersion' : hadoop_version
+        }
+
+        if ec2_keyname:
+            params['Instances.Ec2KeyName'] = ec2_keyname
+        if availability_zone:
+            params['Placement'] = availability_zone
+
+        return params
+
diff --git a/boto/emr/emrobject.py b/boto/emr/emrobject.py
new file mode 100644
index 0000000..0ffe292
--- /dev/null
+++ b/boto/emr/emrobject.py
@@ -0,0 +1,141 @@
+# Copyright (c) 2010 Spotify AB
+# Copyright (c) 2010 Jeremy Thurgood <firxen+boto@gmail.com>
+#
+# 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.
+
+"""
+This module contains EMR response objects
+"""
+
+from boto.resultset import ResultSet
+
+
+class EmrObject(object):
+    Fields = set()
+
+    def __init__(self, connection=None):
+        self.connection = connection
+
+    def startElement(self, name, attrs, connection):
+        pass
+
+    def endElement(self, name, value, connection):
+        if name in self.Fields:
+            setattr(self, name.lower(), value)
+
+
+class RunJobFlowResponse(EmrObject):
+    Fields = set(['JobFlowId'])
+
+
+class Arg(EmrObject):
+    def __init__(self, connection=None):
+        self.value = None
+
+    def endElement(self, name, value, connection):
+        self.value = value
+
+
+class BootstrapAction(EmrObject):
+    Fields = set(['Name',
+                  'Args',
+                  'Path'])
+
+
+class Step(EmrObject):
+    Fields = set(['Name',
+                  'ActionOnFailure',
+                  'CreationDateTime',
+                  'StartDateTime',
+                  'EndDateTime',
+                  'LastStateChangeReason',
+                  'State'])
+
+    def __init__(self, connection=None):
+        self.connection = connection
+        self.args = None
+
+    def startElement(self, name, attrs, connection):
+        if name == 'Args':
+            self.args = ResultSet([('member', Arg)])
+            return self.args
+
+
+class InstanceGroup(EmrObject):
+    Fields = set(['Name',
+                  'CreationDateTime',
+                  'InstanceRunningCount',
+                  'StartDateTime',
+                  'ReadyDateTime',
+                  'State',
+                  'EndDateTime',
+                  'InstanceRequestCount',
+                  'InstanceType',
+                  'Market',
+                  'LastStateChangeReason',
+                  'InstanceRole',
+                  'InstanceGroupId',
+                  'LaunchGroup',
+                  'SpotPrice'])
+
+
+class JobFlow(EmrObject):
+    Fields = set(['CreationDateTime',
+                  'StartDateTime',
+                  'State',
+                  'EndDateTime',
+                  'Id',
+                  'InstanceCount',
+                  'JobFlowId',
+                  'LogUri',
+                  'MasterPublicDnsName',
+                  'MasterInstanceId',
+                  'Name',
+                  'Placement',
+                  'RequestId',
+                  'Type',
+                  'Value',
+                  'AvailabilityZone',
+                  'SlaveInstanceType',
+                  'MasterInstanceType',
+                  'Ec2KeyName',
+                  'InstanceCount',
+                  'KeepJobFlowAliveWhenNoSteps',
+                  'LastStateChangeReason'])
+
+    def __init__(self, connection=None):
+        self.connection = connection
+        self.steps = None
+        self.instancegroups = None
+        self.bootstrapactions = None
+
+    def startElement(self, name, attrs, connection):
+        if name == 'Steps':
+            self.steps = ResultSet([('member', Step)])
+            return self.steps
+        elif name == 'InstanceGroups':
+            self.instancegroups = ResultSet([('member', InstanceGroup)])
+            return self.instancegroups
+        elif name == 'BootstrapActions':
+            self.bootstrapactions = ResultSet([('member', BootstrapAction)])
+            return self.bootstrapactions
+        else:
+            return None
+
diff --git a/boto/emr/step.py b/boto/emr/step.py
new file mode 100644
index 0000000..a444261
--- /dev/null
+++ b/boto/emr/step.py
@@ -0,0 +1,179 @@
+# Copyright (c) 2010 Spotify AB
+#
+# 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.
+
+class Step(object):
+    """
+    Jobflow Step base class
+    """
+    def jar(self):
+        """
+        :rtype: str
+        :return: URI to the jar
+        """
+        raise NotImplemented()
+
+    def args(self):
+        """
+        :rtype: list(str)
+        :return: List of arguments for the step
+        """
+        raise NotImplemented()
+
+    def main_class(self):
+        """
+        :rtype: str
+        :return: The main class name
+        """
+        raise NotImplemented()
+
+
+class JarStep(Step):
+    """
+    Custom jar step
+    """
+    def __init__(self, name, jar, main_class=None,
+                 action_on_failure='TERMINATE_JOB_FLOW', step_args=None):
+        """
+        A elastic mapreduce step that executes a jar
+
+        :type name: str
+        :param name: The name of the step
+        :type jar: str
+        :param jar: S3 URI to the Jar file
+        :type main_class: str
+        :param main_class: The class to execute in the jar
+        :type action_on_failure: str
+        :param action_on_failure: An action, defined in the EMR docs to take on failure.
+        :type step_args: list(str)
+        :param step_args: A list of arguments to pass to the step
+        """
+        self.name = name
+        self._jar = jar
+        self._main_class = main_class
+        self.action_on_failure = action_on_failure
+
+        if isinstance(step_args, basestring):
+            step_args = [step_args]
+
+        self.step_args = step_args
+
+    def jar(self):
+        return self._jar
+
+    def args(self):
+        args = []
+
+        if self.step_args:
+            args.extend(self.step_args)
+
+        return args
+
+    def main_class(self):
+        return self._main_class
+
+
+class StreamingStep(Step):
+    """
+    Hadoop streaming step
+    """
+    def __init__(self, name, mapper, reducer=None,
+                 action_on_failure='TERMINATE_JOB_FLOW',
+                 cache_files=None, cache_archives=None,
+                 step_args=None, input=None, output=None):
+        """
+        A hadoop streaming elastic mapreduce step
+
+        :type name: str
+        :param name: The name of the step
+        :type mapper: str
+        :param mapper: The mapper URI
+        :type reducer: str
+        :param reducer: The reducer URI
+        :type action_on_failure: str
+        :param action_on_failure: An action, defined in the EMR docs to take on failure.
+        :type cache_files: list(str)
+        :param cache_files: A list of cache files to be bundled with the job
+        :type cache_archives: list(str)
+        :param cache_archives: A list of jar archives to be bundled with the job
+        :type step_args: list(str)
+        :param step_args: A list of arguments to pass to the step
+        :type input: str or a list of str
+        :param input: The input uri
+        :type output: str
+        :param output: The output uri
+        """
+        self.name = name
+        self.mapper = mapper
+        self.reducer = reducer
+        self.action_on_failure = action_on_failure
+        self.cache_files = cache_files
+        self.cache_archives = cache_archives
+        self.input = input
+        self.output = output
+
+        if isinstance(step_args, basestring):
+            step_args = [step_args]
+
+        self.step_args = step_args
+
+    def jar(self):
+        return '/home/hadoop/contrib/streaming/hadoop-0.18-streaming.jar'
+
+    def main_class(self):
+        return None
+
+    def args(self):
+        args = ['-mapper', self.mapper]
+
+        if self.reducer:
+            args.extend(['-reducer', self.reducer])
+
+        if self.input:
+            if isinstance(self.input, list):
+                for input in self.input:
+                    args.extend(('-input', input))
+            else:
+                args.extend(('-input', self.input))
+        if self.output:
+            args.extend(('-output', self.output))
+
+        if self.cache_files:
+            for cache_file in self.cache_files:
+                args.extend(('-cacheFile', cache_file))
+
+        if self.cache_archives:
+           for cache_archive in self.cache_archives:
+                args.extend(('-cacheArchive', cache_archive))
+
+        if self.step_args:
+            args.extend(self.step_args)
+
+        if not self.reducer:
+            args.extend(['-jobconf', 'mapred.reduce.tasks=0'])
+
+        return args
+
+    def __repr__(self):
+        return '%s.%s(name=%r, mapper=%r, reducer=%r, action_on_failure=%r, cache_files=%r, cache_archives=%r, step_args=%r, input=%r, output=%r)' % (
+            self.__class__.__module__, self.__class__.__name__,
+            self.name, self.mapper, self.reducer, self.action_on_failure,
+            self.cache_files, self.cache_archives, self.step_args,
+            self.input, self.output)
diff --git a/boto/emr/tests/test_emr_responses.py b/boto/emr/tests/test_emr_responses.py
new file mode 100644
index 0000000..77ec494
--- /dev/null
+++ b/boto/emr/tests/test_emr_responses.py
@@ -0,0 +1,373 @@
+# Copyright (c) 2010 Jeremy Thurgood <firxen+boto@gmail.com>
+#
+# 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.
+
+
+# NOTE: These tests only cover the very simple cases I needed to test
+# for the InstanceGroup fix.
+
+import xml.sax
+import unittest
+
+from boto import handler
+from boto.emr import emrobject
+from boto.resultset import ResultSet
+
+
+JOB_FLOW_EXAMPLE = """
+<DescribeJobFlowsResponse
+    xmlns="http://elasticmapreduce.amazonaws.com/doc/2009-01-15">
+  <DescribeJobFlowsResult>
+    <JobFlows>
+      <member>
+        <ExecutionStatusDetail>
+          <CreationDateTime>2009-01-28T21:49:16Z</CreationDateTime>
+          <StartDateTime>2009-01-28T21:49:16Z</StartDateTime>
+          <State>STARTING</State>
+        </ExecutionStatusDetail>
+        <Name>MyJobFlowName</Name>
+        <LogUri>mybucket/subdir/</LogUri>
+        <Steps>
+          <member>
+            <ExecutionStatusDetail>
+              <CreationDateTime>2009-01-28T21:49:16Z</CreationDateTime>
+              <State>PENDING</State>
+            </ExecutionStatusDetail>
+            <StepConfig>
+              <HadoopJarStep>
+                <Jar>MyJarFile</Jar>
+                <MainClass>MyMailClass</MainClass>
+                <Args>
+                  <member>arg1</member>
+                  <member>arg2</member>
+                </Args>
+                <Properties/>
+              </HadoopJarStep>
+              <Name>MyStepName</Name>
+              <ActionOnFailure>CONTINUE</ActionOnFailure>
+            </StepConfig>
+          </member>
+        </Steps>
+        <JobFlowId>j-3UN6WX5RRO2AG</JobFlowId>
+        <Instances>
+          <Placement>
+            <AvailabilityZone>us-east-1a</AvailabilityZone>
+          </Placement>
+          <SlaveInstanceType>m1.small</SlaveInstanceType>
+          <MasterInstanceType>m1.small</MasterInstanceType>
+          <Ec2KeyName>myec2keyname</Ec2KeyName>
+          <InstanceCount>4</InstanceCount>
+          <KeepJobFlowAliveWhenNoSteps>true</KeepJobFlowAliveWhenNoSteps>
+        </Instances>
+      </member>
+    </JobFlows>
+  </DescribeJobFlowsResult>
+  <ResponseMetadata>
+    <RequestId>9cea3229-ed85-11dd-9877-6fad448a8419</RequestId>
+  </ResponseMetadata>
+</DescribeJobFlowsResponse>
+"""
+
+JOB_FLOW_COMPLETED = """
+<DescribeJobFlowsResponse xmlns="http://elasticmapreduce.amazonaws.com/doc/2009-03-31">
+  <DescribeJobFlowsResult>
+    <JobFlows>
+      <member>
+        <ExecutionStatusDetail>
+          <CreationDateTime>2010-10-21T01:00:25Z</CreationDateTime>
+          <LastStateChangeReason>Steps completed</LastStateChangeReason>
+          <StartDateTime>2010-10-21T01:03:59Z</StartDateTime>
+          <ReadyDateTime>2010-10-21T01:03:59Z</ReadyDateTime>
+          <State>COMPLETED</State>
+          <EndDateTime>2010-10-21T01:44:18Z</EndDateTime>
+        </ExecutionStatusDetail>
+        <BootstrapActions/>
+        <Name>RealJobFlowName</Name>
+        <LogUri>s3n://example.emrtest.scripts/jobflow_logs/</LogUri>
+        <Steps>
+          <member>
+            <StepConfig>
+              <HadoopJarStep>
+                <Jar>s3n://us-east-1.elasticmapreduce/libs/script-runner/script-runner.jar</Jar>
+                <Args>
+                  <member>s3n://us-east-1.elasticmapreduce/libs/state-pusher/0.1/fetch</member>
+                </Args>
+                <Properties/>
+              </HadoopJarStep>
+              <Name>Setup Hadoop Debugging</Name>
+              <ActionOnFailure>TERMINATE_JOB_FLOW</ActionOnFailure>
+            </StepConfig>
+            <ExecutionStatusDetail>
+              <CreationDateTime>2010-10-21T01:00:25Z</CreationDateTime>
+              <StartDateTime>2010-10-21T01:03:59Z</StartDateTime>
+              <State>COMPLETED</State>
+              <EndDateTime>2010-10-21T01:04:22Z</EndDateTime>
+            </ExecutionStatusDetail>
+          </member>
+          <member>
+            <StepConfig>
+              <HadoopJarStep>
+                <Jar>/home/hadoop/contrib/streaming/hadoop-0.20-streaming.jar</Jar>
+                <Args>
+                  <member>-mapper</member>
+                  <member>s3://example.emrtest.scripts/81d8-5a9d3df4a86c-InitialMapper.py</member>
+                  <member>-reducer</member>
+                  <member>s3://example.emrtest.scripts/81d8-5a9d3df4a86c-InitialReducer.py</member>
+                  <member>-input</member>
+                  <member>s3://example.emrtest.data/raw/2010/10/20/*</member>
+                  <member>-input</member>
+                  <member>s3://example.emrtest.data/raw/2010/10/19/*</member>
+                  <member>-input</member>
+                  <member>s3://example.emrtest.data/raw/2010/10/18/*</member>
+                  <member>-input</member>
+                  <member>s3://example.emrtest.data/raw/2010/10/17/*</member>
+                  <member>-input</member>
+                  <member>s3://example.emrtest.data/raw/2010/10/16/*</member>
+                  <member>-input</member>
+                  <member>s3://example.emrtest.data/raw/2010/10/15/*</member>
+                  <member>-input</member>
+                  <member>s3://example.emrtest.data/raw/2010/10/14/*</member>
+                  <member>-output</member>
+                  <member>s3://example.emrtest.crunched/</member>
+                </Args>
+                <Properties/>
+              </HadoopJarStep>
+              <Name>testjob_Initial</Name>
+              <ActionOnFailure>TERMINATE_JOB_FLOW</ActionOnFailure>
+            </StepConfig>
+            <ExecutionStatusDetail>
+              <CreationDateTime>2010-10-21T01:00:25Z</CreationDateTime>
+              <StartDateTime>2010-10-21T01:04:22Z</StartDateTime>
+              <State>COMPLETED</State>
+              <EndDateTime>2010-10-21T01:36:18Z</EndDateTime>
+            </ExecutionStatusDetail>
+          </member>
+          <member>
+            <StepConfig>
+              <HadoopJarStep>
+                <Jar>/home/hadoop/contrib/streaming/hadoop-0.20-streaming.jar</Jar>
+                <Args>
+                  <member>-mapper</member>
+                  <member>s3://example.emrtest.scripts/81d8-5a9d3df4a86c-step1Mapper.py</member>
+                  <member>-reducer</member>
+                  <member>s3://example.emrtest.scripts/81d8-5a9d3df4a86c-step1Reducer.py</member>
+                  <member>-input</member>
+                  <member>s3://example.emrtest.crunched/*</member>
+                  <member>-output</member>
+                  <member>s3://example.emrtest.step1/</member>
+                </Args>
+                <Properties/>
+              </HadoopJarStep>
+              <Name>testjob_step1</Name>
+              <ActionOnFailure>TERMINATE_JOB_FLOW</ActionOnFailure>
+            </StepConfig>
+            <ExecutionStatusDetail>
+              <CreationDateTime>2010-10-21T01:00:25Z</CreationDateTime>
+              <StartDateTime>2010-10-21T01:36:18Z</StartDateTime>
+              <State>COMPLETED</State>
+              <EndDateTime>2010-10-21T01:37:51Z</EndDateTime>
+            </ExecutionStatusDetail>
+          </member>
+          <member>
+            <StepConfig>
+              <HadoopJarStep>
+                <Jar>/home/hadoop/contrib/streaming/hadoop-0.20-streaming.jar</Jar>
+                <Args>
+                  <member>-mapper</member>
+                  <member>s3://example.emrtest.scripts/81d8-5a9d3df4a86c-step2Mapper.py</member>
+                  <member>-reducer</member>
+                  <member>s3://example.emrtest.scripts/81d8-5a9d3df4a86c-step2Reducer.py</member>
+                  <member>-input</member>
+                  <member>s3://example.emrtest.crunched/*</member>
+                  <member>-output</member>
+                  <member>s3://example.emrtest.step2/</member>
+                </Args>
+                <Properties/>
+              </HadoopJarStep>
+              <Name>testjob_step2</Name>
+              <ActionOnFailure>TERMINATE_JOB_FLOW</ActionOnFailure>
+            </StepConfig>
+            <ExecutionStatusDetail>
+              <CreationDateTime>2010-10-21T01:00:25Z</CreationDateTime>
+              <StartDateTime>2010-10-21T01:37:51Z</StartDateTime>
+              <State>COMPLETED</State>
+              <EndDateTime>2010-10-21T01:39:32Z</EndDateTime>
+            </ExecutionStatusDetail>
+          </member>
+          <member>
+            <StepConfig>
+              <HadoopJarStep>
+                <Jar>/home/hadoop/contrib/streaming/hadoop-0.20-streaming.jar</Jar>
+                <Args>
+                  <member>-mapper</member>
+                  <member>s3://example.emrtest.scripts/81d8-5a9d3df4a86c-step3Mapper.py</member>
+                  <member>-reducer</member>
+                  <member>s3://example.emrtest.scripts/81d8-5a9d3df4a86c-step3Reducer.py</member>
+                  <member>-input</member>
+                  <member>s3://example.emrtest.step1/*</member>
+                  <member>-output</member>
+                  <member>s3://example.emrtest.step3/</member>
+                </Args>
+                <Properties/>
+              </HadoopJarStep>
+              <Name>testjob_step3</Name>
+              <ActionOnFailure>TERMINATE_JOB_FLOW</ActionOnFailure>
+            </StepConfig>
+            <ExecutionStatusDetail>
+              <CreationDateTime>2010-10-21T01:00:25Z</CreationDateTime>
+              <StartDateTime>2010-10-21T01:39:32Z</StartDateTime>
+              <State>COMPLETED</State>
+              <EndDateTime>2010-10-21T01:41:22Z</EndDateTime>
+            </ExecutionStatusDetail>
+          </member>
+          <member>
+            <StepConfig>
+              <HadoopJarStep>
+                <Jar>/home/hadoop/contrib/streaming/hadoop-0.20-streaming.jar</Jar>
+                <Args>
+                  <member>-mapper</member>
+                  <member>s3://example.emrtest.scripts/81d8-5a9d3df4a86c-step4Mapper.py</member>
+                  <member>-reducer</member>
+                  <member>s3://example.emrtest.scripts/81d8-5a9d3df4a86c-step4Reducer.py</member>
+                  <member>-input</member>
+                  <member>s3://example.emrtest.step1/*</member>
+                  <member>-output</member>
+                  <member>s3://example.emrtest.step4/</member>
+                </Args>
+                <Properties/>
+              </HadoopJarStep>
+              <Name>testjob_step4</Name>
+              <ActionOnFailure>TERMINATE_JOB_FLOW</ActionOnFailure>
+            </StepConfig>
+            <ExecutionStatusDetail>
+              <CreationDateTime>2010-10-21T01:00:25Z</CreationDateTime>
+              <StartDateTime>2010-10-21T01:41:22Z</StartDateTime>
+              <State>COMPLETED</State>
+              <EndDateTime>2010-10-21T01:43:03Z</EndDateTime>
+            </ExecutionStatusDetail>
+          </member>
+        </Steps>
+        <JobFlowId>j-3H3Q13JPFLU22</JobFlowId>
+        <Instances>
+          <SlaveInstanceType>m1.large</SlaveInstanceType>
+          <MasterInstanceId>i-64c21609</MasterInstanceId>
+          <Placement>
+            <AvailabilityZone>us-east-1b</AvailabilityZone>
+          </Placement>
+          <InstanceGroups>
+            <member>
+              <CreationDateTime>2010-10-21T01:00:25Z</CreationDateTime>
+              <InstanceRunningCount>0</InstanceRunningCount>
+              <StartDateTime>2010-10-21T01:02:09Z</StartDateTime>
+              <ReadyDateTime>2010-10-21T01:03:03Z</ReadyDateTime>
+              <State>ENDED</State>
+              <EndDateTime>2010-10-21T01:44:18Z</EndDateTime>
+              <InstanceRequestCount>1</InstanceRequestCount>
+              <InstanceType>m1.large</InstanceType>
+              <Market>ON_DEMAND</Market>
+              <LastStateChangeReason>Job flow terminated</LastStateChangeReason>
+              <InstanceRole>MASTER</InstanceRole>
+              <InstanceGroupId>ig-EVMHOZJ2SCO8</InstanceGroupId>
+              <Name>master</Name>
+            </member>
+            <member>
+              <CreationDateTime>2010-10-21T01:00:25Z</CreationDateTime>
+              <InstanceRunningCount>0</InstanceRunningCount>
+              <StartDateTime>2010-10-21T01:03:59Z</StartDateTime>
+              <ReadyDateTime>2010-10-21T01:03:59Z</ReadyDateTime>
+              <State>ENDED</State>
+              <EndDateTime>2010-10-21T01:44:18Z</EndDateTime>
+              <InstanceRequestCount>9</InstanceRequestCount>
+              <InstanceType>m1.large</InstanceType>
+              <Market>ON_DEMAND</Market>
+              <LastStateChangeReason>Job flow terminated</LastStateChangeReason>
+              <InstanceRole>CORE</InstanceRole>
+              <InstanceGroupId>ig-YZHDYVITVHKB</InstanceGroupId>
+              <Name>slave</Name>
+            </member>
+          </InstanceGroups>
+          <NormalizedInstanceHours>40</NormalizedInstanceHours>
+          <HadoopVersion>0.20</HadoopVersion>
+          <MasterInstanceType>m1.large</MasterInstanceType>
+          <MasterPublicDnsName>ec2-184-72-153-139.compute-1.amazonaws.com</MasterPublicDnsName>
+          <Ec2KeyName>myubersecurekey</Ec2KeyName>
+          <InstanceCount>10</InstanceCount>
+          <KeepJobFlowAliveWhenNoSteps>false</KeepJobFlowAliveWhenNoSteps>
+        </Instances>
+      </member>
+    </JobFlows>
+  </DescribeJobFlowsResult>
+  <ResponseMetadata>
+    <RequestId>c31e701d-dcb4-11df-b5d9-337fc7fe4773</RequestId>
+  </ResponseMetadata>
+</DescribeJobFlowsResponse>
+"""
+
+
+class TestEMRResponses(unittest.TestCase):
+    def _parse_xml(self, body, markers):
+        rs = ResultSet(markers)
+        h = handler.XmlHandler(rs, None)
+        xml.sax.parseString(body, h)
+        return rs
+
+    def _assert_fields(self, response, **fields):
+        for field, expected in fields.items():
+            actual = getattr(response, field)
+            self.assertEquals(expected, actual,
+                              "Field %s: %r != %r" % (field, expected, actual))
+
+    def test_JobFlows_example(self):
+        [jobflow] = self._parse_xml(JOB_FLOW_EXAMPLE,
+                                    [('member', emrobject.JobFlow)])
+        self._assert_fields(jobflow,
+                            creationdatetime='2009-01-28T21:49:16Z',
+                            startdatetime='2009-01-28T21:49:16Z',
+                            state='STARTING',
+                            instancecount='4',
+                            jobflowid='j-3UN6WX5RRO2AG',
+                            loguri='mybucket/subdir/',
+                            name='MyJobFlowName',
+                            availabilityzone='us-east-1a',
+                            slaveinstancetype='m1.small',
+                            masterinstancetype='m1.small',
+                            ec2keyname='myec2keyname',
+                            keepjobflowalivewhennosteps='true')
+
+    def test_JobFlows_completed(self):
+        [jobflow] = self._parse_xml(JOB_FLOW_COMPLETED,
+                                    [('member', emrobject.JobFlow)])
+        self._assert_fields(jobflow,
+                            creationdatetime='2010-10-21T01:00:25Z',
+                            startdatetime='2010-10-21T01:03:59Z',
+                            enddatetime='2010-10-21T01:44:18Z',
+                            state='COMPLETED',
+                            instancecount='10',
+                            jobflowid='j-3H3Q13JPFLU22',
+                            loguri='s3n://example.emrtest.scripts/jobflow_logs/',
+                            name='RealJobFlowName',
+                            availabilityzone='us-east-1b',
+                            slaveinstancetype='m1.large',
+                            masterinstancetype='m1.large',
+                            ec2keyname='myubersecurekey',
+                            keepjobflowalivewhennosteps='false')
+        self.assertEquals(6, len(jobflow.steps))
+        self.assertEquals(2, len(jobflow.instancegroups))
+
diff --git a/boto/exception.py b/boto/exception.py
new file mode 100644
index 0000000..718be46
--- /dev/null
+++ b/boto/exception.py
@@ -0,0 +1,430 @@
+# Copyright (c) 2006-2010 Mitch Garnaat http://garnaat.org/
+# Copyright (c) 2010, Eucalyptus Systems, Inc.
+# 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.
+
+"""
+Exception classes - Subclassing allows you to check for specific errors
+"""
+import base64
+import xml.sax
+from boto import handler
+from boto.resultset import ResultSet
+
+
+class BotoClientError(StandardError):
+    """
+    General Boto Client error (error accessing AWS)
+    """
+
+    def __init__(self, reason):
+        StandardError.__init__(self)
+        self.reason = reason
+
+    def __repr__(self):
+        return 'BotoClientError: %s' % self.reason
+
+    def __str__(self):
+        return 'BotoClientError: %s' % self.reason
+
+class SDBPersistenceError(StandardError):
+
+    pass
+
+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):
+        StandardError.__init__(self)
+        self.status = status
+        self.reason = reason
+        self.body = body or ''
+        self.request_id = None
+        self.error_code = None
+        self.error_message = None
+        self.box_usage = None
+
+        # Attempt to parse the error response. If body isn't present,
+        # then just ignore the error response.
+        if self.body:
+            try:
+                h = handler.XmlHandler(self, self)
+                xml.sax.parseString(self.body, h)
+            except xml.sax.SAXParseException, pe:
+                # Go ahead and clean up anything that may have
+                # managed to get into the error data so we
+                # don't get partial garbage.
+                print "Warning: failed to parse error message from AWS: %s" % pe
+                self._cleanupParsedProperties()
+
+    def __getattr__(self, name):
+        if name == 'message':
+            return self.error_message
+        if name == 'code':
+            return self.error_code
+        raise AttributeError
+
+    def __repr__(self):
+        return '%s: %s %s\n%s' % (self.__class__.__name__,
+                                  self.status, self.reason, self.body)
+
+    def __str__(self):
+        return '%s: %s %s\n%s' % (self.__class__.__name__,
+                                  self.status, self.reason, self.body)
+
+    def startElement(self, name, attrs, connection):
+        pass
+
+    def endElement(self, name, value, connection):
+        if name in ('RequestId', 'RequestID'):
+            self.request_id = value
+        elif name == 'Code':
+            self.error_code = value
+        elif name == 'Message':
+            self.error_message = value
+        elif name == 'BoxUsage':
+            self.box_usage = value
+        return None
+
+    def _cleanupParsedProperties(self):
+        self.request_id = None
+        self.error_code = None
+        self.error_message = None
+        self.box_usage = None
+
+class ConsoleOutput:
+
+    def __init__(self, parent=None):
+        self.parent = parent
+        self.instance_id = None
+        self.timestamp = None
+        self.comment = None
+        self.output = None
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'instanceId':
+            self.instance_id = value
+        elif name == 'output':
+            self.output = base64.b64decode(value)
+        else:
+            setattr(self, name, value)
+
+class StorageCreateError(BotoServerError):
+    """
+    Error creating a bucket or key on a storage service.
+    """
+    def __init__(self, status, reason, body=None):
+        self.bucket = None
+        BotoServerError.__init__(self, status, reason, body)
+
+    def endElement(self, name, value, connection):
+        if name == 'BucketName':
+            self.bucket = value
+        else:
+            return BotoServerError.endElement(self, name, value, connection)
+
+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.
+    """
+    def __init__(self, status, reason, body=None):
+        self.detail = None
+        self.type = None
+        BotoServerError.__init__(self, status, reason, body)
+
+    def startElement(self, name, attrs, connection):
+        return BotoServerError.startElement(self, name, attrs, connection)
+
+    def endElement(self, name, value, connection):
+        if name == 'Detail':
+            self.detail = value
+        elif name == 'Type':
+            self.type = value
+        else:
+            return BotoServerError.endElement(self, name, value, connection)
+
+    def _cleanupParsedProperties(self):
+        BotoServerError._cleanupParsedProperties(self)
+        for p in ('detail', 'type'):
+            setattr(self, p, None)
+
+class SQSDecodeError(BotoClientError):
+    """
+    Error when decoding an SQS message.
+    """
+    def __init__(self, reason, message):
+        BotoClientError.__init__(self, reason)
+        self.message = message
+
+    def __repr__(self):
+        return 'SQSDecodeError: %s' % self.reason
+
+    def __str__(self):
+        return 'SQSDecodeError: %s' % self.reason
+
+class StorageResponseError(BotoServerError):
+    """
+    Error in response from a storage service.
+    """
+    def __init__(self, status, reason, body=None):
+        self.resource = None
+        BotoServerError.__init__(self, status, reason, body)
+
+    def startElement(self, name, attrs, connection):
+        return BotoServerError.startElement(self, name, attrs, connection)
+
+    def endElement(self, name, value, connection):
+        if name == 'Resource':
+            self.resource = value
+        else:
+            return BotoServerError.endElement(self, name, value, connection)
+
+    def _cleanupParsedProperties(self):
+        BotoServerError._cleanupParsedProperties(self)
+        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.
+    """
+
+    def __init__(self, status, reason, body=None):
+        self.errors = None
+        self._errorResultSet = []
+        BotoServerError.__init__(self, status, reason, body)
+        self.errors = [ (e.error_code, e.error_message) \
+                for e in self._errorResultSet ]
+        if len(self.errors):
+            self.error_code, self.error_message = self.errors[0]
+
+    def startElement(self, name, attrs, connection):
+        if name == 'Errors':
+            self._errorResultSet = ResultSet([('Error', _EC2Error)])
+            return self._errorResultSet
+        else:
+            return None
+
+    def endElement(self, name, value, connection):
+        if name == 'RequestID':
+            self.request_id = value
+        else:
+            return None # don't call subclass here
+
+    def _cleanupParsedProperties(self):
+        BotoServerError._cleanupParsedProperties(self)
+        self._errorResultSet = []
+        for p in ('errors'):
+            setattr(self, p, None)
+
+class EmrResponseError(BotoServerError):
+    """
+    Error in response from EMR
+    """
+    pass
+
+class _EC2Error:
+
+    def __init__(self, connection=None):
+        self.connection = connection
+        self.error_code = None
+        self.error_message = None
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'Code':
+            self.error_code = value
+        elif name == 'Message':
+            self.error_message = value
+        else:
+            return None
+
+class SDBResponseError(BotoServerError):
+    """
+    Error in responses from SDB.
+    """
+    pass
+
+class AWSConnectionError(BotoClientError):
+    """
+    General error connecting to Amazon Web Services.
+    """
+    pass
+
+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
+
+class InvalidUriError(Exception):
+    """Exception raised when URI is invalid."""
+
+    def __init__(self, message):
+        Exception.__init__(self)
+        self.message = message
+
+class InvalidAclError(Exception):
+    """Exception raised when ACL XML is invalid."""
+
+    def __init__(self, message):
+        Exception.__init__(self)
+        self.message = message
+
+class NoAuthHandlerFound(Exception):
+    """Is raised when no auth handlers were found ready to authenticate."""
+    pass
+
+class TooManyAuthHandlerReadyToAuthenticate(Exception):
+    """Is raised when there are more than one auth handler ready.
+
+    In normal situation there should only be one auth handler that is ready to
+    authenticate. In case where more than one auth handler is ready to
+    authenticate, we raise this exception, to prevent unpredictable behavior
+    when multiple auth handlers can handle a particular case and the one chosen
+    depends on the order they were checked.
+    """