diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..57bc88a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3ad93b6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,54 @@
+About IndexTank Service
+=======================
+
+IndexTank Service (http://indextank.com) contains the source code that implementing the Search-as-a-Service platform. It contains the components that allows managing user accounts, server instances (worker) and index instances (deploy); depends on IndexTank Engine (https://github.com/linkedin/indextank-engine) binary.
+
+### Homepage:
+
+Find out more about at: TBD
+
+### License:
+
+Apache Public License (APL) 2.0
+
+### Components:
+
+1. API
+2. Backoffice
+3. Storefront
+4. Nebu
+
+### Dependencies:
+
+* Django
+* Python tested with 2.6.6
+* Java(TM) SE Runtime Environment tested with build 1.6.0_24-b07
+* nginx
+* uWSGI (http://projects.unbit.it/uwsgi/)
+* MySQL
+* daemontools (http://cr.yp.to/daemontools.html)
+* Thrift library (generated sources with version 0.5.0 are provided)
+
+### Getting started:
+
+1. Create the database schema.
+2. Create an account.
+3. Start an index instance (IndexTank Engine).
+4. Start API.
+
+## API
+
+Django application implementing the REST JSON API, enables index management per account, indexing functions and search. Interacts via Thrift with specific index instances (deploy).
+
+## Backoffice
+
+Django application that allows manual administration.
+
+## Storefront
+
+Django application with the service web, contains the user registration form (allows creating accounts).
+
+## Nebu
+
+Index, deploy & worker manager. A worker (server instance) may contain a deploy (index instance) or many.
+
diff --git a/api/.project b/api/.project
new file mode 100644
index 0000000..21937e5
--- /dev/null
+++ b/api/.project
@@ -0,0 +1,18 @@
+
+
+ api
+
+
+
+
+
+ org.python.pydev.PyDevBuilder
+
+
+
+
+
+ org.python.pydev.pythonNature
+ org.python.pydev.django.djangoNature
+
+
diff --git a/api/.pydevproject b/api/.pydevproject
new file mode 100644
index 0000000..5b70c03
--- /dev/null
+++ b/api/.pydevproject
@@ -0,0 +1,13 @@
+
+
+
+
+Default
+python 2.6
+
+/api/
+
+
+../
+
+
diff --git a/api/.settings/org.eclipse.ltk.core.refactoring.prefs b/api/.settings/org.eclipse.ltk.core.refactoring.prefs
new file mode 100644
index 0000000..420b4e4
--- /dev/null
+++ b/api/.settings/org.eclipse.ltk.core.refactoring.prefs
@@ -0,0 +1,3 @@
+#Wed Jun 30 14:24:43 ART 2010
+eclipse.preferences.version=1
+org.eclipse.ltk.core.refactoring.enable.project.refactoring.history=false
diff --git a/api/__init__.py b/api/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/api/amazon_credential.py b/api/amazon_credential.py
new file mode 100644
index 0000000..c2910f5
--- /dev/null
+++ b/api/amazon_credential.py
@@ -0,0 +1,2 @@
+AMAZON_USER = ""
+AMAZON_PASSWORD = ""
diff --git a/api/boto/__init__.py b/api/boto/__init__.py
new file mode 100644
index 0000000..fc2e592
--- /dev/null
+++ b/api/boto/__init__.py
@@ -0,0 +1,263 @@
+# 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.pyami.config import Config, BotoConfigLocations
+import os, sys
+import logging
+import logging.config
+
+Version = '1.9b'
+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_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_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 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
+
diff --git a/api/boto/cloudfront/__init__.py b/api/boto/cloudfront/__init__.py
new file mode 100644
index 0000000..d03d6eb
--- /dev/null
+++ b/api/boto/cloudfront/__init__.py
@@ -0,0 +1,222 @@
+# 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 base64
+import time
+import boto.utils
+from boto.connection import AWSAuthConnection
+from boto import handler
+from boto.cloudfront.distribution import *
+from boto.cloudfront.identity import OriginAccessIdentity
+from boto.cloudfront.identity import OriginAccessIdentityConfig
+from boto.resultset import ResultSet
+from boto.cloudfront.exception import CloudFrontServerError
+
+class CloudFrontConnection(AWSAuthConnection):
+
+ DefaultHost = 'cloudfront.amazonaws.com'
+ Version = '2009-12-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 add_aws_auth_header(self, headers, method, path):
+ if not headers.has_key('Date'):
+ headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT",
+ time.gmtime())
+
+ hmac = self.hmac.copy()
+ hmac.update(headers['Date'])
+ b64_hmac = base64.encodestring(hmac.digest()).strip()
+ headers['Authorization'] = "AWS %s:%s" % (self.aws_access_key_id, b64_hmac)
+
+ # 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()
+ 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()
+ 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()
+ 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()
+ return self.get_etag(response)
+ if response.status != 200:
+ raise CloudFrontServerError(response.status, response.reason, body)
+
+ 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()
+ if response.status == 201:
+ d = dist_class(connection=self)
+ h = handler.XmlHandler(d, self)
+ xml.sax.parseString(body, h)
+ 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()
+ 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')
+
+
diff --git a/api/boto/cloudfront/distribution.py b/api/boto/cloudfront/distribution.py
new file mode 100644
index 0000000..cd36add
--- /dev/null
+++ b/api/boto/cloudfront/distribution.py
@@ -0,0 +1,470 @@
+# 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 Signer, ActiveTrustedSigners, TrustedSigners
+from boto.cloudfront.logging import LoggingInfo
+from boto.s3.acl import ACL
+
+class DistributionConfig:
+
+ def __init__(self, connection=None, origin='', enabled=False,
+ caller_reference='', cnames=None, comment='',
+ origin_access_identity=None, trusted_signers=None):
+ 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.origin_access_identity = origin_access_identity
+ self.trusted_signers = trusted_signers
+ self.logging = None
+
+ def get_oai_value(self):
+ if isinstance(self.origin_access_identity, OriginAccessIdentity):
+ return self.origin_access_identity.uri()
+ else:
+ return self.origin_access_identity
+
+ def to_xml(self):
+ s = '\n'
+ s += '\n'
+ s += ' %s\n' % self.origin
+ s += ' %s\n' % self.caller_reference
+ for cname in self.cnames:
+ s += ' %s\n' % cname
+ if self.comment:
+ s += ' %s\n' % self.comment
+ s += ' '
+ if self.enabled:
+ s += 'true'
+ else:
+ s += 'false'
+ s += '\n'
+ if self.origin_access_identity:
+ val = self.get_oai_value()
+ s += '%s\n' % val
+ if self.trusted_signers:
+ s += '\n'
+ for signer in self.trusted_signers:
+ if signer == 'Self':
+ s += ' \n'
+ else:
+ s += ' %s\n' % signer
+ s += '\n'
+ if self.logging:
+ s += '\n'
+ s += ' %s\n' % self.logging.bucket
+ s += ' %s\n' % self.logging.prefix
+ s += '\n'
+ s += '\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
+ else:
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'CNAME':
+ self.cnames.append(value)
+ elif name == 'Origin':
+ self.origin = 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 == 'OriginAccessIdentity':
+ self.origin_access_identity = value
+ else:
+ setattr(self, name, value)
+
+class StreamingDistributionConfig(DistributionConfig):
+
+ def __init__(self, connection=None, origin='', enabled=False,
+ caller_reference='', cnames=None, comment=''):
+ DistributionConfig.__init__(self, connection, origin,
+ enabled, caller_reference,
+ cnames, comment)
+
+ def to_xml(self):
+ s = '\n'
+ s += '\n'
+ s += ' %s\n' % self.origin
+ s += ' %s\n' % self.caller_reference
+ for cname in self.cnames:
+ s += ' %s\n' % cname
+ if self.comment:
+ s += ' %s\n' % self.comment
+ s += ' '
+ if self.enabled:
+ s += 'true'
+ else:
+ s += 'false'
+ s += '\n'
+ s += '\n'
+ return s
+
+ def startElement(self, name, attrs, connection):
+ pass
+
+class DistributionSummary:
+
+ def __init__(self, connection=None, domain_name='', id='',
+ last_modified_time=None, status='', origin='',
+ 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
+ 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
+
+ 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,
+ origin_access_identity=None,
+ trusted_signers=None):
+ """
+ Update the configuration of the Distribution.
+
+ :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.
+
+ :type origin_access_identity: :class:`boto.cloudfront.identity.OriginAccessIdentity`
+ :param origin_access_identity: The CloudFront origin access identity
+ associated with the distribution. This
+ must be provided if you want the
+ distribution to serve private content.
+
+ :type trusted_signers: :class:`boto.cloudfront.signers.TrustedSigner`
+ :param trusted_signers: The AWS users who are authorized to sign
+ URL's for private content in this Distribution.
+
+ """
+ new_config = DistributionConfig(self.connection, self.config.origin,
+ self.config.enabled, self.config.caller_reference,
+ self.config.cnames, self.config.comment,
+ self.config.origin_access_identity,
+ 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
+ if origin_access_identity != None:
+ new_config.origin_access_identity = origin_access_identity
+ if trusted_signers:
+ new_config.trusted_signers = trusted_signers
+ 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.split('.')[0]
+ 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 self.config.origin_access_identity:
+ id = self.config.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)
+
+ 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 None
+
+ def update(self, enabled=None, cnames=None, comment=None):
+ """
+ Update the configuration of the Distribution.
+
+ :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 = StreamingDistributionConfig(self.connection,
+ self.config.origin,
+ self.config.enabled,
+ self.config.caller_reference,
+ self.config.cnames,
+ self.config.comment)
+ 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
+
+ def delete(self):
+ self.connection.delete_streaming_distribution(self.id, self.etag)
+
+
diff --git a/api/boto/cloudfront/exception.py b/api/boto/cloudfront/exception.py
new file mode 100644
index 0000000..7680642
--- /dev/null
+++ b/api/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/api/boto/cloudfront/identity.py b/api/boto/cloudfront/identity.py
new file mode 100644
index 0000000..711b8b7
--- /dev/null
+++ b/api/boto/cloudfront/identity.py
@@ -0,0 +1,98 @@
+# 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 = OriginAccessIdentifyConfig(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_distribution(self.id, self.etag)
+
+ def uri(self):
+ return 'origin-access-identity/cloudfront/%s' % 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 = '\n'
+ s += '\n'
+ s += ' %s\n' % self.caller_reference
+ if self.comment:
+ s += ' %s\n' % self.comment
+ s += '\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)
+
+
+
diff --git a/api/boto/cloudfront/logging.py b/api/boto/cloudfront/logging.py
new file mode 100644
index 0000000..6c2f4fd
--- /dev/null
+++ b/api/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/api/boto/cloudfront/object.py b/api/boto/cloudfront/object.py
new file mode 100644
index 0000000..3574d13
--- /dev/null
+++ b/api/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 '' % (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/api/boto/cloudfront/signers.py b/api/boto/cloudfront/signers.py
new file mode 100644
index 0000000..0b0cd50
--- /dev/null
+++ b/api/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/api/boto/connection.py b/api/boto/connection.py
new file mode 100644
index 0000000..9a443f7
--- /dev/null
+++ b/api/boto/connection.py
@@ -0,0 +1,648 @@
+# Copyright (c) 2006-2009 Mitch Garnaat http://garnaat.org/
+# Copyright (c) 2008 rPath, Inc.
+# Copyright (c) 2009 The Echo Nest Corporation
+#
+# 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 hmac
+import httplib
+import socket, errno
+import re
+import sys
+import time
+import urllib, urlparse
+import os
+import xml.sax
+import Queue
+import boto
+from boto.exception import AWSConnectionError, BotoClientError, BotoServerError
+from boto.resultset import ResultSet
+import boto.utils
+from boto import config, UserAgent, handler
+
+#
+# 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
+
+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 AWSAuthConnection:
+ 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='/'):
+ """
+ :type host: string
+ :param host: The host to make the connection to
+
+ :type aws_access_key_id: string
+ :param aws_access_key_id: AWS Access Key ID (provided by Amazon)
+
+ :type aws_secret_access_key: string
+ :param aws_secret_access_key: Secret Access Key (provided by Amazon)
+
+ :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}.
+
+ :type proxy:
+ :param proxy:
+
+ :type proxy_port: int
+ :param proxy_port: The port to use when connecting over a proxy
+
+ :type proxy_user: string
+ :param proxy_user: The username to connect with on the proxy
+
+ :type proxy_pass: string
+ :param proxy_pass: The password to use when connection over a proxy.
+
+ :type port: integer
+ :param port: The port to use to connect
+ """
+
+ self.num_retries = 5
+ 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]
+
+ if aws_access_key_id:
+ self.aws_access_key_id = aws_access_key_id
+ elif os.environ.has_key('AWS_ACCESS_KEY_ID'):
+ self.aws_access_key_id = os.environ['AWS_ACCESS_KEY_ID']
+ elif config.has_option('Credentials', 'aws_access_key_id'):
+ self.aws_access_key_id = config.get('Credentials', 'aws_access_key_id')
+
+ if aws_secret_access_key:
+ self.aws_secret_access_key = aws_secret_access_key
+ elif os.environ.has_key('AWS_SECRET_ACCESS_KEY'):
+ self.aws_secret_access_key = os.environ['AWS_SECRET_ACCESS_KEY']
+ elif config.has_option('Credentials', 'aws_secret_access_key'):
+ self.aws_secret_access_key = config.get('Credentials', 'aws_secret_access_key')
+
+ # initialize an HMAC for signatures, make copies with each request
+ self.hmac = hmac.new(self.aws_secret_access_key, digestmod=sha)
+ if sha256:
+ self.hmac_256 = hmac.new(self.aws_secret_access_key, digestmod=sha256)
+ else:
+ self.hmac_256 = None
+
+ # 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
+
+ def __repr__(self):
+ return '%s:%s' % (self.__class__.__name__, self.host)
+
+ 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 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
+ # it no longer does that. Hence, this kludge.
+ if sys.version[:3] == "2.6" 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\w+):(?P.*)@)?' \
+ '(?P[\w\-\.]+)' \
+ '(?::(?P\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()
+ boto.log.debug('establishing HTTP connection')
+ if is_secure:
+ 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:
+ 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):
+ """
+ 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
+ num_retries = config.getint('Boto', 'num_retries', self.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 make_request(self, method, path, headers=None, data='', host=None,
+ auth_path=None, sender=None):
+ path = self.get_path(path)
+ if headers == None:
+ headers = {}
+ else:
+ headers = headers.copy()
+ headers['User-Agent'] = UserAgent
+ if not headers.has_key('Content-Length'):
+ headers['Content-Length'] = str(len(data))
+ 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())
+ request_string = auth_path or path
+ self.add_aws_auth_header(headers, method, request_string)
+ return self._mexe(method, path, data, headers, host, sender)
+
+ def add_aws_auth_header(self, headers, method, path):
+ path = self.get_path(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, path, headers)
+ boto.log.debug('Canonical: %s' % c_string)
+ hmac = self.hmac.copy()
+ hmac.update(c_string)
+ b64_hmac = base64.encodestring(hmac.digest()).strip()
+ headers['Authorization'] = "AWS %s:%s" % (self.aws_access_key_id, b64_hmac)
+
+ 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
+ hosts = list(self._cache.keys())
+ for host in hosts:
+ conn = self._cache[host]
+ conn.close()
+ del self._cache[host]
+
+class AWSQueryConnection(AWSAuthConnection):
+
+ APIVersion = ''
+ SignatureVersion = '1'
+ 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 get_utf8_value(self, value):
+ if not isinstance(value, str) and not isinstance(value, unicode):
+ value = str(value)
+ if isinstance(value, unicode):
+ return value.encode('utf-8')
+ else:
+ return value
+
+ def calc_signature_0(self, params):
+ 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 = self.get_utf8_value(params[key])
+ pairs.append(key + '=' + urllib.quote(val))
+ qs = '&'.join(pairs)
+ return (qs, base64.b64encode(hmac.digest()))
+
+ def calc_signature_1(self, params):
+ 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 = self.get_utf8_value(params[key])
+ hmac.update(val)
+ pairs.append(key + '=' + urllib.quote(val))
+ qs = '&'.join(pairs)
+ return (qs, base64.b64encode(hmac.digest()))
+
+ def calc_signature_2(self, params, verb, path):
+ boto.log.debug('using calc_signature_2')
+ string_to_sign = '%s\n%s\n%s\n' % (verb, self.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 = self.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_signature(self, params, verb, path):
+ if self.SignatureVersion == '0':
+ t = self.calc_signature_0(params)
+ elif self.SignatureVersion == '1':
+ t = self.calc_signature_1(params)
+ elif self.SignatureVersion == '2':
+ t = self.calc_signature_2(params, verb, path)
+ else:
+ raise BotoClientError('Unknown Signature Version: %s' % self.SignatureVersion)
+ return t
+
+ def make_request(self, action, params=None, path='/', verb='GET'):
+ headers = {}
+ if params == None:
+ params = {}
+ params['Action'] = action
+ params['Version'] = self.APIVersion
+ params['AWSAccessKeyId'] = self.aws_access_key_id
+ params['SignatureVersion'] = self.SignatureVersion
+ params['Timestamp'] = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime())
+ qs, signature = self.get_signature(params, verb, self.get_path(path))
+ if verb == 'POST':
+ headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
+ request_body = qs + '&Signature=' + urllib.quote(signature)
+ qs = path
+ else:
+ request_body = ''
+ qs = path + '?' + qs + '&Signature=' + urllib.quote(signature)
+ return AWSAuthConnection.make_request(self, verb, qs,
+ data=request_body,
+ headers=headers)
+
+ def 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 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 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 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/api/boto/contrib/__init__.py b/api/boto/contrib/__init__.py
new file mode 100644
index 0000000..303dbb6
--- /dev/null
+++ b/api/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/api/boto/contrib/m2helpers.py b/api/boto/contrib/m2helpers.py
new file mode 100644
index 0000000..82d2730
--- /dev/null
+++ b/api/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/api/boto/contrib/ymlmessage.py b/api/boto/contrib/ymlmessage.py
new file mode 100644
index 0000000..22e5c62
--- /dev/null
+++ b/api/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/api/boto/ec2/__init__.py b/api/boto/ec2/__init__.py
new file mode 100644
index 0000000..8bb3f53
--- /dev/null
+++ b/api/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/api/boto/ec2/address.py b/api/boto/ec2/address.py
new file mode 100644
index 0000000..b2af107
--- /dev/null
+++ b/api/boto/ec2/address.py
@@ -0,0 +1,51 @@
+# 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 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 delete(self):
+ return self.connection.delete_address(self.public_ip)
+
+
+
diff --git a/api/boto/ec2/autoscale/__init__.py b/api/boto/ec2/autoscale/__init__.py
new file mode 100644
index 0000000..d7c5946
--- /dev/null
+++ b/api/boto/ec2/autoscale/__init__.py
@@ -0,0 +1,206 @@
+# 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 import config
+from boto.connection import AWSQueryConnection
+from boto.resultset import ResultSet
+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')
+ SignatureVersion = '2'
+
+ def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
+ is_secure=True, port=None, proxy=None, proxy_port=None,
+ proxy_user=None, proxy_pass=None, host=Endpoint, 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.
+ """
+ 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=path)
+
+ 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 = self.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)
+
+ 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)
+
diff --git a/api/boto/ec2/autoscale/activity.py b/api/boto/ec2/autoscale/activity.py
new file mode 100644
index 0000000..f895d65
--- /dev/null
+++ b/api/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/api/boto/ec2/autoscale/group.py b/api/boto/ec2/autoscale/group.py
new file mode 100644
index 0000000..d9df39f
--- /dev/null
+++ b/api/boto/ec2/autoscale/group.py
@@ -0,0 +1,190 @@
+# 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.zone import Zone
+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/api/boto/ec2/autoscale/instance.py b/api/boto/ec2/autoscale/instance.py
new file mode 100644
index 0000000..33f2ae6
--- /dev/null
+++ b/api/boto/ec2/autoscale/instance.py
@@ -0,0 +1,51 @@
+# 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)
+
+ def terminate(self):
+ """ Terminate this instance. """
+ params = {'LaunchConfigurationName' : self.instance_id}
+ return self.get_object('DeleteLaunchConfiguration', params,
+ Request)
+
diff --git a/api/boto/ec2/autoscale/launchconfig.py b/api/boto/ec2/autoscale/launchconfig.py
new file mode 100644
index 0000000..7587cb6
--- /dev/null
+++ b/api/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/api/boto/ec2/autoscale/request.py b/api/boto/ec2/autoscale/request.py
new file mode 100644
index 0000000..c066dff
--- /dev/null
+++ b/api/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/api/boto/ec2/autoscale/trigger.py b/api/boto/ec2/autoscale/trigger.py
new file mode 100644
index 0000000..197803d
--- /dev/null
+++ b/api/boto/ec2/autoscale/trigger.py
@@ -0,0 +1,137 @@
+# 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:
+ :param unit
+
+ :type lower_threshold:
+ :param lower_threshold
+ """
+ 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/api/boto/ec2/blockdevicemapping.py b/api/boto/ec2/blockdevicemapping.py
new file mode 100644
index 0000000..ef7163a
--- /dev/null
+++ b/api/boto/ec2/blockdevicemapping.py
@@ -0,0 +1,89 @@
+# 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 EBSBlockDeviceType(object):
+
+ def __init__(self, connection=None):
+ self.connection = connection
+ 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 =='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)
+
+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 = EBSBlockDeviceType(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' % (pre, i)
+ params['%s.DeviceName' % pre] = dev_name
+ ebs = self[dev_name]
+ if ebs.snapshot_id:
+ params['%s.Ebs.SnapshotId' % pre] = ebs.snapshot_id
+ if ebs.size:
+ params['%s.Ebs.VolumeSize' % pre] = ebs.size
+ if ebs.delete_on_termination:
+ params['%s.Ebs.DeleteOnTermination' % pre] = 'true'
+ else:
+ params['%s.Ebs.DeleteOnTermination' % pre] = 'false'
+ i += 1
diff --git a/api/boto/ec2/buyreservation.py b/api/boto/ec2/buyreservation.py
new file mode 100644
index 0000000..ba65590
--- /dev/null
+++ b/api/boto/ec2/buyreservation.py
@@ -0,0 +1,81 @@
+# 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 *
+from boto.manage import propget
+
+InstanceTypes = ['m1.small', 'm1.large', 'm1.xlarge', 'c1.medium', 'c1.xlarge']
+
+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/api/boto/ec2/cloudwatch/__init__.py b/api/boto/ec2/cloudwatch/__init__.py
new file mode 100644
index 0000000..1c606a1
--- /dev/null
+++ b/api/boto/ec2/cloudwatch/__init__.py
@@ -0,0 +1,206 @@
+# 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', 'Samples']
+
+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'Samples': 1.0,
+ u'Timestamp': u'2009-05-21T19:55:00Z',
+ u'Unit': u'Percent'}
+
+My server obviously isn't very busy right now!
+"""
+from boto.connection import AWSQueryConnection
+from boto.ec2.cloudwatch.metric import Metric
+from boto.ec2.cloudwatch.datapoint import Datapoint
+import boto
+import datetime
+
+class CloudWatchConnection(AWSQueryConnection):
+
+ APIVersion = boto.config.get('Boto', 'cloudwatch_version', '2009-05-15')
+ Endpoint = boto.config.get('Boto', 'cloudwatch_endpoint', 'monitoring.amazonaws.com')
+ SignatureVersion = '2'
+
+ def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
+ is_secure=True, port=None, proxy=None, proxy_port=None,
+ proxy_user=None, proxy_pass=None, host=Endpoint, debug=0,
+ https_connection_factory=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.
+ """
+ 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 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, measure_name,
+ namespace, statistics=None, dimensions=None, unit=None):
+ """
+ Get time-series data for one or more statistics of a given metric.
+
+ :type measure_name: string
+ :param measure_name: CPUUtilization|NetworkIO-in|NetworkIO-out|DiskIO-ALL-read|
+ DiskIO-ALL-write|DiskIO-ALL-read-bytes|DiskIO-ALL-write-bytes
+
+ :rtype: list
+ :return: A list of :class:`boto.ec2.image.Image`
+ """
+ params = {'Period' : period,
+ 'MeasureName' : measure_name,
+ 'Namespace' : namespace,
+ 'StartTime' : start_time.isoformat(),
+ 'EndTime' : end_time.isoformat()}
+ 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
+ if statistics:
+ self.build_list_params(params, statistics, 'Statistics.member.%d')
+ return self.get_list('GetMetricStatistics', params, [('member', Datapoint)])
+
+ def list_metrics(self):
+ """
+ Returns a list of the valid metrics for which there is recorded data available.
+ """
+ response = self.make_request('ListMetrics')
+ body = response.read()
+ return self.get_list('ListMetrics', None, [('member', Metric)])
+
+
+
diff --git a/api/boto/ec2/cloudwatch/datapoint.py b/api/boto/ec2/cloudwatch/datapoint.py
new file mode 100644
index 0000000..1860f4a
--- /dev/null
+++ b/api/boto/ec2/cloudwatch/datapoint.py
@@ -0,0 +1,37 @@
+# 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 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', 'Samples', 'Sum']:
+ self[name] = float(value)
+ elif name != 'member':
+ self[name] = value
+
diff --git a/api/boto/ec2/cloudwatch/metric.py b/api/boto/ec2/cloudwatch/metric.py
new file mode 100644
index 0000000..e4661f4
--- /dev/null
+++ b/api/boto/ec2/cloudwatch/metric.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 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', 'Samples']
+ 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 == 'MeasureName':
+ self.name = value
+ elif name == 'Namespace':
+ self.namespace = value
+ else:
+ setattr(self, name, value)
+
+ def query(self, start_time, end_time, statistic, unit, period=60):
+ return self.connection.get_metric_statistics(period, start_time, end_time,
+ self.name, self.namespace, [statistic],
+ self.dimensions, unit)
diff --git a/api/boto/ec2/connection.py b/api/boto/ec2/connection.py
new file mode 100644
index 0000000..9574986
--- /dev/null
+++ b/api/boto/ec2/connection.py
@@ -0,0 +1,1520 @@
+# 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 connection to the EC2 service.
+"""
+
+import urllib
+import xml.sax
+import base64
+import boto
+from boto import config
+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.launchspecification import LaunchSpecification
+from boto.exception import EC2ResponseError
+
+#boto.set_stream_logger('ec2')
+
+class EC2Connection(AWSQueryConnection):
+
+ APIVersion = boto.config.get('Boto', 'ec2_version', '2009-11-30')
+ DefaultRegionName = boto.config.get('Boto', 'ec2_region_name', 'us-east-1')
+ DefaultRegionEndpoint = boto.config.get('Boto', 'ec2_region_endpoint',
+ 'ec2.amazonaws.com')
+ SignatureVersion = '2'
+ 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 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
+
+ # Image methods
+
+ def get_all_images(self, image_ids=None, owners=None, executable_by=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:
+ :param executable_by:
+
+ :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')
+ return self.get_list('DescribeImages', params, [('item', Image)])
+
+ def get_all_kernels(self, kernel_ids=None, owners=None):
+ """
+ Retrieve all the EC2 kernels available on your account. Simply filters the list returned
+ by get_all_images because EC2 does not provide a way to filter 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`
+ """
+ rs = self.get_all_images(kernel_ids, owners)
+ kernels = []
+ for image in rs:
+ if image.type == 'kernel':
+ kernels.append(image)
+ return kernels
+
+ def get_all_ramdisks(self, ramdisk_ids=None, owners=None):
+ """
+ Retrieve all the EC2 ramdisks available on your account.
+ Simply filters the list returned by get_all_images because
+ EC2 does not provide a way to filter 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`
+ """
+ rs = self.get_all_images(ramdisk_ids, owners)
+ ramdisks = []
+ for image in rs:
+ if image.type == 'ramdisk':
+ ramdisks.append(image)
+ return ramdisks
+
+ 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, 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.
+
+ :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 = {'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['Location'] = 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)
+ 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})
+
+ 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'
+ rs = self.get_object('CreateImage', params, Image)
+ image_id = getattr(rs, 'imageId', None)
+ if not image_id:
+ image_id = getattr(rs, 'ImageId', None)
+ return image_id
+
+ # ImageAttribute methods
+
+ def get_image_attribute(self, image_id, attribute='launchPermission'):
+ """
+ Gets an attribute from an image.
+ See http://docs.amazonwebservices.com/AWSEC2/2008-02-01/DeveloperGuide/ApiReference-Query-DescribeImageAttribute.html
+
+ :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)
+
+ 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.
+ See http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/ApiReference-query-ModifyImageAttribute.html
+
+ :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)
+
+ def reset_image_attribute(self, image_id, attribute='launchPermission'):
+ """
+ Resets an attribute of an AMI to its default value.
+ See http://docs.amazonwebservices.com/AWSEC2/2008-02-01/DeveloperGuide/ApiReference-Query-ResetImageAttribute.html
+
+ :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)
+
+ # Instance methods
+
+ def get_all_instances(self, instance_ids=None):
+ """
+ Retrieve all the instances associated with your account.
+
+ :type instance_ids: list
+ :param instance_ids: A list of strings of instance IDs
+
+ :rtype: list
+ :return: A list of :class:`boto.ec2.instance.Reservation`
+ """
+ params = {}
+ if instance_ids:
+ self.build_list_params(params, instance_ids, 'InstanceId')
+ return self.get_list('DescribeInstances', params, [('item', Reservation)])
+
+ 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):
+ """
+ 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)
+
+ :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.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 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 block_device_map:
+ block_device_map.build_list_params(params)
+ 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)])
+
+ def stop_instances(self, instance_ids=None):
+ """
+ Stop the instances specified
+
+ :type instance_ids: list
+ :param instance_ids: A list of strings of the Instance IDs to stop
+
+ :rtype: list
+ :return: A list of the instances stopped
+ """
+ params = {}
+ if instance_ids:
+ self.build_list_params(params, instance_ids, 'InstanceId')
+ return self.get_list('StopInstances', params, [('item', Instance)])
+
+ 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)])
+
+ def get_console_output(self, instance_id):
+ """
+ Retrieves the console output for the specified instance.
+ See http://docs.amazonwebservices.com/AWSEC2/2008-02-01/DeveloperGuide/ApiReference-Query-GetConsoleOutput.html
+
+ :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)
+
+ 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)
+ 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.ImageAttribute`
+ :return: An ImageAttribute object representing the value of the attribute requested
+ """
+ params = {'InstanceId' : instance_id}
+ if attribute:
+ params['Attribute'] = attribute
+ return self.get_object('DescribeInstanceAttribute', params, InstanceAttribute)
+
+ 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.
+ See http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/ApiReference-query-ModifyImageAttribute.html
+
+ :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)
+
+ def reset_image_attribute(self, image_id, attribute='launchPermission'):
+ """
+ Resets an attribute of an AMI to its default value.
+ See http://docs.amazonwebservices.com/AWSEC2/2008-02-01/DeveloperGuide/ApiReference-Query-ResetImageAttribute.html
+
+ :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)
+
+ # Spot Instances
+
+ def get_all_spot_instance_requests(self, request_ids=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
+
+ @rtype: list
+ @return: A list of
+ :class:`boto.ec2.spotinstancerequest.SpotInstanceRequest`
+ """
+ params = {}
+ if request_ids:
+ self.build_list_params(params, request_ids, 'SpotInstanceRequestId')
+ return self.get_list('DescribeSpotInstanceRequests', params,
+ [('item', SpotInstanceRequest)])
+
+ 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)])
+
+ def request_spot_instances(self, price, image_id, count=1, type=None,
+ 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)
+
+ :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.instance.Reservation` associated with the request for machines
+ """
+ params = {'LaunchSpecification.ImageId':image_id,
+ '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)])
+
+ 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)
+
+ 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.
+
+ :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)
+
+ 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)
+
+ # Zone methods
+
+ def get_all_zones(self, zones=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.
+
+ :rtype: list of L{boto.ec2.zone.Zone}
+ :return: The requested Zone objects
+ """
+ params = {}
+ if zones:
+ self.build_list_params(params, zones, 'ZoneName')
+ return self.get_list('DescribeAvailabilityZones', params, [('item', Zone)])
+
+ # Address methods
+
+ def get_all_addresses(self, addresses=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.
+
+ :rtype: list of L{boto.ec2.address.Address}
+ :return: The requested Address objects
+ """
+ params = {}
+ if addresses:
+ self.build_list_params(params, addresses, 'PublicIp')
+ return self.get_list('DescribeAddresses', params, [('item', Address)])
+
+ def allocate_address(self):
+ """
+ Allocate a new Elastic IP address and associate it with your account.
+
+ :rtype: L{boto.ec2.address.Address}
+ :return: The newly allocated Address
+ """
+ return self.get_object('AllocateAddress', None, Address)
+
+ 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)
+
+ 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)
+
+ 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)
+
+ # Volume methods
+
+ def get_all_volumes(self, volume_ids=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.
+
+ :rtype: list of L{boto.ec2.volume.Volume}
+ :return: The requested Volume objects
+ """
+ params = {}
+ if volume_ids:
+ self.build_list_params(params, volume_ids, 'VolumeId')
+ return self.get_list('DescribeVolumes', params, [('item', Volume)])
+
+ 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 L{boto.ec2.zone.Zone}
+ :param zone: The availability zone in which the Volume will be created.
+
+ :type snapshot: string or L{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)
+
+ 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)
+
+ 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)
+
+ 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)
+
+ # Snapshot methods
+
+ def get_all_snapshots(self, snapshot_ids=None, owner=None, restorable_by=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.
+
+ :rtype: list of L{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
+ return self.get_list('DescribeSnapshots', params, [('item', Snapshot)])
+
+ 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]
+ return self.get_object('CreateSnapshot', params, Snapshot)
+
+ def delete_snapshot(self, snapshot_id):
+ params = {'SnapshotId': snapshot_id}
+ return self.get_status('DeleteSnapshot', params)
+
+ 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 L{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)
+
+ 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)
+
+ 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)
+
+ # Keypair methods
+
+ def get_all_key_pairs(self, keynames=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.
+
+ :rtype: list
+ :return: A list of :class:`boto.ec2.keypair.KeyPair`
+ """
+ params = {}
+ if keynames:
+ self.build_list_params(params, keynames, 'KeyName')
+ return self.get_list('DescribeKeyPairs', params, [('item', KeyPair)])
+
+ 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)
+
+ 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)
+
+ # SecurityGroup methods
+
+ def get_all_security_groups(self, groupnames=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.
+
+ :rtype: list
+ :return: A list of :class:`boto.ec2.securitygroup.SecurityGroup`
+ """
+ params = {}
+ if groupnames:
+ self.build_list_params(params, groupnames, 'GroupName')
+ return self.get_list('DescribeSecurityGroups', params, [('item', SecurityGroup)])
+
+ 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)
+ 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)
+
+ 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 to_port: string
+ :param to_port: The CIDR block you are providing access to.
+ See http://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing
+
+ :rtype: bool
+ :return: True if successful.
+ """
+ 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
+ if ip_protocol:
+ params['IpProtocol'] = ip_protocol
+ if from_port:
+ params['FromPort'] = from_port
+ if to_port:
+ params['ToPort'] = to_port
+ if cidr_ip:
+ params['CidrIp'] = urllib.quote(cidr_ip)
+ return self.get_status('AuthorizeSecurityGroupIngress', params)
+
+ 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 to_port: string
+ :param to_port: The CIDR block you are revoking access to.
+ See http://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing
+
+ :rtype: bool
+ :return: True if successful.
+ """
+ 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
+ if ip_protocol:
+ params['IpProtocol'] = ip_protocol
+ if from_port:
+ params['FromPort'] = from_port
+ if to_port:
+ params['ToPort'] = to_port
+ if cidr_ip:
+ params['CidrIp'] = cidr_ip
+ return self.get_status('RevokeSecurityGroupIngress', params)
+
+ #
+ # Regions
+ #
+
+ def get_all_regions(self):
+ """
+ Get all available regions for the EC2 service.
+
+ :rtype: list
+ :return: A list of :class:`boto.ec2.regioninfo.RegionInfo`
+ """
+ return self.get_list('DescribeRegions', None, [('item', RegionInfo)])
+
+ #
+ # Reservation methods
+ #
+
+ def get_all_reserved_instances_offerings(self, reserved_instances_id=None,
+ instance_type=None,
+ availability_zone=None,
+ product_description=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.
+
+ :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
+
+ return self.get_list('DescribeReservedInstancesOfferings',
+ params, [('item', ReservedInstancesOffering)])
+
+ def get_all_reserved_instances(self, reserved_instances_id=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.
+
+ :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')
+ return self.get_list('DescribeReservedInstances',
+ params, [('item', ReservedInstance)])
+
+ 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)
+
+ #
+ # 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)])
+
+ 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)])
+
diff --git a/api/boto/ec2/ec2object.py b/api/boto/ec2/ec2object.py
new file mode 100644
index 0000000..9ffab5d
--- /dev/null
+++ b/api/boto/ec2/ec2object.py
@@ -0,0 +1,41 @@
+# 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 Object
+"""
+
+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)
+
+
diff --git a/api/boto/ec2/elb/__init__.py b/api/boto/ec2/elb/__init__.py
new file mode 100644
index 0000000..55e846f
--- /dev/null
+++ b/api/boto/ec2/elb/__init__.py
@@ -0,0 +1,238 @@
+# 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
+import boto
+
+class ELBConnection(AWSQueryConnection):
+
+ APIVersion = boto.config.get('Boto', 'elb_version', '2009-05-15')
+ Endpoint = boto.config.get('Boto', 'elb_endpoint', 'elasticloadbalancing.amazonaws.com')
+ SignatureVersion = '1'
+ #ResponseError = EC2ResponseError
+
+ 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, host=Endpoint, debug=0,
+ https_connection_factory=None, path='/'):
+ """
+ Init method to create a new connection to EC2 Load Balancing Service.
+
+ B{Note:} The host argument is overridden by the host specified in the boto configuration file.
+ """
+ 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 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_name=None):
+ """
+ Retrieve all load balancers associated with your account.
+
+ :type load_balancer_names: str
+ :param load_balancer_names: An optional filter string to get only one ELB
+
+ :rtype: list
+ :return: A list of :class:`boto.ec2.elb.loadbalancer.LoadBalancer`
+ """
+ params = {}
+ if load_balancer_name:
+ #self.build_list_params(params, load_balancer_names, 'LoadBalancerName.%d')
+ params['LoadBalancerName'] = load_balancer_name
+ 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 values.
+ (LoadBalancerPortNumber, InstancePortNumber, Protocol)
+ where LoadBalancerPortNumber and InstancePortNumber are
+ integer values between 1 and 65535 and Protocol is a
+ string containing either 'TCP' or 'HTTP'.
+
+ :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]
+ 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 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 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')
+ 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)
diff --git a/api/boto/ec2/elb/healthcheck.py b/api/boto/ec2/elb/healthcheck.py
new file mode 100644
index 0000000..5a3edbc
--- /dev/null
+++ b/api/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/api/boto/ec2/elb/instancestate.py b/api/boto/ec2/elb/instancestate.py
new file mode 100644
index 0000000..4a9b0d4
--- /dev/null
+++ b/api/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/api/boto/ec2/elb/listelement.py b/api/boto/ec2/elb/listelement.py
new file mode 100644
index 0000000..5be4599
--- /dev/null
+++ b/api/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/api/boto/ec2/elb/listener.py b/api/boto/ec2/elb/listener.py
new file mode 100644
index 0000000..ab482c2
--- /dev/null
+++ b/api/boto/ec2/elb/listener.py
@@ -0,0 +1,64 @@
+# 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=''):
+ self.load_balancer = load_balancer
+ self.load_balancer_port = load_balancer_port
+ self.instance_port = instance_port
+ self.protocol = protocol
+
+ def __repr__(self):
+ return "(%d, %d, '%s')" % (self.load_balancer_port, self.instance_port, self.protocol)
+
+ 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
+ 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/api/boto/ec2/elb/loadbalancer.py b/api/boto/ec2/elb/loadbalancer.py
new file mode 100644
index 0000000..2902107
--- /dev/null
+++ b/api/boto/ec2/elb/loadbalancer.py
@@ -0,0 +1,144 @@
+# 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.instancestate import InstanceState
+from boto.ec2.elb.listener import Listener
+from boto.ec2.elb.listelement import ListElement
+from boto.ec2.zone import Zone
+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.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 == 'Listeners':
+ 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
+ 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):
+ self.connection.configure_health_check(self.name, health_check)
+
+ def get_instance_health(self, instances=None):
+ self.connection.describe_instance_health(self.name, instances)
+
diff --git a/api/boto/ec2/image.py b/api/boto/ec2/image.py
new file mode 100644
index 0000000..8ef2513
--- /dev/null
+++ b/api/boto/ec2/image.py
@@ -0,0 +1,243 @@
+# 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.ec2.ec2object import EC2Object
+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(EC2Object):
+ """
+ Represents an EC2 Image
+ """
+
+ def __init__(self, connection=None):
+ EC2Object.__init__(self, connection)
+ self.id = None
+ self.location = None
+ self.state = None
+ self.ownerId = 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
+
+ def __repr__(self):
+ return 'Image:%s' % self.id
+
+ def startElement(self, name, attrs, connection):
+ 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
+ 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
+ else:
+ setattr(self, name, value)
+
+ 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):
+ """
+ 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 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 (m1.small, m1.large, m1.xlarge)
+
+ :type placement:
+ :param placement:
+
+ :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.
+
+ :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)
+
+ 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/api/boto/ec2/instance.py b/api/boto/ec2/instance.py
new file mode 100644
index 0000000..5932c4e
--- /dev/null
+++ b/api/boto/ec2/instance.py
@@ -0,0 +1,280 @@
+# 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 Instance
+"""
+import boto
+from boto.ec2.ec2object import EC2Object
+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(EC2Object):
+
+ def __init__(self, connection=None):
+ EC2Object.__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.block_device_mapping = None
+
+ def __repr__(self):
+ return 'Instance:%s' % self.id
+
+ def startElement(self, name, attrs, connection):
+ 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
+ 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 == '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
+ else:
+ setattr(self, name, value)
+
+ def _update(self, updated):
+ self.updated = updated
+ if hasattr(updated, 'dns_name'):
+ self.dns_name = updated.dns_name
+ self.public_dns_name = updated.dns_name
+ if hasattr(updated, 'private_dns_name'):
+ self.private_dns_name = updated.private_dns_name
+ if hasattr(updated, 'ami_launch_index'):
+ self.ami_launch_index = updated.ami_launch_index
+ self.shutdown_state = updated.shutdown_state
+ self.previous_state = updated.previous_state
+ if hasattr(updated, 'state'):
+ self.state = updated.state
+ else:
+ self.state = None
+ if hasattr(updated, 'state_code'):
+ self.state_code = updated.state_code
+ else:
+ self.state_code = None
+
+ def update(self):
+ rs = self.connection.get_all_instances([self.id])
+ if len(rs) > 0:
+ self._update(rs[0].instances[0])
+ return self.state
+
+ def stop(self):
+ rs = self.connection.terminate_instances([self.id])
+ self._update(rs[0])
+
+ def reboot(self):
+ return self.connection.reboot_instances([self.id])
+
+ def get_console_output(self):
+ 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)
+
+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
diff --git a/api/boto/ec2/instanceinfo.py b/api/boto/ec2/instanceinfo.py
new file mode 100644
index 0000000..6efbaed
--- /dev/null
+++ b/api/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/api/boto/ec2/keypair.py b/api/boto/ec2/keypair.py
new file mode 100644
index 0000000..d08e5ce
--- /dev/null
+++ b/api/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/api/boto/ec2/launchspecification.py b/api/boto/ec2/launchspecification.py
new file mode 100644
index 0000000..a574a38
--- /dev/null
+++ b/api/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/api/boto/ec2/regioninfo.py b/api/boto/ec2/regioninfo.py
new file mode 100644
index 0000000..ab61703
--- /dev/null
+++ b/api/boto/ec2/regioninfo.py
@@ -0,0 +1,60 @@
+# 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 RegionInfo(object):
+ """
+ Represents an EC2 Region
+ """
+
+ def __init__(self, connection=None, name=None, endpoint=None):
+ self.connection = connection
+ self.name = name
+ self.endpoint = endpoint
+
+ def __repr__(self):
+ return 'RegionInfo:%s' % self.name
+
+ def startElement(self, name, attrs, connection):
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'regionName':
+ self.name = value
+ elif name == 'regionEndpoint':
+ self.endpoint = value
+ else:
+ setattr(self, name, value)
+
+ def connect(self, **kw_params):
+ """
+ Connect to this Region's endpoint. Returns an EC2Connection
+ object pointing to the endpoint associated with this region.
+ 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: :class:`boto.ec2.connection.EC2Connection`
+ :return: The connection to this regions endpoint
+ """
+ from boto.ec2.connection import EC2Connection
+ return EC2Connection(region=self, **kw_params)
+
+
diff --git a/api/boto/ec2/reservedinstance.py b/api/boto/ec2/reservedinstance.py
new file mode 100644
index 0000000..1d35c1d
--- /dev/null
+++ b/api/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/api/boto/ec2/securitygroup.py b/api/boto/ec2/securitygroup.py
new file mode 100644
index 0000000..6f17ad3
--- /dev/null
+++ b/api/boto/ec2/securitygroup.py
@@ -0,0 +1,281 @@
+# 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
+
+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 image %s'%(
+ value,
+ self.id
+ )
+ )
+ 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):
+ 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):
+ 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:
+ from_port = None
+ to_port = None
+ cidr_ip = None
+ ip_protocol = 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:
+ from_port=None
+ to_port=None
+ cidr_ip=None
+ ip_protocol = 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, owner_id=None, name=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/api/boto/ec2/snapshot.py b/api/boto/ec2/snapshot.py
new file mode 100644
index 0000000..33b53b0
--- /dev/null
+++ b/api/boto/ec2/snapshot.py
@@ -0,0 +1,124 @@
+# 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 Snapshot
+"""
+from boto.ec2.ec2object import EC2Object
+
+class Snapshot(EC2Object):
+
+ def __init__(self, connection=None):
+ EC2Object.__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':
+ self.volume_size = int(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):
+ rs = self.connection.get_all_snapshots([self.id])
+ if len(rs) > 0:
+ self._update(rs[0])
+ 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/api/boto/ec2/spotdatafeedsubscription.py b/api/boto/ec2/spotdatafeedsubscription.py
new file mode 100644
index 0000000..9b820a3
--- /dev/null
+++ b/api/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/api/boto/ec2/spotinstancerequest.py b/api/boto/ec2/spotinstancerequest.py
new file mode 100644
index 0000000..5b1d7ce
--- /dev/null
+++ b/api/boto/ec2/spotinstancerequest.py
@@ -0,0 +1,106 @@
+# 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
+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 = code
+ elif name == 'message':
+ self.message = message
+ setattr(self, name, value)
+
+class SpotInstanceRequest(EC2Object):
+
+ def __init__(self, connection=None):
+ EC2Object.__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
+
+ def __repr__(self):
+ return 'SpotInstanceRequest:%s' % self.id
+
+ def startElement(self, name, attrs, connection):
+ 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
+ else:
+ setattr(self, name, value)
+
+ def cancel(self):
+ self.connection.cancel_spot_instance_requests([self.id])
+
+
+
diff --git a/api/boto/ec2/spotpricehistory.py b/api/boto/ec2/spotpricehistory.py
new file mode 100644
index 0000000..d4e1711
--- /dev/null
+++ b/api/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/api/boto/ec2/volume.py b/api/boto/ec2/volume.py
new file mode 100644
index 0000000..200ca90
--- /dev/null
+++ b/api/boto/ec2/volume.py
@@ -0,0 +1,229 @@
+# 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 Elastic IP Volume
+"""
+from boto.ec2.ec2object import EC2Object
+
+class Volume(EC2Object):
+
+ def __init__(self, connection=None):
+ EC2Object.__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):
+ if name == 'attachmentSet':
+ self.attach_data = AttachmentSet()
+ return self.attach_data
+ 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.updated = updated
+ if hasattr(updated, 'create_time'):
+ self.create_time = updated.create_time
+ if hasattr(updated, 'status'):
+ self.status = updated.status
+ else:
+ self.status = None
+ if hasattr(updated, 'size'):
+ self.size = updated.size
+ if hasattr(updated, 'snapshot_id'):
+ self.snapshot_id = updated.snapshot_id
+ if hasattr(updated, 'attach_data'):
+ self.attach_data = updated.attach_data
+ if hasattr(updated, 'zone'):
+ self.zone = updated.zone
+
+ def update(self):
+ rs = self.connection.get_all_volumes([self.id])
+ if len(rs) > 0:
+ self._update(rs[0])
+ 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 attachmentSet information for the volume. This info is stored
+ in a dictionary object and contains at least the following info:
+
+ - volumeId
+ - instanceId
+ - device
+ - status
+ - attachTime
+ """
+ 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/api/boto/ec2/zone.py b/api/boto/ec2/zone.py
new file mode 100644
index 0000000..aec79b2
--- /dev/null
+++ b/api/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/api/boto/exception.py b/api/boto/exception.py
new file mode 100644
index 0000000..ba65694
--- /dev/null
+++ b/api/boto/exception.py
@@ -0,0 +1,284 @@
+# 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.
+
+"""
+Exception classes - Subclassing allows you to check for specific errors
+"""
+
+from boto import handler
+from boto.resultset import ResultSet
+
+import xml.sax
+
+class BotoClientError(StandardError):
+ """
+ General Boto Client error (error accessing AWS)
+ """
+
+ def __init__(self, reason):
+ self.reason = reason
+
+ def __repr__(self):
+ return 'S3Error: %s' % self.reason
+
+ def __str__(self):
+ return 'S3Error: %s' % self.reason
+
+class SDBPersistenceError(StandardError):
+
+ pass
+
+class S3PermissionsError(BotoClientError):
+ """
+ Permissions error when accessing a bucket or key on S3.
+ """
+ pass
+
+class BotoServerError(StandardError):
+
+ def __init__(self, status, reason, body=None):
+ 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
+
+ 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 S3CreateError(BotoServerError):
+ """
+ Error creating a bucket or key on S3.
+ """
+ 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 S3CopyError(BotoServerError):
+ """
+ Error copying a key on S3.
+ """
+ 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):
+ self.reason = reason
+ self.message = message
+
+ def __repr__(self):
+ return 'SQSDecodeError: %s' % self.reason
+
+ def __str__(self):
+ return 'SQSDecodeError: %s' % self.reason
+
+class S3ResponseError(BotoServerError):
+ """
+ Error in response from S3.
+ """
+ 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 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 _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 respones from SDB.
+ """
+ pass
+
+class AWSConnectionError(BotoClientError):
+ """
+ General error connecting to Amazon Web Services.
+ """
+ pass
+
+class S3DataError(BotoClientError):
+ """
+ Error receiving data from S3.
+ """
+ pass
+
+class FPSResponseError(BotoServerError):
+ pass
diff --git a/api/boto/fps/__init__.py b/api/boto/fps/__init__.py
new file mode 100644
index 0000000..2f44483
--- /dev/null
+++ b/api/boto/fps/__init__.py
@@ -0,0 +1,23 @@
+# Copyright (c) 2008, 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.
+#
+
+
diff --git a/api/boto/fps/connection.py b/api/boto/fps/connection.py
new file mode 100644
index 0000000..0f14775
--- /dev/null
+++ b/api/boto/fps/connection.py
@@ -0,0 +1,173 @@
+# Copyright (c) 2008 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 urllib
+import xml.sax
+import uuid
+import boto
+import boto.utils
+import urllib
+from boto import handler
+from boto.connection import AWSQueryConnection
+from boto.resultset import ResultSet
+from boto.exception import FPSResponseError
+
+class FPSConnection(AWSQueryConnection):
+
+ APIVersion = '2007-01-08'
+ SignatureVersion = '1'
+
+ def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
+ is_secure=True, port=None, proxy=None, proxy_port=None,
+ host='fps.sandbox.amazonaws.com', debug=0,
+ https_connection_factory=None):
+ AWSQueryConnection.__init__(self, aws_access_key_id,
+ aws_secret_access_key,
+ is_secure, port, proxy, proxy_port,
+ host, debug, https_connection_factory)
+
+ def install_payment_instruction(self, instruction, token_type="Unrestricted", transaction_id=None):
+ """
+ InstallPaymentInstruction
+ instruction: The PaymentInstruction to send, for example:
+
+ MyRole=='Caller' orSay 'Roles do not match';
+
+ token_type: Defaults to "Unrestricted"
+ transaction_id: Defaults to a new ID
+ """
+
+ if(transaction_id == None):
+ transaction_id = uuid.uuid4()
+ params = {}
+ params['PaymentInstruction'] = instruction
+ params['TokenType'] = token_type
+ params['CallerReference'] = transaction_id
+ response = self.make_request("InstallPaymentInstruction", params)
+ return response
+
+ def install_caller_instruction(self, token_type="Unrestricted", transaction_id=None):
+ """
+ Set us up as a caller
+ This will install a new caller_token into the FPS section.
+ This should really only be called to regenerate the caller token.
+ """
+ response = self.install_payment_instruction("MyRole=='Caller';", token_type=token_type, transaction_id=transaction_id)
+ body = response.read()
+ if(response.status == 200):
+ rs = ResultSet()
+ h = handler.XmlHandler(rs, self)
+ xml.sax.parseString(body, h)
+ caller_token = rs.TokenId
+ try:
+ boto.config.save_system_option("FPS", "caller_token", caller_token)
+ except(IOError):
+ boto.config.save_user_option("FPS", "caller_token", caller_token)
+ return caller_token
+ else:
+ raise FPSResponseError(response.status, respons.reason, body)
+
+ def install_recipient_instruction(self, token_type="Unrestricted", transaction_id=None):
+ """
+ Set us up as a Recipient
+ This will install a new caller_token into the FPS section.
+ This should really only be called to regenerate the recipient token.
+ """
+ response = self.install_payment_instruction("MyRole=='Recipient';", token_type=token_type, transaction_id=transaction_id)
+ body = response.read()
+ if(response.status == 200):
+ rs = ResultSet()
+ h = handler.XmlHandler(rs, self)
+ xml.sax.parseString(body, h)
+ recipient_token = rs.TokenId
+ try:
+ boto.config.save_system_option("FPS", "recipient_token", recipient_token)
+ except(IOError):
+ boto.config.save_user_option("FPS", "recipient_token", recipient_token)
+
+ return recipient_token
+ else:
+ raise FPSResponseError(response.status, respons.reason, body)
+
+ def make_url(self, returnURL, paymentReason, pipelineName, **params):
+ """
+ Generate the URL with the signature required for a transaction
+ """
+ params['callerKey'] = str(self.aws_access_key_id)
+ params['returnURL'] = str(returnURL)
+ params['paymentReason'] = str(paymentReason)
+ params['pipelineName'] = pipelineName
+
+ if(not params.has_key('callerReference')):
+ params['callerReference'] = str(uuid.uuid4())
+
+ url = ""
+ keys = params.keys()
+ keys.sort()
+ for k in keys:
+ url += "&%s=%s" % (k, urllib.quote_plus(str(params[k])))
+
+ url = "/cobranded-ui/actions/start?%s" % ( url[1:])
+ signature= boto.utils.encode(self.aws_secret_access_key, url, True)
+ return "https://authorize.payments-sandbox.amazon.com%s&awsSignature=%s" % (url, signature)
+
+ def make_payment(self, amount, sender_token, charge_fee_to="Recipient", reference=None, senderReference=None, recipientReference=None, senderDescription=None, recipientDescription=None, callerDescription=None, metadata=None, transactionDate=None):
+ """
+ Make a payment transaction
+ You must specify the amount and the sender token.
+ """
+ params = {}
+ params['RecipientTokenId'] = boto.config.get("FPS", "recipient_token")
+ params['CallerTokenId'] = boto.config.get("FPS", "caller_token")
+ params['SenderTokenId'] = sender_token
+ params['TransactionAmount.Amount'] = str(amount)
+ params['TransactionAmount.CurrencyCode'] = "USD"
+ params['ChargeFeeTo'] = charge_fee_to
+
+ if(transactionDate != None):
+ params['TransactionDate'] = transactionDate
+ if(senderReference != None):
+ params['SenderReference'] = senderReference
+ if(recipientReference != None):
+ params['RecipientReference'] = recipientReference
+ if(senderDescription != None):
+ params['SenderDescription'] = senderDescription
+ if(recipientDescription != None):
+ params['RecipientDescription'] = recipientDescription
+ if(callerDescription != None):
+ params['CallerDescription'] = callerDescription
+ if(metadata != None):
+ params['MetaData'] = metadata
+ if(transactionDate != None):
+ params['TransactionDate'] = transactionDate
+ if(reference == None):
+ reference = uuid.uuid4()
+ params['CallerReference'] = reference
+
+ response = self.make_request("Pay", params)
+ body = response.read()
+ if(response.status == 200):
+ rs = ResultSet()
+ h = handler.XmlHandler(rs, self)
+ xml.sax.parseString(body, h)
+ return rs
+ else:
+ raise FPSResponseError(response.status, response.reason, body)
diff --git a/api/boto/handler.py b/api/boto/handler.py
new file mode 100644
index 0000000..525f9c9
--- /dev/null
+++ b/api/boto/handler.py
@@ -0,0 +1,46 @@
+# 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 xml.sax
+
+class XmlHandler(xml.sax.ContentHandler):
+
+ def __init__(self, root_node, connection):
+ self.connection = connection
+ self.nodes = [('root', root_node)]
+ self.current_text = ''
+
+ def startElement(self, name, attrs):
+ self.current_text = ''
+ new_node = self.nodes[-1][1].startElement(name, attrs, self.connection)
+ if new_node != None:
+ self.nodes.append((name, new_node))
+
+ def endElement(self, name):
+ self.nodes[-1][1].endElement(name, self.current_text, self.connection)
+ if self.nodes[-1][0] == name:
+ self.nodes.pop()
+ self.current_text = ''
+
+ def characters(self, content):
+ self.current_text += content
+
+
diff --git a/api/boto/manage/__init__.py b/api/boto/manage/__init__.py
new file mode 100644
index 0000000..49d029b
--- /dev/null
+++ b/api/boto/manage/__init__.py
@@ -0,0 +1,23 @@
+# 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.
+#
+
+
diff --git a/api/boto/manage/cmdshell.py b/api/boto/manage/cmdshell.py
new file mode 100644
index 0000000..340b1e2
--- /dev/null
+++ b/api/boto/manage/cmdshell.py
@@ -0,0 +1,165 @@
+# 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.mashups.interactive import interactive_shell
+import boto
+import os, time, shutil
+import StringIO
+import paramiko
+import socket
+
+class SSHClient(object):
+
+ def __init__(self, server, host_key_file='~/.ssh/known_hosts', uname='root'):
+ self.server = server
+ self.host_key_file = host_key_file
+ self.uname = uname
+ self._pkey = paramiko.RSAKey.from_private_key_file(server.ssh_key_file)
+ self._ssh_client = paramiko.SSHClient()
+ self._ssh_client.load_system_host_keys()
+ self._ssh_client.load_host_keys(os.path.expanduser(host_key_file))
+ self._ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ self.connect()
+
+ def connect(self):
+ retry = 0
+ while retry < 5:
+ try:
+ self._ssh_client.connect(self.server.hostname, username=self.uname, pkey=self._pkey)
+ return
+ except socket.error, (value,message):
+ if value == 61:
+ print 'SSH Connection refused, will retry in 5 seconds'
+ time.sleep(5)
+ retry += 1
+ else:
+ raise
+ except paramiko.BadHostKeyException:
+ print "%s has an entry in ~/.ssh/known_hosts and it doesn't match" % self.server.hostname
+ print 'Edit that file to remove the entry and then hit return to try again'
+ rawinput('Hit Enter when ready')
+ retry += 1
+ except EOFError:
+ print 'Unexpected Error from SSH Connection, retry in 5 seconds'
+ time.sleep(5)
+ retry += 1
+ print 'Could not establish SSH connection'
+
+ def get_file(self, src, dst):
+ sftp_client = self._ssh_client.open_sftp()
+ sftp_client.get(src, dst)
+
+ def put_file(self, src, dst):
+ sftp_client = self._ssh_client.open_sftp()
+ sftp_client.put(src, dst)
+
+ def listdir(self, path):
+ sftp_client = self._ssh_client.open_sftp()
+ return sftp_client.listdir(path)
+
+ def open_sftp(self):
+ return self._ssh_client.open_sftp()
+
+ def isdir(self, path):
+ status = self.run('[ -d %s ] || echo "FALSE"' % path)
+ if status[1].startswith('FALSE'):
+ return 0
+ return 1
+
+ def exists(self, path):
+ status = self.run('[ -a %s ] || echo "FALSE"' % path)
+ if status[1].startswith('FALSE'):
+ return 0
+ return 1
+
+ def shell(self):
+ channel = self._ssh_client.invoke_shell()
+ interactive_shell(channel)
+
+ def run(self, command):
+ boto.log.info('running:%s on %s' % (command, self.server.instance_id))
+ log_fp = StringIO.StringIO()
+ status = 0
+ try:
+ t = self._ssh_client.exec_command(command)
+ except paramiko.SSHException:
+ status = 1
+ log_fp.write(t[1].read())
+ log_fp.write(t[2].read())
+ t[0].close()
+ t[1].close()
+ t[2].close()
+ boto.log.info('output: %s' % log_fp.getvalue())
+ return (status, log_fp.getvalue())
+
+ def close(self):
+ transport = self._ssh_client.get_transport()
+ transport.close()
+ self.server.reset_cmdshell()
+
+class LocalClient(object):
+
+ def __init__(self, server, host_key_file=None, uname='root'):
+ self.server = server
+ self.host_key_file = host_key_file
+ self.uname = uname
+
+ def get_file(self, src, dst):
+ shutil.copyfile(src, dst)
+
+ def put_file(self, src, dst):
+ shutil.copyfile(src, dst)
+
+ def listdir(self, path):
+ return os.listdir(path)
+
+ def isdir(self, path):
+ return os.path.isdir(path)
+
+ def exists(self, path):
+ return os.path.exists(path)
+
+ def shell(self):
+ raise NotImplementedError, 'shell not supported with LocalClient'
+
+ def run(self):
+ boto.log.info('running:%s' % self.command)
+ log_fp = StringIO.StringIO()
+ process = subprocess.Popen(self.command, shell=True, stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ while process.poll() == None:
+ time.sleep(1)
+ t = process.communicate()
+ log_fp.write(t[0])
+ log_fp.write(t[1])
+ boto.log.info(log_fp.getvalue())
+ boto.log.info('output: %s' % log_fp.getvalue())
+ return (process.returncode, log_fp.getvalue())
+
+ def close(self):
+ pass
+
+def start(server):
+ instance_id = boto.config.get('Instance', 'instance-id', None)
+ if instance_id == server.instance_id:
+ return LocalClient(server)
+ else:
+ return SSHClient(server)
diff --git a/api/boto/manage/propget.py b/api/boto/manage/propget.py
new file mode 100644
index 0000000..172e1aa
--- /dev/null
+++ b/api/boto/manage/propget.py
@@ -0,0 +1,66 @@
+# 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 os
+from boto.sdb.db.property import *
+
+def get(prop, choices=None):
+ prompt = prop.verbose_name
+ if not prompt:
+ prompt = prop.name
+ if choices:
+ if callable(choices):
+ choices = choices()
+ else:
+ choices = prop.get_choices()
+ valid = False
+ while not valid:
+ if choices:
+ min = 1
+ max = len(choices)
+ for i in range(min, max+1):
+ value = choices[i-1]
+ if isinstance(value, tuple):
+ value = value[0]
+ print '[%d] %s' % (i, value)
+ value = raw_input('%s [%d-%d]: ' % (prompt, min, max))
+ try:
+ int_value = int(value)
+ value = choices[int_value-1]
+ if isinstance(value, tuple):
+ value = value[1]
+ valid = True
+ except ValueError:
+ print '%s is not a valid choice' % value
+ except IndexError:
+ print '%s is not within the range[%d-%d]' % (min, max)
+ else:
+ value = raw_input('%s: ' % prompt)
+ try:
+ value = prop.validate(value)
+ if prop.empty(value) and prop.required:
+ print 'A value is required'
+ else:
+ valid = True
+ except:
+ print 'Invalid value: %s' % value
+ return value
+
diff --git a/api/boto/manage/server.py b/api/boto/manage/server.py
new file mode 100644
index 0000000..cc623ef
--- /dev/null
+++ b/api/boto/manage/server.py
@@ -0,0 +1,542 @@
+# 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.
+
+"""
+High-level abstraction of an EC2 server
+"""
+from __future__ import with_statement
+import boto.ec2
+from boto.mashups.iobject import IObject
+from boto.pyami.config import BotoConfigPath, Config
+from boto.sdb.db.model import Model
+from boto.sdb.db.property import *
+from boto.manage import propget
+from boto.ec2.zone import Zone
+from boto.ec2.keypair import KeyPair
+import os, time, StringIO
+from contextlib import closing
+from boto.exception import EC2ResponseError
+
+InstanceTypes = ['m1.small', 'm1.large', 'm1.xlarge',
+ 'c1.medium', 'c1.xlarge',
+ 'm2.2xlarge', 'm2.4xlarge']
+
+class Bundler(object):
+
+ def __init__(self, server, uname='root'):
+ from boto.manage.cmdshell import SSHClient
+ self.server = server
+ self.uname = uname
+ self.ssh_client = SSHClient(server, uname=uname)
+
+ def copy_x509(self, key_file, cert_file):
+ print '\tcopying cert and pk over to /mnt directory on server'
+ sftp_client = self.ssh_client.open_sftp()
+ path, name = os.path.split(key_file)
+ self.remote_key_file = '/mnt/%s' % name
+ self.ssh_client.put_file(key_file, self.remote_key_file)
+ path, name = os.path.split(cert_file)
+ self.remote_cert_file = '/mnt/%s' % name
+ self.ssh_client.put_file(cert_file, self.remote_cert_file)
+ print '...complete!'
+
+ def bundle_image(self, prefix, size, ssh_key):
+ command = ""
+ if self.uname != 'root':
+ command = "sudo "
+ command += 'ec2-bundle-vol '
+ command += '-c %s -k %s ' % (self.remote_cert_file, self.remote_key_file)
+ command += '-u %s ' % self.server._reservation.owner_id
+ command += '-p %s ' % prefix
+ command += '-s %d ' % size
+ command += '-d /mnt '
+ if self.server.instance_type == 'm1.small' or self.server.instance_type == 'c1.medium':
+ command += '-r i386'
+ else:
+ command += '-r x86_64'
+ return command
+
+ def upload_bundle(self, bucket, prefix, ssh_key):
+ command = ""
+ if self.uname != 'root':
+ command = "sudo "
+ command += 'ec2-upload-bundle '
+ command += '-m /mnt/%s.manifest.xml ' % prefix
+ command += '-b %s ' % bucket
+ command += '-a %s ' % self.server.ec2.aws_access_key_id
+ command += '-s %s ' % self.server.ec2.aws_secret_access_key
+ return command
+
+ def bundle(self, bucket=None, prefix=None, key_file=None, cert_file=None,
+ size=None, ssh_key=None, fp=None, clear_history=True):
+ iobject = IObject()
+ if not bucket:
+ bucket = iobject.get_string('Name of S3 bucket')
+ if not prefix:
+ prefix = iobject.get_string('Prefix for AMI file')
+ if not key_file:
+ key_file = iobject.get_filename('Path to RSA private key file')
+ if not cert_file:
+ cert_file = iobject.get_filename('Path to RSA public cert file')
+ if not size:
+ size = iobject.get_int('Size (in MB) of bundled image')
+ if not ssh_key:
+ ssh_key = self.server.get_ssh_key_file()
+ self.copy_x509(key_file, cert_file)
+ if not fp:
+ fp = StringIO.StringIO()
+ fp.write('mv %s /mnt/boto.cfg; ' % BotoConfigPath)
+ fp.write('mv /root/.ssh/authorized_keys /mnt/authorized_keys; ')
+ if clear_history:
+ fp.write('history -c; ')
+ fp.write(self.bundle_image(prefix, size, ssh_key))
+ fp.write('; ')
+ fp.write(self.upload_bundle(bucket, prefix, ssh_key))
+ fp.write('; ')
+ fp.write('mv /mnt/boto.cfg %s; ' % BotoConfigPath)
+ fp.write('mv /mnt/authorized_keys /root/.ssh/authorized_keys\n')
+ command = fp.getvalue()
+ print 'running the following command on the remote server:'
+ print command
+ t = self.ssh_client.run(command)
+ print '\t%s' % t[0]
+ print '\t%s' % t[1]
+ print '...complete!'
+ print 'registering image...'
+ self.image_id = self.server.ec2.register_image('%s/%s.manifest.xml' % (bucket, prefix))
+ return self.image_id
+
+class CommandLineGetter(object):
+
+ def get_ami_list(self):
+ my_amis = []
+ for ami in self.ec2.get_all_images():
+ # hack alert, need a better way to do this!
+ if ami.location.find('pyami') >= 0:
+ my_amis.append((ami.location, ami))
+ return my_amis
+
+ def get_region(self, params):
+ region = params.get('region', None)
+ if isinstance(region, str) or isinstance(region, unicode):
+ region = boto.ec2.get_region(region)
+ params['region'] = region
+ if not region:
+ prop = self.cls.find_property('region_name')
+ params['region'] = propget.get(prop, choices=boto.ec2.regions)
+
+ def get_name(self, params):
+ if not params.get('name', None):
+ prop = self.cls.find_property('name')
+ params['name'] = propget.get(prop)
+
+ def get_description(self, params):
+ if not params.get('description', None):
+ prop = self.cls.find_property('description')
+ params['description'] = propget.get(prop)
+
+ 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_ami_id(self, params):
+ ami = params.get('ami', None)
+ if isinstance(ami, str) or isinstance(ami, unicode):
+ ami_list = self.get_ami_list()
+ for l,a in ami_list:
+ if a.id == ami:
+ ami = a
+ params['ami'] = a
+ if not params.get('ami', None):
+ prop = StringProperty(name='ami', verbose_name='AMI',
+ choices=self.get_ami_list)
+ params['ami'] = propget.get(prop)
+
+ def get_group(self, params):
+ group = params.get('group', None)
+ if isinstance(group, str) or isinstance(group, unicode):
+ group_list = self.ec2.get_all_security_groups()
+ for g in group_list:
+ if g.name == group:
+ group = g
+ params['group'] = g
+ if not group:
+ prop = StringProperty(name='group', verbose_name='EC2 Security Group',
+ choices=self.ec2.get_all_security_groups)
+ params['group'] = propget.get(prop)
+
+ def get_key(self, params):
+ keypair = params.get('keypair', None)
+ if isinstance(keypair, str) or isinstance(keypair, unicode):
+ key_list = self.ec2.get_all_key_pairs()
+ for k in key_list:
+ if k.name == keypair:
+ keypair = k.name
+ params['keypair'] = k.name
+ if not keypair:
+ prop = StringProperty(name='keypair', verbose_name='EC2 KeyPair',
+ choices=self.ec2.get_all_key_pairs)
+ params['keypair'] = propget.get(prop).name
+
+ def get(self, cls, params):
+ self.cls = cls
+ self.get_region(params)
+ self.ec2 = params['region'].connect()
+ self.get_name(params)
+ self.get_description(params)
+ self.get_instance_type(params)
+ self.get_zone(params)
+ self.get_quantity(params)
+ self.get_ami_id(params)
+ self.get_group(params)
+ self.get_key(params)
+
+class Server(Model):
+
+ #
+ # The properties of this object consists of real properties for data that
+ # is not already stored in EC2 somewhere (e.g. name, description) plus
+ # calculated properties for all of the properties that are already in
+ # EC2 (e.g. hostname, security groups, etc.)
+ #
+ name = StringProperty(unique=True, verbose_name="Name")
+ description = StringProperty(verbose_name="Description")
+ region_name = StringProperty(verbose_name="EC2 Region Name")
+ instance_id = StringProperty(verbose_name="EC2 Instance ID")
+ elastic_ip = StringProperty(verbose_name="EC2 Elastic IP Address")
+ production = BooleanProperty(verbose_name="Is This Server Production", default=False)
+ ami_id = CalculatedProperty(verbose_name="AMI ID", calculated_type=str, use_method=True)
+ zone = CalculatedProperty(verbose_name="Availability Zone Name", calculated_type=str, use_method=True)
+ hostname = CalculatedProperty(verbose_name="Public DNS Name", calculated_type=str, use_method=True)
+ private_hostname = CalculatedProperty(verbose_name="Private DNS Name", calculated_type=str, use_method=True)
+ groups = CalculatedProperty(verbose_name="Security Groups", calculated_type=list, use_method=True)
+ security_group = CalculatedProperty(verbose_name="Primary Security Group Name", calculated_type=str, use_method=True)
+ key_name = CalculatedProperty(verbose_name="Key Name", calculated_type=str, use_method=True)
+ instance_type = CalculatedProperty(verbose_name="Instance Type", calculated_type=str, use_method=True)
+ status = CalculatedProperty(verbose_name="Current Status", calculated_type=str, use_method=True)
+ launch_time = CalculatedProperty(verbose_name="Server Launch Time", calculated_type=str, use_method=True)
+ console_output = CalculatedProperty(verbose_name="Console Output", calculated_type=file, use_method=True)
+
+ packages = []
+ plugins = []
+
+ @classmethod
+ def add_credentials(cls, cfg, aws_access_key_id, aws_secret_access_key):
+ if not cfg.has_section('Credentials'):
+ cfg.add_section('Credentials')
+ cfg.set('Credentials', 'aws_access_key_id', aws_access_key_id)
+ cfg.set('Credentials', 'aws_secret_access_key', aws_secret_access_key)
+ if not cfg.has_section('DB_Server'):
+ cfg.add_section('DB_Server')
+ cfg.set('DB_Server', 'db_type', 'SimpleDB')
+ cfg.set('DB_Server', 'db_name', cls._manager.domain.name)
+
+ '''
+ Create a new instance based on the specified configuration file or the specified
+ configuration and the passed in parameters.
+
+ If the config_file argument is not None, the configuration is read from there.
+ Otherwise, the cfg argument is used.
+
+ The config file may include other config files with a #import reference. The included
+ config files must reside in the same directory as the specified file.
+
+ The logical_volume argument, if supplied, will be used to get the current physical
+ volume ID and use that as an override of the value specified in the config file. This
+ may be useful for debugging purposes when you want to debug with a production config
+ file but a test Volume.
+
+ The dictionary argument may be used to override any EC2 configuration values in the
+ config file.
+ '''
+ @classmethod
+ def create(cls, config_file=None, logical_volume = None, cfg = None, **params):
+ if config_file:
+ cfg = Config(path=config_file)
+ if cfg.has_section('EC2'):
+ # include any EC2 configuration values that aren't specified in params:
+ for option in cfg.options('EC2'):
+ if option not in params:
+ params[option] = cfg.get('EC2', option)
+ getter = CommandLineGetter()
+ getter.get(cls, params)
+ region = params.get('region')
+ ec2 = region.connect()
+ cls.add_credentials(cfg, ec2.aws_access_key_id, ec2.aws_secret_access_key)
+ ami = params.get('ami')
+ kp = params.get('keypair')
+ group = params.get('group')
+ zone = params.get('zone')
+ # deal with possibly passed in logical volume:
+ if logical_volume != None:
+ cfg.set('EBS', 'logical_volume_name', logical_volume.name)
+ cfg_fp = StringIO.StringIO()
+ cfg.write(cfg_fp)
+ # deal with the possibility that zone and/or keypair are strings read from the config file:
+ if isinstance(zone, Zone):
+ zone = zone.name
+ if isinstance(kp, KeyPair):
+ kp = kp.name
+ reservation = ami.run(min_count=1,
+ max_count=params.get('quantity', 1),
+ key_name=kp,
+ security_groups=[group],
+ instance_type=params.get('instance_type'),
+ placement = zone,
+ user_data = cfg_fp.getvalue())
+ l = []
+ i = 0
+ elastic_ip = params.get('elastic_ip')
+ instances = reservation.instances
+ if elastic_ip != None and instances.__len__() > 0:
+ instance = instances[0]
+ while instance.update() != 'running':
+ time.sleep(1)
+ instance.use_ip(elastic_ip)
+ print 'set the elastic IP of the first instance to %s' % elastic_ip
+ for instance in instances:
+ s = cls()
+ s.ec2 = ec2
+ s.name = params.get('name') + '' if i==0 else str(i)
+ s.description = params.get('description')
+ s.region_name = region.name
+ s.instance_id = instance.id
+ if elastic_ip and i == 0:
+ s.elastic_ip = elastic_ip
+ s.put()
+ l.append(s)
+ i += 1
+ return l
+
+ @classmethod
+ def create_from_instance_id(cls, instance_id, name, description=''):
+ regions = boto.ec2.regions()
+ for region in regions:
+ ec2 = region.connect()
+ try:
+ rs = ec2.get_all_instances([instance_id])
+ except:
+ rs = []
+ if len(rs) == 1:
+ s = cls()
+ s.ec2 = ec2
+ s.name = name
+ s.description = description
+ s.region_name = region.name
+ s.instance_id = instance_id
+ s._reservation = rs[0]
+ for instance in s._reservation.instances:
+ if instance.id == instance_id:
+ s._instance = instance
+ s.put()
+ return s
+ return None
+
+ @classmethod
+ def create_from_current_instances(cls):
+ servers = []
+ regions = boto.ec2.regions()
+ for region in regions:
+ ec2 = region.connect()
+ rs = ec2.get_all_instances()
+ for reservation in rs:
+ for instance in reservation.instances:
+ try:
+ Server.find(instance_id=instance.id).next()
+ boto.log.info('Server for %s already exists' % instance.id)
+ except StopIteration:
+ s = cls()
+ s.ec2 = ec2
+ s.name = instance.id
+ s.region_name = region.name
+ s.instance_id = instance.id
+ s._reservation = reservation
+ s.put()
+ servers.append(s)
+ return servers
+
+ def __init__(self, id=None, **kw):
+ Model.__init__(self, id, **kw)
+ self.ssh_key_file = None
+ self.ec2 = None
+ self._cmdshell = None
+ self._reservation = None
+ self._instance = None
+ self._setup_ec2()
+
+ def _setup_ec2(self):
+ if self.ec2 and self._instance and self._reservation:
+ return
+ if self.id:
+ if self.region_name:
+ for region in boto.ec2.regions():
+ if region.name == self.region_name:
+ self.ec2 = region.connect()
+ if self.instance_id and not self._instance:
+ try:
+ rs = self.ec2.get_all_instances([self.instance_id])
+ if len(rs) >= 1:
+ for instance in rs[0].instances:
+ if instance.id == self.instance_id:
+ self._reservation = rs[0]
+ self._instance = instance
+ except EC2ResponseError:
+ pass
+
+ def _status(self):
+ status = ''
+ if self._instance:
+ self._instance.update()
+ status = self._instance.state
+ return status
+
+ def _hostname(self):
+ hostname = ''
+ if self._instance:
+ hostname = self._instance.public_dns_name
+ return hostname
+
+ def _private_hostname(self):
+ hostname = ''
+ if self._instance:
+ hostname = self._instance.private_dns_name
+ return hostname
+
+ def _instance_type(self):
+ it = ''
+ if self._instance:
+ it = self._instance.instance_type
+ return it
+
+ def _launch_time(self):
+ lt = ''
+ if self._instance:
+ lt = self._instance.launch_time
+ return lt
+
+ def _console_output(self):
+ co = ''
+ if self._instance:
+ co = self._instance.get_console_output()
+ return co
+
+ def _groups(self):
+ gn = []
+ if self._reservation:
+ gn = self._reservation.groups
+ return gn
+
+ def _security_group(self):
+ groups = self._groups()
+ if len(groups) >= 1:
+ return groups[0].id
+ return ""
+
+ def _zone(self):
+ zone = None
+ if self._instance:
+ zone = self._instance.placement
+ return zone
+
+ def _key_name(self):
+ kn = None
+ if self._instance:
+ kn = self._instance.key_name
+ return kn
+
+ def put(self):
+ Model.put(self)
+ self._setup_ec2()
+
+ def delete(self):
+ if self.production:
+ raise ValueError, "Can't delete a production server"
+ #self.stop()
+ Model.delete(self)
+
+ def stop(self):
+ if self.production:
+ raise ValueError, "Can't delete a production server"
+ if self._instance:
+ self._instance.stop()
+
+ def reboot(self):
+ if self._instance:
+ self._instance.reboot()
+
+ def wait(self):
+ while self.status != 'running':
+ time.sleep(5)
+
+ def get_ssh_key_file(self):
+ if not self.ssh_key_file:
+ ssh_dir = os.path.expanduser('~/.ssh')
+ if os.path.isdir(ssh_dir):
+ ssh_file = os.path.join(ssh_dir, '%s.pem' % self.key_name)
+ if os.path.isfile(ssh_file):
+ self.ssh_key_file = ssh_file
+ if not self.ssh_key_file:
+ iobject = IObject()
+ self.ssh_key_file = iobject.get_filename('Path to OpenSSH Key file')
+ return self.ssh_key_file
+
+ def get_cmdshell(self):
+ if not self._cmdshell:
+ import cmdshell
+ self.get_ssh_key_file()
+ self._cmdshell = cmdshell.start(self)
+ return self._cmdshell
+
+ def reset_cmdshell(self):
+ self._cmdshell = None
+
+ def run(self, command):
+ with closing(self.get_cmdshell()) as cmd:
+ status = cmd.run(command)
+ return status
+
+ def get_bundler(self, uname='root'):
+ ssh_key_file = self.get_ssh_key_file()
+ return Bundler(self, uname)
+
+ def get_ssh_client(self, uname='root'):
+ from boto.manage.cmdshell import SSHClient
+ ssh_key_file = self.get_ssh_key_file()
+ return SSHClient(self, uname=uname)
+
+ def install(self, pkg):
+ return self.run('apt-get -y install %s' % pkg)
+
+
+
diff --git a/api/boto/manage/task.py b/api/boto/manage/task.py
new file mode 100644
index 0000000..5fb234d
--- /dev/null
+++ b/api/boto/manage/task.py
@@ -0,0 +1,173 @@
+# 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
+from boto.sdb.db.property import *
+from boto.sdb.db.model import Model
+import datetime, subprocess, StringIO, time
+
+def check_hour(val):
+ if val == '*':
+ return
+ if int(val) < 0 or int(val) > 23:
+ raise ValueError
+
+class Task(Model):
+
+ """
+ A scheduled, repeating task that can be executed by any participating servers.
+ The scheduling is similar to cron jobs. Each task has an hour attribute.
+ The allowable values for hour are [0-23|*].
+
+ To keep the operation reasonably efficient and not cause excessive polling,
+ the minimum granularity of a Task is hourly. Some examples:
+
+ hour='*' - the task would be executed each hour
+ hour='3' - the task would be executed at 3AM GMT each day.
+
+ """
+ name = StringProperty()
+ hour = StringProperty(required=True, validator=check_hour, default='*')
+ command = StringProperty(required=True)
+ last_executed = DateTimeProperty()
+ last_status = IntegerProperty()
+ last_output = StringProperty()
+ message_id = StringProperty()
+
+ @classmethod
+ def start_all(cls, queue_name):
+ for task in cls.all():
+ task.start(queue_name)
+
+ def __init__(self, id=None, **kw):
+ Model.__init__(self, id, **kw)
+ self.hourly = self.hour == '*'
+ self.daily = self.hour != '*'
+ self.now = datetime.datetime.utcnow()
+
+ def check(self):
+ """
+ Determine how long until the next scheduled time for a Task.
+ Returns the number of seconds until the next scheduled time or zero
+ if the task needs to be run immediately.
+ If it's an hourly task and it's never been run, run it now.
+ If it's a daily task and it's never been run and the hour is right, run it now.
+ """
+ need_to_run = False
+ boto.log.info('checking Task[%s]-now=%s, last=%s' % (self.name, self.now, self.last_executed))
+
+ if self.hourly and not self.last_executed:
+ return 0
+
+ if self.daily and not self.last_executed:
+ if int(self.hour) == self.now.hour:
+ return 0
+ else:
+ return max((int(self.hour) - self.now.hour),0)*60*60
+
+ delta = self.now - self.last_executed
+ if self.hourly:
+ if delta.seconds >= 60*60:
+ return 0
+ else:
+ return 60*60 - delta.seconds
+ else:
+ if delta.days >= 1:
+ return 0
+ else:
+ return min(60*60*24-delta.seconds, 43200)
+
+ def _run(self, msg, vtimeout):
+ boto.log.info('Task[%s] - running:%s' % (self.name, self.command))
+ log_fp = StringIO.StringIO()
+ process = subprocess.Popen(self.command, shell=True, stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ nsecs = 5
+ current_timeout = vtimeout
+ while process.poll() == None:
+ boto.log.info('nsecs=%s, timeout=%s' % (nsecs, current_timeout))
+ if nsecs >= current_timeout:
+ current_timeout += vtimeout
+ boto.log.info('Task[%s] - setting timeout to %d seconds' % (self.name, current_timeout))
+ if msg:
+ msg.change_visibility(current_timeout)
+ time.sleep(5)
+ nsecs += 5
+ t = process.communicate()
+ log_fp.write(t[0])
+ log_fp.write(t[1])
+ boto.log.info('Task[%s] - output: %s' % (self.name, log_fp.getvalue()))
+ self.last_executed = self.now
+ self.last_status = process.returncode
+ self.last_output = log_fp.getvalue()[0:1023]
+
+ def run(self, msg, vtimeout=60):
+ delay = self.check()
+ boto.log.info('Task[%s] - delay=%s seconds' % (self.name, delay))
+ if delay == 0:
+ self._run(msg, vtimeout)
+ queue = msg.queue
+ new_msg = queue.new_message(self.id)
+ new_msg = queue.write(msg)
+ self.message_id = new_msg.id
+ self.put()
+ boto.log.info('Task[%s] - new message id=%s' % (self.name, new_msg.id))
+ msg.delete()
+ boto.log.info('Task[%s] - deleted message %s' % (self.name, msg.id))
+ else:
+ boto.log.info('new_vtimeout: %d' % delay)
+ msg.change_visibility(delay)
+
+ def start(self, queue_name):
+ boto.log.info('Task[%s] - starting with queue: %s' % (self.name, queue_name))
+ queue = boto.lookup('sqs', queue_name)
+ msg = queue.new_message(self.id)
+ msg = queue.write(msg)
+ self.message_id = msg.id
+ self.put()
+ boto.log.info('Task[%s] - start successful' % self.name)
+
+class TaskPoller(object):
+
+ def __init__(self, queue_name):
+ self.sqs = boto.connect_sqs()
+ self.queue = self.sqs.lookup(queue_name)
+
+ def poll(self, wait=60, vtimeout=60):
+ while 1:
+ m = self.queue.read(vtimeout)
+ if m:
+ task = Task.get_by_id(m.get_body())
+ if task:
+ if not task.message_id or m.id == task.message_id:
+ boto.log.info('Task[%s] - read message %s' % (task.name, m.id))
+ task.run(m, vtimeout)
+ else:
+ boto.log.info('Task[%s] - found extraneous message, ignoring' % task.name)
+ else:
+ time.sleep(wait)
+
+
+
+
+
+
diff --git a/api/boto/manage/test_manage.py b/api/boto/manage/test_manage.py
new file mode 100644
index 0000000..e0b032a
--- /dev/null
+++ b/api/boto/manage/test_manage.py
@@ -0,0 +1,34 @@
+from boto.manage.server import Server
+from boto.manage.volume import Volume
+import time
+
+print '--> Creating New Volume'
+volume = Volume.create()
+print volume
+
+print '--> Creating New Server'
+server_list = Server.create()
+server = server_list[0]
+print server
+
+print '----> Waiting for Server to start up'
+while server.status != 'running':
+ print '*'
+ time.sleep(10)
+print '----> Server is running'
+
+print '--> Run "df -k" on Server'
+status = server.run('df -k')
+print status[1]
+
+print '--> Now run volume.make_ready to make the volume ready to use on server'
+volume.make_ready(server)
+
+print '--> Run "df -k" on Server'
+status = server.run('df -k')
+print status[1]
+
+print '--> Do an "ls -al" on the new filesystem'
+status = server.run('ls -al %s' % volume.mount_point)
+print status[1]
+
diff --git a/api/boto/manage/volume.py b/api/boto/manage/volume.py
new file mode 100644
index 0000000..bed5594
--- /dev/null
+++ b/api/boto/manage/volume.py
@@ -0,0 +1,417 @@
+# 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 __future__ import with_statement
+from boto.sdb.db.model import Model
+from boto.sdb.db.property import *
+from boto.manage.server import Server
+from boto.manage import propget
+import boto.ec2
+import time, traceback
+from contextlib import closing
+import dateutil.parser
+
+class CommandLineGetter(object):
+
+ def get_region(self, params):
+ if not params.get('region', None):
+ prop = self.cls.find_property('region_name')
+ params['region'] = propget.get(prop, choices=boto.ec2.regions)
+
+ 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_name(self, params):
+ if not params.get('name', None):
+ prop = self.cls.find_property('name')
+ params['name'] = propget.get(prop)
+
+ def get_size(self, params):
+ if not params.get('size', None):
+ prop = IntegerProperty(name='size', verbose_name='Size (GB)')
+ params['size'] = propget.get(prop)
+
+ def get_mount_point(self, params):
+ if not params.get('mount_point', None):
+ prop = self.cls.find_property('mount_point')
+ params['mount_point'] = propget.get(prop)
+
+ def get_device(self, params):
+ if not params.get('device', None):
+ prop = self.cls.find_property('device')
+ params['device'] = propget.get(prop)
+
+ def get(self, cls, params):
+ self.cls = cls
+ self.get_region(params)
+ self.ec2 = params['region'].connect()
+ self.get_zone(params)
+ self.get_name(params)
+ self.get_size(params)
+ self.get_mount_point(params)
+ self.get_device(params)
+
+class Volume(Model):
+
+ name = StringProperty(required=True, unique=True, verbose_name='Name')
+ region_name = StringProperty(required=True, verbose_name='EC2 Region')
+ zone_name = StringProperty(required=True, verbose_name='EC2 Zone')
+ mount_point = StringProperty(verbose_name='Mount Point')
+ device = StringProperty(verbose_name="Device Name", default='/dev/sdp')
+ volume_id = StringProperty(required=True)
+ past_volume_ids = ListProperty(item_type=str)
+ server = ReferenceProperty(Server, collection_name='volumes',
+ verbose_name='Server Attached To')
+ volume_state = CalculatedProperty(verbose_name="Volume State",
+ calculated_type=str, use_method=True)
+ attachment_state = CalculatedProperty(verbose_name="Attachment State",
+ calculated_type=str, use_method=True)
+ size = CalculatedProperty(verbose_name="Size (GB)",
+ calculated_type=int, use_method=True)
+
+ @classmethod
+ def create(cls, **params):
+ getter = CommandLineGetter()
+ getter.get(cls, params)
+ region = params.get('region')
+ ec2 = region.connect()
+ zone = params.get('zone')
+ size = params.get('size')
+ ebs_volume = ec2.create_volume(size, zone.name)
+ v = cls()
+ v.ec2 = ec2
+ v.volume_id = ebs_volume.id
+ v.name = params.get('name')
+ v.mount_point = params.get('mount_point')
+ v.device = params.get('device')
+ v.region_name = region.name
+ v.zone_name = zone.name
+ v.put()
+ return v
+
+ @classmethod
+ def create_from_volume_id(cls, region_name, volume_id, name):
+ vol = None
+ ec2 = boto.ec2.connect_to_region(region_name)
+ rs = ec2.get_all_volumes([volume_id])
+ if len(rs) == 1:
+ v = rs[0]
+ vol = cls()
+ vol.volume_id = v.id
+ vol.name = name
+ vol.region_name = v.region.name
+ vol.zone_name = v.zone
+ vol.put()
+ return vol
+
+ def create_from_latest_snapshot(self, name, size=None):
+ snapshot = self.get_snapshots()[-1]
+ return self.create_from_snapshot(name, snapshot, size)
+
+ def create_from_snapshot(self, name, snapshot, size=None):
+ if size < self.size:
+ size = self.size
+ ec2 = self.get_ec2_connection()
+ if self.zone_name == None or self.zone_name == '':
+ # deal with the migration case where the zone is not set in the logical volume:
+ current_volume = ec2.get_all_volumes([self.volume_id])[0]
+ self.zone_name = current_volume.zone
+ ebs_volume = ec2.create_volume(size, self.zone_name, snapshot)
+ v = Volume()
+ v.ec2 = self.ec2
+ v.volume_id = ebs_volume.id
+ v.name = name
+ v.mount_point = self.mount_point
+ v.device = self.device
+ v.region_name = self.region_name
+ v.zone_name = self.zone_name
+ v.put()
+ return v
+
+ def get_ec2_connection(self):
+ if self.server:
+ return self.server.ec2
+ if not hasattr(self, 'ec2') or self.ec2 == None:
+ self.ec2 = boto.ec2.connect_to_region(self.region_name)
+ return self.ec2
+
+ def _volume_state(self):
+ ec2 = self.get_ec2_connection()
+ rs = ec2.get_all_volumes([self.volume_id])
+ return rs[0].volume_state()
+
+ def _attachment_state(self):
+ ec2 = self.get_ec2_connection()
+ rs = ec2.get_all_volumes([self.volume_id])
+ return rs[0].attachment_state()
+
+ def _size(self):
+ if not hasattr(self, '__size'):
+ ec2 = self.get_ec2_connection()
+ rs = ec2.get_all_volumes([self.volume_id])
+ self.__size = rs[0].size
+ return self.__size
+
+ def install_xfs(self):
+ if self.server:
+ self.server.install('xfsprogs xfsdump')
+
+ def get_snapshots(self):
+ """
+ Returns a list of all completed snapshots for this volume ID.
+ """
+ ec2 = self.get_ec2_connection()
+ rs = ec2.get_all_snapshots()
+ all_vols = [self.volume_id] + self.past_volume_ids
+ snaps = []
+ for snapshot in rs:
+ if snapshot.volume_id in all_vols:
+ if snapshot.progress == '100%':
+ snapshot.date = dateutil.parser.parse(snapshot.start_time)
+ snapshot.keep = True
+ snaps.append(snapshot)
+ snaps.sort(cmp=lambda x,y: cmp(x.date, y.date))
+ return snaps
+
+ def attach(self, server=None):
+ if self.attachment_state == 'attached':
+ print 'already attached'
+ return None
+ if server:
+ self.server = server
+ self.put()
+ ec2 = self.get_ec2_connection()
+ ec2.attach_volume(self.volume_id, self.server.instance_id, self.device)
+
+ def detach(self, force=False):
+ state = self.attachment_state
+ if state == 'available' or state == None or state == 'detaching':
+ print 'already detached'
+ return None
+ ec2 = self.get_ec2_connection()
+ ec2.detach_volume(self.volume_id, self.server.instance_id, self.device, force)
+ self.server = None
+ self.put()
+
+ def checkfs(self, use_cmd=None):
+ if self.server == None:
+ raise ValueError, 'server attribute must be set to run this command'
+ # detemine state of file system on volume, only works if attached
+ if use_cmd:
+ cmd = use_cmd
+ else:
+ cmd = self.server.get_cmdshell()
+ status = cmd.run('xfs_check %s' % self.device)
+ if not use_cmd:
+ cmd.close()
+ if status[1].startswith('bad superblock magic number 0'):
+ return False
+ return True
+
+ def wait(self):
+ if self.server == None:
+ raise ValueError, 'server attribute must be set to run this command'
+ with closing(self.server.get_cmdshell()) as cmd:
+ # wait for the volume device to appear
+ cmd = self.server.get_cmdshell()
+ while not cmd.exists(self.device):
+ boto.log.info('%s still does not exist, waiting 10 seconds' % self.device)
+ time.sleep(10)
+
+ def format(self):
+ if self.server == None:
+ raise ValueError, 'server attribute must be set to run this command'
+ status = None
+ with closing(self.server.get_cmdshell()) as cmd:
+ if not self.checkfs(cmd):
+ boto.log.info('make_fs...')
+ status = cmd.run('mkfs -t xfs %s' % self.device)
+ return status
+
+ def mount(self):
+ if self.server == None:
+ raise ValueError, 'server attribute must be set to run this command'
+ boto.log.info('handle_mount_point')
+ with closing(self.server.get_cmdshell()) as cmd:
+ cmd = self.server.get_cmdshell()
+ if not cmd.isdir(self.mount_point):
+ boto.log.info('making directory')
+ # mount directory doesn't exist so create it
+ cmd.run("mkdir %s" % self.mount_point)
+ else:
+ boto.log.info('directory exists already')
+ status = cmd.run('mount -l')
+ lines = status[1].split('\n')
+ for line in lines:
+ t = line.split()
+ if t and t[2] == self.mount_point:
+ # something is already mounted at the mount point
+ # unmount that and mount it as /tmp
+ if t[0] != self.device:
+ cmd.run('umount %s' % self.mount_point)
+ cmd.run('mount %s /tmp' % t[0])
+ cmd.run('chmod 777 /tmp')
+ break
+ # Mount up our new EBS volume onto mount_point
+ cmd.run("mount %s %s" % (self.device, self.mount_point))
+ cmd.run('xfs_growfs %s' % self.mount_point)
+
+ def make_ready(self, server):
+ self.server = server
+ self.put()
+ self.install_xfs()
+ self.attach()
+ self.wait()
+ self.format()
+ self.mount()
+
+ def freeze(self):
+ if self.server:
+ return self.server.run("/usr/sbin/xfs_freeze -f %s" % self.mount_point)
+
+ def unfreeze(self):
+ if self.server:
+ return self.server.run("/usr/sbin/xfs_freeze -u %s" % self.mount_point)
+
+ def snapshot(self):
+ # if this volume is attached to a server
+ # we need to freeze the XFS file system
+ try:
+ self.freeze()
+ if self.server == None:
+ snapshot = self.get_ec2_connection().create_snapshot(self.volume_id)
+ else:
+ snapshot = self.server.ec2.create_snapshot(self.volume_id)
+ boto.log.info('Snapshot of Volume %s created: %s' % (self.name, snapshot))
+ except Exception, e:
+ boto.log.info('Snapshot error')
+ boto.log.info(traceback.format_exc())
+ finally:
+ status = self.unfreeze()
+ return status
+
+ def get_snapshot_range(self, snaps, start_date=None, end_date=None):
+ l = []
+ for snap in snaps:
+ if start_date and end_date:
+ if snap.date >= start_date and snap.date <= end_date:
+ l.append(snap)
+ elif start_date:
+ if snap.date >= start_date:
+ l.append(snap)
+ elif end_date:
+ if snap.date <= end_date:
+ l.append(snap)
+ else:
+ l.append(snap)
+ return l
+
+ def trim_snapshots(self, delete=False):
+ """
+ Trim the number of snapshots for this volume. This method always
+ keeps the oldest snapshot. It then uses the parameters passed in
+ to determine how many others should be kept.
+
+ The algorithm is to keep all snapshots from the current day. Then
+ it will keep the first snapshot of the day for the previous seven days.
+ Then, it will keep the first snapshot of the week for the previous
+ four weeks. After than, it will keep the first snapshot of the month
+ for as many months as there are.
+
+ """
+ snaps = self.get_snapshots()
+ # Always keep the oldest and the newest
+ if len(snaps) <= 2:
+ return snaps
+ snaps = snaps[1:-1]
+ now = datetime.datetime.now(snaps[0].date.tzinfo)
+ midnight = datetime.datetime(year=now.year, month=now.month,
+ day=now.day, tzinfo=now.tzinfo)
+ # Keep the first snapshot from each day of the previous week
+ one_week = datetime.timedelta(days=7, seconds=60*60)
+ print midnight-one_week, midnight
+ previous_week = self.get_snapshot_range(snaps, midnight-one_week, midnight)
+ print previous_week
+ if not previous_week:
+ return snaps
+ current_day = None
+ for snap in previous_week:
+ if current_day and current_day == snap.date.day:
+ snap.keep = False
+ else:
+ current_day = snap.date.day
+ # Get ourselves onto the next full week boundary
+ if previous_week:
+ week_boundary = previous_week[0].date
+ if week_boundary.weekday() != 0:
+ delta = datetime.timedelta(days=week_boundary.weekday())
+ week_boundary = week_boundary - delta
+ # Keep one within this partial week
+ partial_week = self.get_snapshot_range(snaps, week_boundary, previous_week[0].date)
+ if len(partial_week) > 1:
+ for snap in partial_week[1:]:
+ snap.keep = False
+ # Keep the first snapshot of each week for the previous 4 weeks
+ for i in range(0,4):
+ weeks_worth = self.get_snapshot_range(snaps, week_boundary-one_week, week_boundary)
+ if len(weeks_worth) > 1:
+ for snap in weeks_worth[1:]:
+ snap.keep = False
+ week_boundary = week_boundary - one_week
+ # Now look through all remaining snaps and keep one per month
+ remainder = self.get_snapshot_range(snaps, end_date=week_boundary)
+ current_month = None
+ for snap in remainder:
+ if current_month and current_month == snap.date.month:
+ snap.keep = False
+ else:
+ current_month = snap.date.month
+ if delete:
+ for snap in snaps:
+ if not snap.keep:
+ boto.log.info('Deleting %s(%s) for %s' % (snap, snap.date, self.name))
+ snap.delete()
+ return snaps
+
+ def grow(self, size):
+ pass
+
+ def copy(self, snapshot):
+ pass
+
+ def get_snapshot_from_date(self, date):
+ pass
+
+ def delete(self, delete_ebs_volume=False):
+ if delete_ebs_volume:
+ self.detach()
+ ec2 = self.get_ec2_connection()
+ ec2.delete_volume(self.volume_id)
+ Model.delete(self)
+
+ def archive(self):
+ # snapshot volume, trim snaps, delete volume-id
+ pass
+
+
diff --git a/api/boto/mapreduce/__init__.py b/api/boto/mapreduce/__init__.py
new file mode 100644
index 0000000..ac3ddc4
--- /dev/null
+++ b/api/boto/mapreduce/__init__.py
@@ -0,0 +1,23 @@
+# 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.
+#
+
+
diff --git a/api/boto/mapreduce/lqs.py b/api/boto/mapreduce/lqs.py
new file mode 100644
index 0000000..fc76e50
--- /dev/null
+++ b/api/boto/mapreduce/lqs.py
@@ -0,0 +1,152 @@
+import SocketServer, os, datetime, sys, random, time
+import simplejson
+
+class LQSCommand:
+
+ def __init__(self, line):
+ self.raw_line = line
+ self.line = self.raw_line.strip()
+ l = self.line.split(' ')
+ self.name = l[0]
+ if len(l) > 1:
+ self.args = [arg for arg in l[1:] if arg]
+ else:
+ self.args = []
+
+class LQSMessage(dict):
+
+ def __init__(self, item=None, args=None, jsonvalue=None):
+ dict.__init__(self)
+ if jsonvalue:
+ self.decode(jsonvalue)
+ else:
+ self['id'] = '%d_%d' % (int(time.time()), int(random.random()*1000000))
+ self['item'] = item
+ self['args'] = args
+
+ def encode(self):
+ return simplejson.dumps(self)
+
+ def decode(self, value):
+ self.update(simplejson.loads(value))
+
+ def is_empty(self):
+ if self['item'] == None:
+ return True
+ return False
+
+class LQSServer(SocketServer.UDPServer):
+
+ PORT = 5151
+ TIMEOUT = 30
+ MAXSIZE = 8192
+
+ def __init__(self, server_address, RequestHandlerClass, iterator, args=None):
+ server_address = (server_address, self.PORT)
+ SocketServer.UDPServer.__init__(self, server_address, RequestHandlerClass)
+ self.count = 0
+ self.iterator = iterator
+ self.args = args
+ self.start = datetime.datetime.now()
+ self.end = None
+ self.extant = []
+
+class LQSHandler(SocketServer.DatagramRequestHandler):
+
+ def get_cmd(self):
+ return LQSCommand(self.rfile.readline())
+
+ def build_msg(self):
+ if not self.server.iterator:
+ return LQSMessage(None)
+ try:
+ item = self.server.iterator.next()
+ msg = LQSMessage(item, self.server.args)
+ return msg
+ except StopIteration:
+ self.server.iterator = None
+ return LQSMessage(None)
+
+ def respond(self, msg):
+ self.wfile.write(msg.encode())
+
+ def check_extant(self):
+ if len(self.server.extant) == 0 and not self.server.iterator:
+ self.server.end = datetime.datetime.now()
+ delta = self.server.end - self.server.start
+ print 'Total Processing Time: %s' % delta
+ print 'Total Messages Processed: %d' % self.server.count
+
+ def do_debug(self, cmd):
+ args = {'extant' : self.server.extant,
+ 'count' : self.server.count}
+ msg = LQSMessage('debug', args)
+ self.respond(msg)
+
+ def do_next(self, cmd):
+ out_msg = self.build_msg()
+ if not out_msg.is_empty():
+ self.server.count += 1
+ self.server.extant.append(out_msg['id'])
+ self.respond(out_msg)
+
+ def do_delete(self, cmd):
+ if len(cmd.args) != 1:
+ self.error(cmd, 'delete command requires message id')
+ else:
+ mid = cmd.args[0]
+ try:
+ self.server.extant.remove(mid)
+ except ValueError:
+ self.error(cmd, 'message id not found')
+ args = {'deleted' : True}
+ msg = LQSMessage(mid, args)
+ self.respond(msg)
+ self.check_extant()
+
+ def error(self, cmd, error_msg=None):
+ args = {'error_msg' : error_msg,
+ 'cmd_name' : cmd.name,
+ 'cmd_args' : cmd.args}
+ msg = LQSMessage('error', args)
+ self.respond(msg)
+
+ def do_stop(self, cmd):
+ sys.exit(0)
+
+ def handle(self):
+ cmd = self.get_cmd()
+ if hasattr(self, 'do_%s' % cmd.name):
+ method = getattr(self, 'do_%s' % cmd.name)
+ method(cmd)
+ else:
+ self.error(cmd, 'unrecognized command')
+
+class PersistHandler(LQSHandler):
+
+ def build_msg(self):
+ if not self.server.iterator:
+ return LQSMessage(None)
+ try:
+ obj = self.server.iterator.next()
+ msg = LQSMessage(obj.id, self.server.args)
+ return msg
+ except StopIteration:
+ self.server.iterator = None
+ return LQSMessage(None)
+
+def test_file(path, args=None):
+ l = os.listdir(path)
+ if not args:
+ args = {}
+ args['path'] = path
+ s = LQSServer('', LQSHandler, iter(l), args)
+ print "Awaiting UDP messages on port %d" % s.PORT
+ s.serve_forever()
+
+def test_simple(n):
+ l = range(0, n)
+ s = LQSServer('', LQSHandler, iter(l), None)
+ print "Awaiting UDP messages on port %d" % s.PORT
+ s.serve_forever()
+
diff --git a/api/boto/mapreduce/partitiondb.py b/api/boto/mapreduce/partitiondb.py
new file mode 100644
index 0000000..c5b0475
--- /dev/null
+++ b/api/boto/mapreduce/partitiondb.py
@@ -0,0 +1,172 @@
+# 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.
+#
+
+import random, time, os, datetime
+import boto
+from boto.sdb.persist.object import SDBObject
+from boto.sdb.persist.property import *
+
+class Identifier(object):
+
+ _hex_digits = '0123456789abcdef'
+
+ @classmethod
+ def gen(cls, prefix):
+ suffix = ''
+ for i in range(0,8):
+ suffix += random.choice(cls._hex_digits)
+ return ts + '-' + suffix
+
+class Version(SDBObject):
+
+ name = StringProperty()
+ pdb = ObjectProperty(ref_class=SDBObject)
+ date = DateTimeProperty()
+
+ def __init__(self, id=None, manager=None):
+ SDBObject.__init__(self, id, manager)
+ if id == None:
+ self.name = Identifier.gen('v')
+ self.date = datetime.datetime.now()
+ print 'created Version %s' % self.name
+
+ def partitions(self):
+ """
+ Return an iterator containing all Partition objects related to this Version.
+
+ :rtype: iterator of :class:`boto.mapreduce.partitiondb.Partition`
+ :return: The Partitions in this Version
+ """
+ return self.get_related_objects('version', Partition)
+
+ def add_partition(self, name=None):
+ """
+ Add a new Partition to this Version.
+
+ :type name: string
+ :param name: The name of the new Partition (optional)
+
+ :rtype: :class:`boto.mapreduce.partitiondb.Partition`
+ :return: The new Partition object
+ """
+ p = Partition(manager=self.manager, name=name)
+ p.version = self
+ p.pdb = self.pdb
+ p.save()
+ return p
+
+ def get_s3_prefix(self):
+ if not self.pdb:
+ raise ValueError, 'pdb attribute must be set to compute S3 prefix'
+ return self.pdb.get_s3_prefix() + self.name + '/'
+
+class PartitionDB(SDBObject):
+
+ name = StringProperty()
+ bucket_name = StringProperty()
+ versions = ObjectListProperty(ref_class=Version)
+
+ def __init__(self, id=None, manager=None, name='', bucket_name=''):
+ SDBObject.__init__(self, id, manager)
+ if id == None:
+ self.name = name
+ self.bucket_name = bucket_name
+
+ def get_s3_prefix(self):
+ return self.name + '/'
+
+ def add_version(self):
+ """
+ Add a new Version to this PartitionDB. The newly added version becomes the
+ current version.
+
+ :rtype: :class:`boto.mapreduce.partitiondb.Version`
+ :return: The newly created Version object.
+ """
+ v = Version()
+ v.pdb = self
+ v.save()
+ self.versions.append(v)
+ return v
+
+ def revert(self):
+ """
+ Revert to the previous version of this PartitionDB. The current version is removed from the
+ list of Versions and the Version immediately preceeding it becomes the current version.
+ Note that this method does not delete the Version object or any Partitions related to the
+ Version object.
+
+ :rtype: :class:`boto.mapreduce.partitiondb.Version`
+ :return: The previous current Version object.
+ """
+ v = self.current_version()
+ if v:
+ self.versions.remove(v)
+ return v
+
+ def current_version(self):
+ """
+ Get the currently active Version of this PartitionDB object.
+
+ :rtype: :class:`boto.mapreduce.partitiondb.Version`
+ :return: The current Version object or None if there are no Versions associated
+ with this PartitionDB object.
+ """
+ if self.versions:
+ if len(self.versions) > 0:
+ return self.versions[-1]
+ return None
+
+class Partition(SDBObject):
+
+ def __init__(self, id=None, manager=None, name=None):
+ SDBObject.__init__(self, id, manager)
+ if id == None:
+ self.name = name
+
+ name = StringProperty()
+ version = ObjectProperty(ref_class=Version)
+ pdb = ObjectProperty(ref_class=PartitionDB)
+ data = S3KeyProperty()
+
+ def get_key_name(self):
+ return self.version.get_s3_prefix() + self.name
+
+ def upload(self, path, bucket_name=None):
+ if not bucket_name:
+ bucket_name = self.version.pdb.bucket_name
+ s3 = self.manager.get_s3_connection()
+ bucket = s3.lookup(bucket_name)
+ directory, filename = os.path.split(path)
+ self.name = filename
+ key = bucket.new_key(self.get_key_name())
+ key.set_contents_from_filename(path)
+ self.data = key
+ self.save()
+
+ def delete(self):
+ if self.data:
+ self.data.delete()
+ SDBObject.delete(self)
+
+
+
diff --git a/api/boto/mapreduce/pdb_delete b/api/boto/mapreduce/pdb_delete
new file mode 100644
index 0000000..b7af9cc
--- /dev/null
+++ b/api/boto/mapreduce/pdb_delete
@@ -0,0 +1,135 @@
+#!/usr/bin/env python
+# 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.
+import queuetools, os, signal, sys
+import subprocess
+import time
+from optparse import OptionParser
+from boto.mapreduce.partitiondb import PartitionDB, Partition, Version
+from lqs import LQSServer, PersistHandler
+from boto.exception import SDBPersistenceError
+from boto.sdb.persist import get_manager, revive_object_from_id
+
+USAGE = """
+ SYNOPSIS
+ %prog [options] [command]
+ DESCRIPTION
+ Delete a PartitionDB and all related data in SimpleDB and S3.
+"""
+class Client:
+
+ def __init__(self, queue_name):
+ self.q = queuetools.get_queue(queue_name)
+ self.q.connect()
+ self.manager = get_manager()
+ self.process()
+
+ def process(self):
+ m = self.q.get()
+ while m['item']:
+ print 'Deleting: %s' % m['item']
+ obj = revive_object_from_id(m['item'], manager=self.manager)
+ obj.delete()
+ self.q.delete(m)
+ m = self.q.get()
+ print 'client processing complete'
+
+class Server:
+
+ def __init__(self, pdb_name, domain_name=None):
+ self.pdb_name = pdb_name
+ self.manager = get_manager(domain_name)
+ self.pdb = PartitionDB.get(name=self.pdb_name)
+ self.serve()
+
+ def serve(self):
+ args = {'pdb_id' : self.pdb.id}
+ rs = self.pdb.get_related_objects('pdb')
+ self.pdb.delete()
+ s = LQSServer('', PersistHandler, rs, args)
+ s.serve_forever()
+
+class Delete:
+
+ Commands = {'client' : 'Start a Delete client',
+ 'server' : 'Start a Delete server'}
+
+ def __init__(self):
+ self.parser = OptionParser(usage=USAGE)
+ self.parser.add_option("--help-commands", action="store_true", dest="help_commands",
+ help="provides help on the available commands")
+ self.parser.add_option('-d', '--domain-name', action='store', type='string',
+ help='name of the SimpleDB domain where PDB objects are stored')
+ self.parser.add_option('-n', '--num-processes', action='store', type='int', dest='num_processes',
+ help='the number of client processes launched')
+ self.parser.set_defaults(num_processes=5)
+ self.parser.add_option('-p', '--pdb-name', action='store', type='string',
+ help='name of the PDB in which to store files (will create if necessary)')
+ self.options, self.args = self.parser.parse_args()
+ self.prog_name = sys.argv[0]
+
+ def print_command_help(self):
+ print '\nCommands:'
+ for key in self.Commands.keys():
+ print ' %s\t\t%s' % (key, self.Commands[key])
+
+ def do_server(self):
+ if not self.options.pdb_name:
+ self.parser.error('No PDB name provided')
+ s = Server(self.options.pdb_name, self.options.domain_name)
+
+ def do_client(self):
+ c = Client('localhost')
+
+ def main(self):
+ if self.options.help_commands:
+ self.print_command_help()
+ sys.exit(0)
+ if len(self.args) == 0:
+ if not self.options.pdb_name:
+ self.parser.error('No PDB name provided')
+ server_command = '%s -p %s ' % (self.prog_name, self.options.pdb_name)
+ server_command += ' server'
+ client_command = '%s client' % self.prog_name
+ server = subprocess.Popen(server_command, shell=True)
+ print 'server pid: %s' % server.pid
+ time.sleep(5)
+ clients = []
+ for i in range(0, self.options.num_processes):
+ client = subprocess.Popen(client_command, shell=True)
+ clients.append(client)
+ print 'waiting for clients to finish'
+ for client in clients:
+ client.wait()
+ os.kill(server.pid, signal.SIGTERM)
+ elif len(self.args) == 1:
+ self.command = self.args[0]
+ if hasattr(self, 'do_%s' % self.command):
+ method = getattr(self, 'do_%s' % self.command)
+ method()
+ else:
+ self.parser.error('command (%s) not recognized' % self.command)
+ else:
+ self.parser.error('unrecognized commands')
+
+if __name__ == "__main__":
+ delete = Delete()
+ delete.main()
diff --git a/api/boto/mapreduce/pdb_describe b/api/boto/mapreduce/pdb_describe
new file mode 100755
index 0000000..d0fa86c
--- /dev/null
+++ b/api/boto/mapreduce/pdb_describe
@@ -0,0 +1,124 @@
+#!/usr/bin/env python
+# 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.
+import sys
+from optparse import OptionParser
+from boto.mapreduce.partitiondb import PartitionDB, Partition, Version
+from boto.exception import SDBPersistenceError
+from boto.sdb.persist import get_manager, get_domain
+
+USAGE = """
+ SYNOPSIS
+ %prog [options]
+ DESCRIPTION
+ List and describe your PartitionDBs.
+ Called with no options, all PartitionDB objects defined in your default
+ domain (as specified in the "default_domain" option in the "[Persist]"
+ section of your boto config file) will be listed.
+ When called with a particular PartitionDB name (using -p option) all
+ Version objects of that PartitionDB object will be listed.
+ When called with the -p option and a particular Version name specified
+ (using the -v option) all Partitions in that Version object will be listed.
+"""
+class Describe:
+
+ def __init__(self):
+ self.parser = OptionParser(usage=USAGE)
+ self.parser.add_option('-d', '--domain-name', action='store', type='string',
+ help='name of the SimpleDB domain where PDB objects are stored')
+ self.parser.add_option('-n', '--num-entries', action='store', type='int',
+ help='maximum number of entries to print (default 100)')
+ self.parser.set_defaults(num_entries=100)
+ self.parser.add_option('-p', '--pdb-name', action='store', type='string',
+ help='name of the PDB to describe')
+ self.parser.add_option('-v', '--version-name', action='store', type='string',
+ help='name of the PDB Version to describe')
+ self.options, self.args = self.parser.parse_args()
+ self.prog_name = sys.argv[0]
+
+ def describe_all(self):
+ print 'Using SimpleDB Domain: %s' % get_domain()
+ print 'PDBs:'
+ rs = PartitionDB.list()
+ i = 0
+ for pdb in rs:
+ print '%s\t%s\t%s' % (pdb.id, pdb.name, pdb.bucket_name)
+ i += 1
+ if i == self.options.num_entries:
+ break
+
+ def describe_pdb(self, pdb_name):
+ print 'Using SimpleDB Domain: %s' % get_domain()
+ print 'PDB: %s' % pdb_name
+ print 'Versions:'
+ try:
+ pdb = PartitionDB.get(name=pdb_name)
+ i = 0
+ for v in pdb.versions:
+ if v.date:
+ ds = v.date.isoformat()
+ else:
+ ds = 'unknown'
+ print '%s\t%s\t%s' % (v.id, v.name, ds)
+ i += 1
+ if i == self.options.num_entries:
+ break
+ cv = pdb.current_version()
+ if cv:
+ print 'Current Version: %s' % cv.name
+ else:
+ print 'Current Version: None'
+ except SDBPersistenceError:
+ self.parser.error('pdb_name (%s) unknown' % pdb_name)
+
+ def describe_version(self, pdb_name, version_name):
+ print 'Using SimpleDB Domain: %s' % get_domain()
+ print 'PDB: %s' % pdb_name
+ print 'Version: %s' % version_name
+ print 'Partitions:'
+ try:
+ pdb = PartitionDB.get(name=pdb_name)
+ for v in pdb.versions:
+ if v.name == version_name:
+ i = 0
+ for p in v.partitions():
+ print '%s\t%s' % (p.id, p.name)
+ i += 1
+ if i == self.options.num_entries:
+ break
+ except SDBPersistenceError:
+ self.parser.error('pdb_name (%s) unknown' % pdb_name)
+
+ def main(self):
+ self.options, self.args = self.parser.parse_args()
+ self.manager = get_manager(self.options.domain_name)
+
+ if self.options.pdb_name:
+ if self.options.version_name:
+ self.describe_version(self.options.pdb_name, self.options.version_name)
+ else:
+ self.describe_pdb(self.options.pdb_name)
+ else:
+ self.describe_all()
+
+if __name__ == "__main__":
+ describe = Describe()
+ describe.main()
diff --git a/api/boto/mapreduce/pdb_revert b/api/boto/mapreduce/pdb_revert
new file mode 100755
index 0000000..daffeef
--- /dev/null
+++ b/api/boto/mapreduce/pdb_revert
@@ -0,0 +1,135 @@
+#!/usr/bin/env python
+# 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.
+import queuetools, os, signal, sys
+import subprocess
+import time
+from optparse import OptionParser
+from boto.mapreduce.partitiondb import PartitionDB, Partition, Version
+from lqs import LQSServer, PersistHandler
+from boto.exception import SDBPersistenceError
+from boto.sdb.persist import get_manager
+
+USAGE = """
+ SYNOPSIS
+ %prog [options] [command]
+ DESCRIPTION
+ Revert to the previous Version in a PartitionDB.
+"""
+class Client:
+
+ def __init__(self, queue_name):
+ self.q = queuetools.get_queue(queue_name)
+ self.q.connect()
+ self.manager = get_manager()
+ self.process()
+
+ def process(self):
+ m = self.q.get()
+ while m['item']:
+ print 'Deleting: %s' % m['item']
+ p = Partition(id=m['item'], manager=self.manager)
+ p.delete()
+ self.q.delete(m)
+ m = self.q.get()
+ print 'client processing complete'
+
+class Server:
+
+ def __init__(self, pdb_name, domain_name=None):
+ self.pdb_name = pdb_name
+ self.manager = get_manager(domain_name)
+ self.pdb = PartitionDB.get(name=self.pdb_name)
+ self.serve()
+
+ def serve(self):
+ v = self.pdb.revert()
+ args = {'v_id' : v.id}
+ rs = v.partitions()
+ s = LQSServer('', PersistHandler, rs, args)
+ s.serve_forever()
+
+class Revert:
+
+ Commands = {'client' : 'Start a Revert client',
+ 'server' : 'Start a Revert server'}
+
+ def __init__(self):
+ self.parser = OptionParser(usage=USAGE)
+ self.parser.add_option("--help-commands", action="store_true", dest="help_commands",
+ help="provides help on the available commands")
+ self.parser.add_option('-d', '--domain-name', action='store', type='string',
+ help='name of the SimpleDB domain where PDB objects are stored')
+ self.parser.add_option('-n', '--num-processes', action='store', type='int', dest='num_processes',
+ help='the number of client processes launched')
+ self.parser.set_defaults(num_processes=5)
+ self.parser.add_option('-p', '--pdb-name', action='store', type='string',
+ help='name of the PDB in which to store files (will create if necessary)')
+ self.options, self.args = self.parser.parse_args()
+ self.prog_name = sys.argv[0]
+
+ def print_command_help(self):
+ print '\nCommands:'
+ for key in self.Commands.keys():
+ print ' %s\t\t%s' % (key, self.Commands[key])
+
+ def do_server(self):
+ if not self.options.pdb_name:
+ self.parser.error('No PDB name provided')
+ s = Server(self.options.pdb_name, self.options.domain_name)
+
+ def do_client(self):
+ c = Client('localhost')
+
+ def main(self):
+ if self.options.help_commands:
+ self.print_command_help()
+ sys.exit(0)
+ if len(self.args) == 0:
+ if not self.options.pdb_name:
+ self.parser.error('No PDB name provided')
+ server_command = '%s -p %s ' % (self.prog_name, self.options.pdb_name)
+ server_command += ' server'
+ client_command = '%s client' % self.prog_name
+ server = subprocess.Popen(server_command, shell=True)
+ print 'server pid: %s' % server.pid
+ time.sleep(5)
+ clients = []
+ for i in range(0, self.options.num_processes):
+ client = subprocess.Popen(client_command, shell=True)
+ clients.append(client)
+ print 'waiting for clients to finish'
+ for client in clients:
+ client.wait()
+ os.kill(server.pid, signal.SIGTERM)
+ elif len(self.args) == 1:
+ self.command = self.args[0]
+ if hasattr(self, 'do_%s' % self.command):
+ method = getattr(self, 'do_%s' % self.command)
+ method()
+ else:
+ self.parser.error('command (%s) not recognized' % self.command)
+ else:
+ self.parser.error('unrecognized commands')
+
+if __name__ == "__main__":
+ revert = Revert()
+ revert.main()
diff --git a/api/boto/mapreduce/pdb_upload b/api/boto/mapreduce/pdb_upload
new file mode 100755
index 0000000..1ca2b6d
--- /dev/null
+++ b/api/boto/mapreduce/pdb_upload
@@ -0,0 +1,172 @@
+#!/usr/bin/env python
+# 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.
+import queuetools, os, signal, sys
+import subprocess
+import time
+from optparse import OptionParser
+from boto.mapreduce.partitiondb import PartitionDB, Partition, Version
+from lqs import LQSServer, LQSHandler
+from boto.exception import SDBPersistenceError
+from boto.sdb.persist import get_manager
+
+USAGE = """
+ SYNOPSIS
+ %prog [options]
+ DESCRIPTION
+ Upload partition files to a PartitionDB.
+ Called with no options, all PartitionDB objects defined in your default
+ domain (as specified in the "default_domain" option in the "[Persist]"
+ section of your boto config file) will be listed.
+ When called with a particular PartitionDB name (using -p option) all
+ Version objects of that PartitionDB object will be listed.
+ When called with the -p option and a particular Version name specified
+ (using the -v option) all Partitions in that Version object will be listed.
+"""
+class Client:
+
+ def __init__(self, queue_name):
+ self.q = queuetools.get_queue(queue_name)
+ self.q.connect()
+ self.manager = get_manager()
+ self.process()
+
+ def process(self):
+ m = self.q.get()
+ if m['item']:
+ v = Version(m['args']['v_id'], self.manager)
+ bucket_name = v.pdb.bucket_name
+ while m['item']:
+ print 'Uploading: %s' % m['item']
+ p = v.add_partition(name=m['item'])
+ p.upload(os.path.join(m['args']['path'], m['item']), bucket_name)
+ self.q.delete(m)
+ m = self.q.get()
+ print 'client processing complete'
+
+class Server:
+
+ def __init__(self, path, pdb_name, bucket_name=None, domain_name=None):
+ self.path = path
+ self.pdb_name = pdb_name
+ self.bucket_name = bucket_name
+ self.manager = get_manager(domain_name)
+ self.get_pdb()
+ self.serve()
+
+ def get_pdb(self):
+ try:
+ self.pdb = PartitionDB.get(name=self.pdb_name)
+ except SDBPersistenceError:
+ self.pdb = PartitionDB(manager=self.manager, name=self.pdb_name, bucket_name=self.bucket_name)
+ self.pdb.save()
+
+ def serve(self):
+ v = self.pdb.add_version()
+ args = {'path' : self.path,
+ 'v_id' : v.id}
+ l = os.listdir(self.path)
+ s = LQSServer('', LQSHandler, iter(l), args)
+ s.serve_forever()
+
+class Upload:
+
+ Usage = "usage: %prog [options] command"
+
+ Commands = {'client' : 'Start an Upload client',
+ 'server' : 'Start an Upload server'}
+
+ def __init__(self):
+ self.parser = OptionParser(usage=self.Usage)
+ self.parser.add_option("--help-commands", action="store_true", dest="help_commands",
+ help="provides help on the available commands")
+ self.parser.add_option('-d', '--domain-name', action='store', type='string',
+ help='name of the SimpleDB domain where PDB objects are stored')
+ self.parser.add_option('-n', '--num-processes', action='store', type='int', dest='num_processes',
+ help='the number of client processes launched')
+ self.parser.set_defaults(num_processes=2)
+ self.parser.add_option('-i', '--input-path', action='store', type='string',
+ help='the path to directory to upload')
+ self.parser.add_option('-p', '--pdb-name', action='store', type='string',
+ help='name of the PDB in which to store files (will create if necessary)')
+ self.parser.add_option('-b', '--bucket-name', action='store', type='string',
+ help='name of S3 bucket (only needed if creating new PDB)')
+ self.options, self.args = self.parser.parse_args()
+ self.prog_name = sys.argv[0]
+
+ def print_command_help(self):
+ print '\nCommands:'
+ for key in self.Commands.keys():
+ print ' %s\t\t%s' % (key, self.Commands[key])
+
+ def do_server(self):
+ if not self.options.input_path:
+ self.parser.error('No path provided')
+ if not os.path.isdir(self.options.input_path):
+ self.parser.error('Invalid path (%s)' % self.options.input_path)
+ if not self.options.pdb_name:
+ self.parser.error('No PDB name provided')
+ s = Server(self.options.input_path, self.options.pdb_name,
+ self.options.bucket_name, self.options.domain_name)
+
+ def do_client(self):
+ c = Client('localhost')
+
+ def main(self):
+ if self.options.help_commands:
+ self.print_command_help()
+ sys.exit(0)
+ if len(self.args) == 0:
+ if not self.options.input_path:
+ self.parser.error('No path provided')
+ if not os.path.isdir(self.options.input_path):
+ self.parser.error('Invalid path (%s)' % self.options.input_path)
+ if not self.options.pdb_name:
+ self.parser.error('No PDB name provided')
+ server_command = '%s -p %s -i %s' % (self.prog_name, self.options.pdb_name, self.options.input_path)
+ if self.options.bucket_name:
+ server_command += ' -b %s' % self.options.bucket_name
+ server_command += ' server'
+ client_command = '%s client' % self.prog_name
+ server = subprocess.Popen(server_command, shell=True)
+ print 'server pid: %s' % server.pid
+ time.sleep(5)
+ clients = []
+ for i in range(0, self.options.num_processes):
+ client = subprocess.Popen(client_command, shell=True)
+ clients.append(client)
+ print 'waiting for clients to finish'
+ for client in clients:
+ client.wait()
+ os.kill(server.pid, signal.SIGTERM)
+ elif len(self.args) == 1:
+ self.command = self.args[0]
+ if hasattr(self, 'do_%s' % self.command):
+ method = getattr(self, 'do_%s' % self.command)
+ method()
+ else:
+ self.parser.error('command (%s) not recognized' % self.command)
+ else:
+ self.parser.error('unrecognized commands')
+
+if __name__ == "__main__":
+ upload = Upload()
+ upload.main()
diff --git a/api/boto/mapreduce/queuetools.py b/api/boto/mapreduce/queuetools.py
new file mode 100644
index 0000000..3e08a10
--- /dev/null
+++ b/api/boto/mapreduce/queuetools.py
@@ -0,0 +1,66 @@
+#!/usr/bin/python
+import socket, sys
+from lqs import LQSServer, LQSMessage
+import boto
+from boto.sqs.jsonmessage import JSONMessage
+
+class LQSClient:
+
+ def __init__(self, host):
+ self.host = host
+ self.port = LQSServer.PORT
+ self.timeout = LQSServer.TIMEOUT
+ self.max_len = LQSServer.MAXSIZE
+ self.sock = None
+
+ def connect(self):
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ self.sock.settimeout(self.timeout)
+ self.sock.connect((self.host, self.port))
+
+ def decode(self, jsonstr):
+ return LQSMessage(jsonvalue=jsonstr)
+
+ def get(self):
+ self.sock.send('next')
+ try:
+ jsonstr = self.sock.recv(self.max_len)
+ msg = LQSMessage(jsonvalue=jsonstr)
+ return msg
+ except:
+ print "recv from %s failed" % self.host
+
+ def delete(self, msg):
+ self.sock.send('delete %s' % msg['id'])
+ try:
+ jsonstr = self.sock.recv(self.max_len)
+ msg = LQSMessage(jsonvalue=jsonstr)
+ return msg
+ except:
+ print "recv from %s failed" % self.host
+
+ def close(self):
+ self.sock.close()
+
+class SQSClient:
+
+ def __init__(self, queue_name):
+ self.queue_name = queue_name
+
+ def connect(self):
+ self.queue = boto.lookup('sqs', self.queue_name)
+ self.queue.set_mesasge_class(JSONMessage)
+
+ def get(self):
+ m = self.queue.read()
+ return m.get_body()
+
+ def close(self):
+ pass
+
+def get_queue(name):
+ if name == 'localhost':
+ return LQSClient(name)
+ else:
+ return SQSClient(name)
+
diff --git a/api/boto/mashups/__init__.py b/api/boto/mashups/__init__.py
new file mode 100644
index 0000000..449bd16
--- /dev/null
+++ b/api/boto/mashups/__init__.py
@@ -0,0 +1,23 @@
+# 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/api/boto/mashups/iobject.py b/api/boto/mashups/iobject.py
new file mode 100644
index 0000000..a226b5c
--- /dev/null
+++ b/api/boto/mashups/iobject.py
@@ -0,0 +1,115 @@
+# 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 os
+
+def int_val_fn(v):
+ try:
+ int(v)
+ return True
+ except:
+ return False
+
+class IObject(object):
+
+ def choose_from_list(self, item_list, search_str='',
+ prompt='Enter Selection'):
+ if not item_list:
+ print 'No Choices Available'
+ return
+ choice = None
+ while not choice:
+ n = 1
+ choices = []
+ for item in item_list:
+ if isinstance(item, str):
+ print '[%d] %s' % (n, item)
+ choices.append(item)
+ n += 1
+ else:
+ obj, id, desc = item
+ if desc:
+ if desc.find(search_str) >= 0:
+ print '[%d] %s - %s' % (n, id, desc)
+ choices.append(obj)
+ n += 1
+ else:
+ if id.find(search_str) >= 0:
+ print '[%d] %s' % (n, id)
+ choices.append(obj)
+ n += 1
+ if choices:
+ val = raw_input('%s[1-%d]: ' % (prompt, len(choices)))
+ if val.startswith('/'):
+ search_str = val[1:]
+ else:
+ try:
+ int_val = int(val)
+ if int_val == 0:
+ return None
+ choice = choices[int_val-1]
+ except ValueError:
+ print '%s is not a valid choice' % val
+ except IndexError:
+ print '%s is not within the range[1-%d]' % (val,
+ len(choices))
+ else:
+ print "No objects matched your pattern"
+ search_str = ''
+ return choice
+
+ def get_string(self, prompt, validation_fn=None):
+ okay = False
+ while not okay:
+ val = raw_input('%s: ' % prompt)
+ if validation_fn:
+ okay = validation_fn(val)
+ if not okay:
+ print 'Invalid value: %s' % val
+ else:
+ okay = True
+ return val
+
+ def get_filename(self, prompt):
+ okay = False
+ val = ''
+ while not okay:
+ val = raw_input('%s: %s' % (prompt, val))
+ val = os.path.expanduser(val)
+ if os.path.isfile(val):
+ okay = True
+ elif os.path.isdir(val):
+ path = val
+ val = self.choose_from_list(os.listdir(path))
+ if val:
+ val = os.path.join(path, val)
+ okay = True
+ else:
+ val = ''
+ else:
+ print 'Invalid value: %s' % val
+ val = ''
+ return val
+
+ def get_int(self, prompt):
+ s = self.get_string(prompt, int_val_fn)
+ return int(s)
+
diff --git a/api/boto/mashups/order.py b/api/boto/mashups/order.py
new file mode 100644
index 0000000..6efdc3e
--- /dev/null
+++ b/api/boto/mashups/order.py
@@ -0,0 +1,211 @@
+# 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.
+
+"""
+High-level abstraction of an EC2 order for servers
+"""
+
+import boto
+import boto.ec2
+from boto.mashups.server import Server, ServerSet
+from boto.mashups.iobject import IObject
+from boto.pyami.config import Config
+from boto.sdb.persist import get_domain, set_domain
+import time, StringIO
+
+InstanceTypes = ['m1.small', 'm1.large', 'm1.xlarge', 'c1.medium', 'c1.xlarge']
+
+class Item(IObject):
+
+ def __init__(self):
+ self.region = None
+ self.name = None
+ self.instance_type = None
+ self.quantity = 0
+ self.zone = None
+ self.ami = None
+ self.groups = []
+ self.key = None
+ self.ec2 = None
+ self.config = None
+
+ def set_userdata(self, key, value):
+ self.userdata[key] = value
+
+ def get_userdata(self, key):
+ return self.userdata[key]
+
+ def set_region(self, region=None):
+ if region:
+ self.region = region
+ else:
+ l = [(r, r.name, r.endpoint) for r in boto.ec2.regions()]
+ self.region = self.choose_from_list(l, prompt='Choose Region')
+
+ def set_name(self, name=None):
+ if name:
+ self.name = name
+ else:
+ self.name = self.get_string('Name')
+
+ def set_instance_type(self, instance_type=None):
+ if instance_type:
+ self.instance_type = instance_type
+ else:
+ self.instance_type = self.choose_from_list(InstanceTypes, 'Instance Type')
+
+ def set_quantity(self, n=0):
+ if n > 0:
+ self.quantity = n
+ else:
+ self.quantity = self.get_int('Quantity')
+
+ def set_zone(self, zone=None):
+ if zone:
+ self.zone = zone
+ else:
+ l = [(z, z.name, z.state) for z in self.ec2.get_all_zones()]
+ self.zone = self.choose_from_list(l, prompt='Choose Availability Zone')
+
+ def set_ami(self, ami=None):
+ if ami:
+ self.ami = ami
+ else:
+ l = [(a, a.id, a.location) for a in self.ec2.get_all_images()]
+ self.ami = self.choose_from_list(l, prompt='Choose AMI')
+
+ def add_group(self, group=None):
+ if group:
+ self.groups.append(group)
+ else:
+ l = [(s, s.name, s.description) for s in self.ec2.get_all_security_groups()]
+ self.groups.append(self.choose_from_list(l, prompt='Choose Security Group'))
+
+ def set_key(self, key=None):
+ if key:
+ self.key = key
+ else:
+ l = [(k, k.name, '') for k in self.ec2.get_all_key_pairs()]
+ self.key = self.choose_from_list(l, prompt='Choose Keypair')
+
+ def update_config(self):
+ if not self.config.has_section('Credentials'):
+ self.config.add_section('Credentials')
+ self.config.set('Credentials', 'aws_access_key_id', self.ec2.aws_access_key_id)
+ self.config.set('Credentials', 'aws_secret_access_key', self.ec2.aws_secret_access_key)
+ if not self.config.has_section('Pyami'):
+ self.config.add_section('Pyami')
+ sdb_domain = get_domain()
+ if sdb_domain:
+ self.config.set('Pyami', 'server_sdb_domain', sdb_domain)
+ self.config.set('Pyami', 'server_sdb_name', self.name)
+
+ def set_config(self, config_path=None):
+ if not config_path:
+ config_path = self.get_filename('Specify Config file')
+ self.config = Config(path=config_path)
+
+ def get_userdata_string(self):
+ s = StringIO.StringIO()
+ self.config.write(s)
+ return s.getvalue()
+
+ def enter(self, **params):
+ self.region = params.get('region', self.region)
+ if not self.region:
+ self.set_region()
+ self.ec2 = self.region.connect()
+ self.name = params.get('name', self.name)
+ if not self.name:
+ self.set_name()
+ self.instance_type = params.get('instance_type', self.instance_type)
+ if not self.instance_type:
+ self.set_instance_type()
+ self.zone = params.get('zone', self.zone)
+ if not self.zone:
+ self.set_zone()
+ self.quantity = params.get('quantity', self.quantity)
+ if not self.quantity:
+ self.set_quantity()
+ self.ami = params.get('ami', self.ami)
+ if not self.ami:
+ self.set_ami()
+ self.groups = params.get('groups', self.groups)
+ if not self.groups:
+ self.add_group()
+ self.key = params.get('key', self.key)
+ if not self.key:
+ self.set_key()
+ self.config = params.get('config', self.config)
+ if not self.config:
+ self.set_config()
+ self.update_config()
+
+class Order(IObject):
+
+ def __init__(self):
+ self.items = []
+ self.reservation = None
+
+ def add_item(self, **params):
+ item = Item()
+ item.enter(**params)
+ self.items.append(item)
+
+ def display(self):
+ print 'This Order consists of the following items'
+ print
+ print 'QTY\tNAME\tTYPE\nAMI\t\tGroups\t\t\tKeyPair'
+ for item in self.items:
+ print '%s\t%s\t%s\t%s\t%s\t%s' % (item.quantity, item.name, item.instance_type,
+ item.ami.id, item.groups, item.key.name)
+
+ def place(self, block=True):
+ if get_domain() == None:
+ print 'SDB Persistence Domain not set'
+ domain_name = self.get_string('Specify SDB Domain')
+ set_domain(domain_name)
+ s = ServerSet()
+ for item in self.items:
+ r = item.ami.run(min_count=1, max_count=item.quantity,
+ key_name=item.key.name, user_data=item.get_userdata_string(),
+ security_groups=item.groups, instance_type=item.instance_type,
+ placement=item.zone.name)
+ if block:
+ states = [i.state for i in r.instances]
+ if states.count('running') != len(states):
+ print states
+ time.sleep(15)
+ states = [i.update() for i in r.instances]
+ for i in r.instances:
+ server = Server()
+ server.name = item.name
+ server.instance_id = i.id
+ server.reservation = r
+ server.save()
+ s.append(server)
+ if len(s) == 1:
+ return s[0]
+ else:
+ return s
+
+
+
diff --git a/api/boto/mashups/server.py b/api/boto/mashups/server.py
new file mode 100644
index 0000000..48f637b
--- /dev/null
+++ b/api/boto/mashups/server.py
@@ -0,0 +1,388 @@
+# 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.
+
+"""
+High-level abstraction of an EC2 server
+"""
+import boto, boto.utils
+from boto.mashups.iobject import IObject
+from boto.pyami.config import Config, BotoConfigPath
+from boto.mashups.interactive import interactive_shell
+from boto.sdb.db.model import Model
+from boto.sdb.db.property import *
+import os
+import StringIO
+
+class ServerSet(list):
+
+ def __getattr__(self, name):
+ results = []
+ is_callable = False
+ for server in self:
+ try:
+ val = getattr(server, name)
+ if callable(val):
+ is_callable = True
+ results.append(val)
+ except:
+ results.append(None)
+ if is_callable:
+ self.map_list = results
+ return self.map
+ return results
+
+ def map(self, *args):
+ results = []
+ for fn in self.map_list:
+ results.append(fn(*args))
+ return results
+
+class Server(Model):
+
+ ec2 = boto.connect_ec2()
+
+ @classmethod
+ def Inventory(cls):
+ """
+ Returns a list of Server instances, one for each Server object
+ persisted in the db
+ """
+ l = ServerSet()
+ rs = cls.find()
+ for server in rs:
+ l.append(server)
+ return l
+
+ @classmethod
+ def Register(cls, name, instance_id, description=''):
+ s = cls()
+ s.name = name
+ s.instance_id = instance_id
+ s.description = description
+ s.save()
+ return s
+
+ def __init__(self, id=None, **kw):
+ Model.__init__(self, id, **kw)
+ self._reservation = None
+ self._instance = None
+ self._ssh_client = None
+ self._pkey = None
+ self._config = None
+
+ name = StringProperty(unique=True, verbose_name="Name")
+ instance_id = StringProperty(verbose_name="Instance ID")
+ config_uri = StringProperty()
+ ami_id = StringProperty(verbose_name="AMI ID")
+ zone = StringProperty(verbose_name="Availability Zone")
+ security_group = StringProperty(verbose_name="Security Group", default="default")
+ key_name = StringProperty(verbose_name="Key Name")
+ elastic_ip = StringProperty(verbose_name="Elastic IP")
+ instance_type = StringProperty(verbose_name="Instance Type")
+ description = StringProperty(verbose_name="Description")
+ log = StringProperty()
+
+ def setReadOnly(self, value):
+ raise AttributeError
+
+ def getInstance(self):
+ if not self._instance:
+ if self.instance_id:
+ try:
+ rs = self.ec2.get_all_instances([self.instance_id])
+ except:
+ return None
+ if len(rs) > 0:
+ self._reservation = rs[0]
+ self._instance = self._reservation.instances[0]
+ return self._instance
+
+ instance = property(getInstance, setReadOnly, None, 'The Instance for the server')
+
+ def getAMI(self):
+ if self.instance:
+ return self.instance.image_id
+
+ ami = property(getAMI, setReadOnly, None, 'The AMI for the server')
+
+ def getStatus(self):
+ if self.instance:
+ self.instance.update()
+ return self.instance.state
+
+ status = property(getStatus, setReadOnly, None,
+ 'The status of the server')
+
+ def getHostname(self):
+ if self.instance:
+ return self.instance.public_dns_name
+
+ hostname = property(getHostname, setReadOnly, None,
+ 'The public DNS name of the server')
+
+ def getPrivateHostname(self):
+ if self.instance:
+ return self.instance.private_dns_name
+
+ private_hostname = property(getPrivateHostname, setReadOnly, None,
+ 'The private DNS name of the server')
+
+ def getLaunchTime(self):
+ if self.instance:
+ return self.instance.launch_time
+
+ launch_time = property(getLaunchTime, setReadOnly, None,
+ 'The time the Server was started')
+
+ def getConsoleOutput(self):
+ if self.instance:
+ return self.instance.get_console_output()
+
+ console_output = property(getConsoleOutput, setReadOnly, None,
+ 'Retrieve the console output for server')
+
+ def getGroups(self):
+ if self._reservation:
+ return self._reservation.groups
+ else:
+ return None
+
+ groups = property(getGroups, setReadOnly, None,
+ 'The Security Groups controlling access to this server')
+
+ def getConfig(self):
+ if not self._config:
+ remote_file = BotoConfigPath
+ local_file = '%s.ini' % self.instance.id
+ self.get_file(remote_file, local_file)
+ self._config = Config(local_file)
+ return self._config
+
+ def setConfig(self, config):
+ local_file = '%s.ini' % self.instance.id
+ fp = open(local_file)
+ config.write(fp)
+ fp.close()
+ self.put_file(local_file, BotoConfigPath)
+ self._config = config
+
+ config = property(getConfig, setConfig, None,
+ 'The instance data for this server')
+
+ def set_config(self, config):
+ """
+ Set SDB based config
+ """
+ self._config = config
+ self._config.dump_to_sdb("botoConfigs", self.id)
+
+ def load_config(self):
+ self._config = Config(do_load=False)
+ self._config.load_from_sdb("botoConfigs", self.id)
+
+ def stop(self):
+ if self.instance:
+ self.instance.stop()
+
+ def start(self):
+ self.stop()
+ ec2 = boto.connect_ec2()
+ ami = ec2.get_all_images(image_ids = [str(self.ami_id)])[0]
+ groups = ec2.get_all_security_groups(groupnames=[str(self.security_group)])
+ if not self._config:
+ self.load_config()
+ if not self._config.has_section("Credentials"):
+ self._config.add_section("Credentials")
+ self._config.set("Credentials", "aws_access_key_id", ec2.aws_access_key_id)
+ self._config.set("Credentials", "aws_secret_access_key", ec2.aws_secret_access_key)
+
+ if not self._config.has_section("Pyami"):
+ self._config.add_section("Pyami")
+
+ if self._manager.domain:
+ self._config.set('Pyami', 'server_sdb_domain', self._manager.domain.name)
+ self._config.set("Pyami", 'server_sdb_name', self.name)
+
+ cfg = StringIO.StringIO()
+ self._config.write(cfg)
+ cfg = cfg.getvalue()
+ r = ami.run(min_count=1,
+ max_count=1,
+ key_name=self.key_name,
+ security_groups = groups,
+ instance_type = self.instance_type,
+ placement = self.zone,
+ user_data = cfg)
+ i = r.instances[0]
+ self.instance_id = i.id
+ self.put()
+ if self.elastic_ip:
+ ec2.associate_address(self.instance_id, self.elastic_ip)
+
+ def reboot(self):
+ if self.instance:
+ self.instance.reboot()
+
+ def get_ssh_client(self, key_file=None, host_key_file='~/.ssh/known_hosts',
+ uname='root'):
+ import paramiko
+ if not self.instance:
+ print 'No instance yet!'
+ return
+ if not self._ssh_client:
+ if not key_file:
+ iobject = IObject()
+ key_file = iobject.get_filename('Path to OpenSSH Key file')
+ self._pkey = paramiko.RSAKey.from_private_key_file(key_file)
+ self._ssh_client = paramiko.SSHClient()
+ self._ssh_client.load_system_host_keys()
+ self._ssh_client.load_host_keys(os.path.expanduser(host_key_file))
+ self._ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ self._ssh_client.connect(self.instance.public_dns_name,
+ username=uname, pkey=self._pkey)
+ return self._ssh_client
+
+ def get_file(self, remotepath, localpath):
+ ssh_client = self.get_ssh_client()
+ sftp_client = ssh_client.open_sftp()
+ sftp_client.get(remotepath, localpath)
+
+ def put_file(self, localpath, remotepath):
+ ssh_client = self.get_ssh_client()
+ sftp_client = ssh_client.open_sftp()
+ sftp_client.put(localpath, remotepath)
+
+ def listdir(self, remotepath):
+ ssh_client = self.get_ssh_client()
+ sftp_client = ssh_client.open_sftp()
+ return sftp_client.listdir(remotepath)
+
+ def shell(self, key_file=None):
+ ssh_client = self.get_ssh_client(key_file)
+ channel = ssh_client.invoke_shell()
+ interactive_shell(channel)
+
+ def bundle_image(self, prefix, key_file, cert_file, size):
+ print 'bundling image...'
+ print '\tcopying cert and pk over to /mnt directory on server'
+ ssh_client = self.get_ssh_client()
+ sftp_client = ssh_client.open_sftp()
+ path, name = os.path.split(key_file)
+ remote_key_file = '/mnt/%s' % name
+ self.put_file(key_file, remote_key_file)
+ path, name = os.path.split(cert_file)
+ remote_cert_file = '/mnt/%s' % name
+ self.put_file(cert_file, remote_cert_file)
+ print '\tdeleting %s' % BotoConfigPath
+ # delete the metadata.ini file if it exists
+ try:
+ sftp_client.remove(BotoConfigPath)
+ except:
+ pass
+ command = 'ec2-bundle-vol '
+ command += '-c %s -k %s ' % (remote_cert_file, remote_key_file)
+ command += '-u %s ' % self._reservation.owner_id
+ command += '-p %s ' % prefix
+ command += '-s %d ' % size
+ command += '-d /mnt '
+ if self.instance.instance_type == 'm1.small' or self.instance_type == 'c1.medium':
+ command += '-r i386'
+ else:
+ command += '-r x86_64'
+ print '\t%s' % command
+ t = ssh_client.exec_command(command)
+ response = t[1].read()
+ print '\t%s' % response
+ print '\t%s' % t[2].read()
+ print '...complete!'
+
+ def upload_bundle(self, bucket, prefix):
+ print 'uploading bundle...'
+ command = 'ec2-upload-bundle '
+ command += '-m /mnt/%s.manifest.xml ' % prefix
+ command += '-b %s ' % bucket
+ command += '-a %s ' % self.ec2.aws_access_key_id
+ command += '-s %s ' % self.ec2.aws_secret_access_key
+ print '\t%s' % command
+ ssh_client = self.get_ssh_client()
+ t = ssh_client.exec_command(command)
+ response = t[1].read()
+ print '\t%s' % response
+ print '\t%s' % t[2].read()
+ print '...complete!'
+
+ def create_image(self, bucket=None, prefix=None, key_file=None, cert_file=None, size=None):
+ iobject = IObject()
+ if not bucket:
+ bucket = iobject.get_string('Name of S3 bucket')
+ if not prefix:
+ prefix = iobject.get_string('Prefix for AMI file')
+ if not key_file:
+ key_file = iobject.get_filename('Path to RSA private key file')
+ if not cert_file:
+ cert_file = iobject.get_filename('Path to RSA public cert file')
+ if not size:
+ size = iobject.get_int('Size (in MB) of bundled image')
+ self.bundle_image(prefix, key_file, cert_file, size)
+ self.upload_bundle(bucket, prefix)
+ print 'registering image...'
+ self.image_id = self.ec2.register_image('%s/%s.manifest.xml' % (bucket, prefix))
+ return self.image_id
+
+ def attach_volume(self, volume, device="/dev/sdp"):
+ """
+ Attach an EBS volume to this server
+
+ :param volume: EBS Volume to attach
+ :type volume: boto.ec2.volume.Volume
+
+ :param device: Device to attach to (default to /dev/sdp)
+ :type device: string
+ """
+ if hasattr(volume, "id"):
+ volume_id = volume.id
+ else:
+ volume_id = volume
+ return self.ec2.attach_volume(volume_id=volume_id, instance_id=self.instance_id, device=device)
+
+ def detach_volume(self, volume):
+ """
+ Detach an EBS volume from this server
+
+ :param volume: EBS Volume to detach
+ :type volume: boto.ec2.volume.Volume
+ """
+ if hasattr(volume, "id"):
+ volume_id = volume.id
+ else:
+ volume_id = volume
+ return self.ec2.detach_volume(volume_id=volume_id, instance_id=self.instance_id)
+
+ def install_package(self, package_name):
+ print 'installing %s...' % package_name
+ command = 'yum -y install %s' % package_name
+ print '\t%s' % command
+ ssh_client = self.get_ssh_client()
+ t = ssh_client.exec_command(command)
+ response = t[1].read()
+ print '\t%s' % response
+ print '\t%s' % t[2].read()
+ print '...complete!'
diff --git a/api/boto/mturk/__init__.py b/api/boto/mturk/__init__.py
new file mode 100644
index 0000000..449bd16
--- /dev/null
+++ b/api/boto/mturk/__init__.py
@@ -0,0 +1,23 @@
+# 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/api/boto/mturk/connection.py b/api/boto/mturk/connection.py
new file mode 100644
index 0000000..261e2a7
--- /dev/null
+++ b/api/boto/mturk/connection.py
@@ -0,0 +1,504 @@
+# 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 xml.sax
+import datetime
+
+from boto import handler
+from boto.mturk.price import Price
+import boto.mturk.notification
+from boto.connection import AWSQueryConnection
+from boto.exception import EC2ResponseError
+from boto.resultset import ResultSet
+
+class MTurkConnection(AWSQueryConnection):
+
+ APIVersion = '2006-10-31'
+ SignatureVersion = '1'
+
+ 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, host='mechanicalturk.amazonaws.com', debug=0,
+ https_connection_factory=None):
+ 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)
+
+ def get_account_balance(self):
+ """
+ """
+ params = {}
+ return self._process_request('GetAccountBalance', params, [('AvailableBalance', Price),
+ ('OnHoldBalance', Price)])
+
+ def register_hit_type(self, title, description, reward, duration,
+ keywords=None, approval_delay=None, qual_req=None):
+ """
+ Register a new HIT Type
+ \ttitle, description are strings
+ \treward is a Price object
+ \tduration can be an integer or string
+ """
+ params = {'Title' : title,
+ 'Description' : description,
+ 'AssignmentDurationInSeconds' : duration}
+ params.update(MTurkConnection.get_price_as_price(reward).get_as_params('Reward'))
+
+ if keywords:
+ params['Keywords'] = keywords
+
+ if approval_delay is not None:
+ params['AutoApprovalDelayInSeconds']= approval_delay
+
+ return self._process_request('RegisterHITType', params)
+
+ def set_email_notification(self, hit_type, email, event_types=None):
+ """
+ Performs a SetHITTypeNotification operation to set email notification for a specified HIT type
+ """
+ return self._set_notification(hit_type, 'Email', email, event_types)
+
+ def set_rest_notification(self, hit_type, url, event_types=None):
+ """
+ Performs a SetHITTypeNotification operation to set REST notification for a specified HIT type
+ """
+ return self._set_notification(hit_type, 'REST', url, event_types)
+
+ def _set_notification(self, hit_type, transport, destination, event_types=None):
+ """
+ Common SetHITTypeNotification operation to set notification for a specified HIT type
+ """
+ assert type(hit_type) is str, "hit_type argument should be a string."
+
+ params = {'HITTypeId': hit_type}
+
+ # from the Developer Guide:
+ # The 'Active' parameter is optional. If omitted, the active status of the HIT type's
+ # notification specification is unchanged. All HIT types begin with their
+ # notification specifications in the "inactive" status.
+ notification_params = {'Destination': destination,
+ 'Transport': transport,
+ 'Version': boto.mturk.notification.NotificationMessage.NOTIFICATION_VERSION,
+ 'Active': True,
+ }
+
+ # add specific event types if required
+ if event_types:
+ self.build_list_params(notification_params, event_types, 'EventType')
+
+ # Set up dict of 'Notification.1.Transport' etc. values
+ notification_rest_params = {}
+ num = 1
+ for key in notification_params:
+ notification_rest_params['Notification.%d.%s' % (num, key)] = notification_params[key]
+
+ # Update main params dict
+ params.update(notification_rest_params)
+
+ # Execute operation
+ return self._process_request('SetHITTypeNotification', params)
+
+ def create_hit(self, hit_type=None, question=None, lifetime=60*60*24*7, max_assignments=1,
+ title=None, description=None, keywords=None, reward=None,
+ duration=60*60*24*7, approval_delay=None, annotation=None, qual_req=None,
+ questions=None, qualifications=None, response_groups=None):
+ """
+ Creates a new HIT.
+ Returns a ResultSet
+ See: http://docs.amazonwebservices.com/AWSMechanicalTurkRequester/2006-10-31/ApiReference_CreateHITOperation.html
+ """
+
+ # handle single or multiple questions
+ if question is not None and questions is not None:
+ raise ValueError("Must specify either question (single Question instance) or questions (list), but not both")
+ if question is not None and questions is None:
+ questions = [question]
+
+
+ # Handle basic required arguments and set up params dict
+ params = {'Question': question.get_as_xml(),
+ 'LifetimeInSeconds' : lifetime,
+ 'MaxAssignments' : max_assignments,
+ }
+
+ # if hit type specified then add it
+ # else add the additional required parameters
+ if hit_type:
+ params['HITTypeId'] = hit_type
+ else:
+ # Handle keywords
+ final_keywords = MTurkConnection.get_keywords_as_string(keywords)
+
+ # Handle price argument
+ final_price = MTurkConnection.get_price_as_price(reward)
+
+ additional_params = {'Title': title,
+ 'Description' : description,
+ 'Keywords': final_keywords,
+ 'AssignmentDurationInSeconds' : duration,
+ }
+ additional_params.update(final_price.get_as_params('Reward'))
+
+ if approval_delay is not None:
+ additional_params['AutoApprovalDelayInSeconds'] = approval_delay
+
+ # add these params to the others
+ params.update(additional_params)
+
+ # add the annotation if specified
+ if annotation is not None:
+ params['RequesterAnnotation'] = annotation
+
+ # Add the Qualifications if specified
+ if qualifications is not None:
+ params.update(qualifications.get_as_params())
+
+ # Handle optional response groups argument
+ if response_groups:
+ self.build_list_params(params, response_groups, 'ResponseGroup')
+
+ # Submit
+ return self._process_request('CreateHIT', params, [('HIT', HIT),])
+
+ def get_reviewable_hits(self, hit_type=None, status='Reviewable',
+ sort_by='Expiration', sort_direction='Ascending',
+ page_size=10, page_number=1):
+ """
+ Retrieve the HITs that have a status of Reviewable, or HITs that
+ have a status of Reviewing, and that belong to the Requester calling the operation.
+ """
+ params = {'Status' : status,
+ 'SortProperty' : sort_by,
+ 'SortDirection' : sort_direction,
+ 'PageSize' : page_size,
+ 'PageNumber' : page_number}
+
+ # Handle optional hit_type argument
+ if hit_type is not None:
+ params.update({'HITTypeId': hit_type})
+
+ return self._process_request('GetReviewableHITs', params, [('HIT', HIT),])
+
+ def search_hits(self, sort_by='CreationTime', sort_direction='Ascending',
+ page_size=10, page_number=1):
+ """
+ Return all of a Requester's HITs, on behalf of the Requester.
+ The operation returns HITs of any status, except for HITs that have been disposed
+ with the DisposeHIT operation.
+ Note:
+ The SearchHITs operation does not accept any search parameters that filter the results.
+ """
+ params = {'SortProperty' : sort_by,
+ 'SortDirection' : sort_direction,
+ 'PageSize' : page_size,
+ 'PageNumber' : page_number}
+
+ return self._process_request('SearchHITs', params, [('HIT', HIT),])
+
+ def get_assignments(self, hit_id, status=None,
+ sort_by='SubmitTime', sort_direction='Ascending',
+ page_size=10, page_number=1):
+ """
+ Retrieves completed assignments for a HIT.
+ Use this operation to retrieve the results for a HIT.
+
+ The returned ResultSet will have the following attributes:
+
+ NumResults
+ The number of assignments on the page in the filtered results list,
+ equivalent to the number of assignments being returned by this call.
+ A non-negative integer
+ PageNumber
+ The number of the page in the filtered results list being returned.
+ A positive integer
+ TotalNumResults
+ The total number of HITs in the filtered results list based on this call.
+ A non-negative integer
+
+ The ResultSet will contain zero or more Assignment objects
+
+ """
+ params = {'HITId' : hit_id,
+ 'SortProperty' : sort_by,
+ 'SortDirection' : sort_direction,
+ 'PageSize' : page_size,
+ 'PageNumber' : page_number}
+
+ if status is not None:
+ params['AssignmentStatus'] = status
+
+ return self._process_request('GetAssignmentsForHIT', params, [('Assignment', Assignment),])
+
+ def approve_assignment(self, assignment_id, feedback=None):
+ """
+ """
+ params = {'AssignmentId' : assignment_id,}
+ if feedback:
+ params['RequesterFeedback'] = feedback
+ return self._process_request('ApproveAssignment', params)
+
+ def reject_assignment(self, assignment_id, feedback=None):
+ """
+ """
+ params = {'AssignmentId' : assignment_id,}
+ if feedback:
+ params['RequesterFeedback'] = feedback
+ return self._process_request('RejectAssignment', params)
+
+ def get_hit(self, hit_id):
+ """
+ """
+ params = {'HITId' : hit_id,}
+ return self._process_request('GetHIT', params, [('HIT', HIT),])
+
+ def set_reviewing(self, hit_id, revert=None):
+ """
+ Update a HIT with a status of Reviewable to have a status of Reviewing,
+ or reverts a Reviewing HIT back to the Reviewable status.
+
+ Only HITs with a status of Reviewable can be updated with a status of Reviewing.
+ Similarly, only Reviewing HITs can be reverted back to a status of Reviewable.
+ """
+ params = {'HITId' : hit_id,}
+ if revert:
+ params['Revert'] = revert
+ return self._process_request('SetHITAsReviewing', params)
+
+ def disable_hit(self, hit_id):
+ """
+ Remove a HIT from the Mechanical Turk marketplace, approves all submitted assignments
+ that have not already been approved or rejected, and disposes of the HIT and all
+ assignment data.
+
+ Assignments for the HIT that have already been submitted, but not yet approved or rejected, will be
+ automatically approved. Assignments in progress at the time of the call to DisableHIT will be
+ approved once the assignments are submitted. You will be charged for approval of these assignments.
+ DisableHIT completely disposes of the HIT and all submitted assignment data. Assignment results
+ data cannot be retrieved for a HIT that has been disposed.
+
+ It is not possible to re-enable a HIT once it has been disabled. To make the work from a disabled HIT
+ available again, create a new HIT.
+ """
+ params = {'HITId' : hit_id,}
+ return self._process_request('DisableHIT', params)
+
+ def dispose_hit(self, hit_id):
+ """
+ Dispose of a HIT that is no longer needed.
+
+ Only HITs in the "reviewable" state, with all submitted assignments approved or rejected,
+ can be disposed. A Requester can call GetReviewableHITs to determine which HITs are
+ reviewable, then call GetAssignmentsForHIT to retrieve the assignments.
+ Disposing of a HIT removes the HIT from the results of a call to GetReviewableHITs.
+ """
+ params = {'HITId' : hit_id,}
+ return self._process_request('DisposeHIT', params)
+
+ def expire_hit(self, hit_id):
+
+ """
+ Expire a HIT that is no longer needed.
+
+ The effect is identical to the HIT expiring on its own. The HIT no longer appears on the
+ Mechanical Turk web site, and no new Workers are allowed to accept the HIT. Workers who
+ have accepted the HIT prior to expiration are allowed to complete it or return it, or
+ allow the assignment duration to elapse (abandon the HIT). Once all remaining assignments
+ have been submitted, the expired HIT becomes "reviewable", and will be returned by a call
+ to GetReviewableHITs.
+ """
+ params = {'HITId' : hit_id,}
+ return self._process_request('ForceExpireHIT', params)
+
+ def extend_hit(self, hit_id, assignments_increment=None, expiration_increment=None):
+ """
+ Increase the maximum number of assignments, or extend the expiration date, of an existing HIT.
+
+ NOTE: If a HIT has a status of Reviewable and the HIT is extended to make it Available, the
+ HIT will not be returned by GetReviewableHITs, and its submitted assignments will not
+ be returned by GetAssignmentsForHIT, until the HIT is Reviewable again.
+ Assignment auto-approval will still happen on its original schedule, even if the HIT has
+ been extended. Be sure to retrieve and approve (or reject) submitted assignments before
+ extending the HIT, if so desired.
+ """
+ # must provide assignment *or* expiration increment
+ if (assignments_increment is None and expiration_increment is None) or \
+ (assignments_increment is not None and expiration_increment is not None):
+ raise ValueError("Must specify either assignments_increment or expiration_increment, but not both")
+
+ params = {'HITId' : hit_id,}
+ if assignments_increment:
+ params['MaxAssignmentsIncrement'] = assignments_increment
+ if expiration_increment:
+ params['ExpirationIncrementInSeconds'] = expiration_increment
+
+ return self._process_request('ExtendHIT', params)
+
+ def get_help(self, about, help_type='Operation'):
+ """
+ Return information about the Mechanical Turk Service operations and response group
+ NOTE - this is basically useless as it just returns the URL of the documentation
+
+ help_type: either 'Operation' or 'ResponseGroup'
+ """
+ params = {'About': about, 'HelpType': help_type,}
+ return self._process_request('Help', params)
+
+ def grant_bonus(self, worker_id, assignment_id, bonus_price, reason):
+ """
+ Issues a payment of money from your account to a Worker.
+ To be eligible for a bonus, the Worker must have submitted results for one of your
+ HITs, and have had those results approved or rejected. This payment happens separately
+ from the reward you pay to the Worker when you approve the Worker's assignment.
+ The Bonus must be passed in as an instance of the Price object.
+ """
+ params = bonus_price.get_as_params('BonusAmount', 1)
+ params['WorkerId'] = worker_id
+ params['AssignmentId'] = assignment_id
+ params['Reason'] = reason
+
+ return self._process_request('GrantBonus', params)
+
+ def _process_request(self, request_type, params, marker_elems=None):
+ """
+ Helper to process the xml response from AWS
+ """
+ response = self.make_request(request_type, params)
+ return self._process_response(response, marker_elems)
+
+ def _process_response(self, response, marker_elems=None):
+ """
+ Helper to process the xml response from AWS
+ """
+ body = response.read()
+ #print body
+ if '' not in body:
+ rs = ResultSet(marker_elems)
+ h = handler.XmlHandler(rs, self)
+ xml.sax.parseString(body, h)
+ return rs
+ else:
+ raise EC2ResponseError(response.status, response.reason, body)
+
+ @staticmethod
+ def get_keywords_as_string(keywords):
+ """
+ Returns a comma+space-separated string of keywords from either a list or a string
+ """
+ if type(keywords) is list:
+ final_keywords = ', '.join(keywords)
+ elif type(keywords) is str:
+ final_keywords = keywords
+ elif type(keywords) is unicode:
+ final_keywords = keywords.encode('utf-8')
+ elif keywords is None:
+ final_keywords = ""
+ else:
+ raise TypeError("keywords argument must be a string or a list of strings; got a %s" % type(keywords))
+ return final_keywords
+
+ @staticmethod
+ def get_price_as_price(reward):
+ """
+ Returns a Price data structure from either a float or a Price
+ """
+ if isinstance(reward, Price):
+ final_price = reward
+ else:
+ final_price = Price(reward)
+ return final_price
+
+class BaseAutoResultElement:
+ """
+ Base class to automatically add attributes when parsing XML
+ """
+ def __init__(self, connection):
+ self.connection = connection
+
+ def startElement(self, name, attrs, connection):
+ return None
+
+ def endElement(self, name, value, connection):
+ setattr(self, name, value)
+
+class HIT(BaseAutoResultElement):
+ """
+ Class to extract a HIT structure from a response (used in ResultSet)
+
+ Will have attributes named as per the Developer Guide,
+ e.g. HITId, HITTypeId, CreationTime
+ """
+
+ # property helper to determine if HIT has expired
+ def _has_expired(self):
+ """ Has this HIT expired yet? """
+ expired = False
+ if hasattr(self, 'Expiration'):
+ now = datetime.datetime.utcnow()
+ expiration = datetime.datetime.strptime(self.Expiration, '%Y-%m-%dT%H:%M:%SZ')
+ expired = (now >= expiration)
+ else:
+ raise ValueError("ERROR: Request for expired property, but no Expiration in HIT!")
+ return expired
+
+ # are we there yet?
+ expired = property(_has_expired)
+
+class Assignment(BaseAutoResultElement):
+ """
+ Class to extract an Assignment structure from a response (used in ResultSet)
+
+ Will have attributes named as per the Developer Guide,
+ e.g. AssignmentId, WorkerId, HITId, Answer, etc
+ """
+
+ def __init__(self, connection):
+ BaseAutoResultElement.__init__(self, connection)
+ self.answers = []
+
+ def endElement(self, name, value, connection):
+ # the answer consists of embedded XML, so it needs to be parsed independantly
+ if name == 'Answer':
+ answer_rs = ResultSet([('Answer', QuestionFormAnswer),])
+ h = handler.XmlHandler(answer_rs, connection)
+ value = self.connection.get_utf8_value(value)
+ xml.sax.parseString(value, h)
+ self.answers.append(answer_rs)
+ else:
+ BaseAutoResultElement.endElement(self, name, value, connection)
+
+class QuestionFormAnswer(BaseAutoResultElement):
+ """
+ Class to extract Answers from inside the embedded XML QuestionFormAnswers element inside the
+ Answer element which is part of the Assignment structure
+
+ A QuestionFormAnswers element contains an Answer element for each question in the HIT or
+ Qualification test for which the Worker provided an answer. Each Answer contains a
+ QuestionIdentifier element whose value corresponds to the QuestionIdentifier of a
+ Question in the QuestionForm. See the QuestionForm data structure for more information about
+ questions and answer specifications.
+
+ If the question expects a free-text answer, the Answer element contains a FreeText element. This
+ element contains the Worker's answer
+
+ *NOTE* - currently really only supports free-text answers
+ """
+
+ pass
diff --git a/api/boto/mturk/notification.py b/api/boto/mturk/notification.py
new file mode 100644
index 0000000..4904a99
--- /dev/null
+++ b/api/boto/mturk/notification.py
@@ -0,0 +1,95 @@
+# 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.
+
+"""
+Provides NotificationMessage and Event classes, with utility methods, for
+implementations of the Mechanical Turk Notification API.
+"""
+
+import hmac
+try:
+ from hashlib import sha1 as sha
+except ImportError:
+ import sha
+import base64
+import re
+
+class NotificationMessage:
+
+ NOTIFICATION_WSDL = "http://mechanicalturk.amazonaws.com/AWSMechanicalTurk/2006-05-05/AWSMechanicalTurkRequesterNotification.wsdl"
+ NOTIFICATION_VERSION = '2006-05-05'
+
+ SERVICE_NAME = "AWSMechanicalTurkRequesterNotification"
+ OPERATION_NAME = "Notify"
+
+ EVENT_PATTERN = r"Event\.(?P\d+)\.(?P\w+)"
+ EVENT_RE = re.compile(EVENT_PATTERN)
+
+ def __init__(self, d):
+ """
+ Constructor; expects parameter d to be a dict of string parameters from a REST transport notification message
+ """
+ self.signature = d['Signature'] # vH6ZbE0NhkF/hfNyxz2OgmzXYKs=
+ self.timestamp = d['Timestamp'] # 2006-05-23T23:22:30Z
+ self.version = d['Version'] # 2006-05-05
+ assert d['method'] == NotificationMessage.OPERATION_NAME, "Method should be '%s'" % NotificationMessage.OPERATION_NAME
+
+ # Build Events
+ self.events = []
+ events_dict = {}
+ if 'Event' in d:
+ # TurboGears surprised me by 'doing the right thing' and making { 'Event': { '1': { 'EventType': ... } } } etc.
+ events_dict = d['Event']
+ else:
+ for k in d:
+ v = d[k]
+ if k.startswith('Event.'):
+ ed = NotificationMessage.EVENT_RE.search(k).groupdict()
+ n = int(ed['n'])
+ param = str(ed['param'])
+ if n not in events_dict:
+ events_dict[n] = {}
+ events_dict[n][param] = v
+ for n in events_dict:
+ self.events.append(Event(events_dict[n]))
+
+ def verify(self, secret_key):
+ """
+ Verifies the authenticity of a notification message.
+ """
+ verification_input = NotificationMessage.SERVICE_NAME + NotificationMessage.OPERATION_NAME + self.timestamp
+ h = hmac.new(key=secret_key, digestmod=sha)
+ h.update(verification_input)
+ signature_calc = base64.b64encode(h.digest())
+ return self.signature == signature_calc
+
+class Event:
+ def __init__(self, d):
+ self.event_type = d['EventType']
+ self.event_time_str = d['EventTime']
+ self.hit_type = d['HITTypeId']
+ self.hit_id = d['HITId']
+ self.assignment_id = d['AssignmentId']
+
+ #TODO: build self.event_time datetime from string self.event_time_str
+
+ def __repr__(self):
+ return "" % (self.event_type, self.hit_id)
diff --git a/api/boto/mturk/price.py b/api/boto/mturk/price.py
new file mode 100644
index 0000000..3c88a96
--- /dev/null
+++ b/api/boto/mturk/price.py
@@ -0,0 +1,48 @@
+# 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.
+
+class Price:
+
+ def __init__(self, amount=0.0, currency_code='USD'):
+ self.amount = amount
+ self.currency_code = currency_code
+ self.formatted_price = ''
+
+ def __repr__(self):
+ if self.formatted_price:
+ return self.formatted_price
+ else:
+ return str(self.amount)
+
+ def startElement(self, name, attrs, connection):
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'Amount':
+ self.amount = float(value)
+ elif name == 'CurrencyCode':
+ self.currency_code = value
+ elif name == 'FormattedPrice':
+ self.formatted_price = value
+
+ def get_as_params(self, label, ord=1):
+ return {'%s.%d.Amount'%(label, ord) : str(self.amount),
+ '%s.%d.CurrencyCode'%(label, ord) : self.currency_code}
diff --git a/api/boto/mturk/qualification.py b/api/boto/mturk/qualification.py
new file mode 100644
index 0000000..ed02087
--- /dev/null
+++ b/api/boto/mturk/qualification.py
@@ -0,0 +1,118 @@
+# Copyright (c) 2008 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.
+
+class Qualifications:
+
+ def __init__(self, requirements = []):
+ self.requirements = requirements
+
+ def add(self, req):
+ self.requirements.append(req)
+
+ def get_as_params(self):
+ params = {}
+ assert(len(self.requirements) <= 10)
+ for n, req in enumerate(self.requirements):
+ reqparams = req.get_as_params()
+ for rp in reqparams:
+ params['QualificationRequirement.%s.%s' % ((n+1),rp) ] = reqparams[rp]
+ return params
+
+
+class Requirement(object):
+ """
+ Representation of a single requirement
+ """
+
+ def __init__(self, qualification_type_id, comparator, integer_value, required_to_preview=False):
+ self.qualification_type_id = qualification_type_id
+ self.comparator = comparator
+ self.integer_value = integer_value
+ self.required_to_preview = required_to_preview
+
+ def get_as_params(self):
+ params = {
+ "QualificationTypeId": self.qualification_type_id,
+ "Comparator": self.comparator,
+ "IntegerValue": self.integer_value,
+ }
+ if self.required_to_preview:
+ params['RequiredToPreview'] = "true"
+ return params
+
+class PercentAssignmentsSubmittedRequirement(Requirement):
+ """
+ The percentage of assignments the Worker has submitted, over all assignments the Worker has accepted. The value is an integer between 0 and 100.
+ """
+
+ def __init__(self, comparator, integer_value, required_to_preview=False):
+ Requirement.__init__(self, qualification_type_id="00000000000000000000", comparator=comparator, integer_value=integer_value, required_to_preview=required_to_preview)
+
+class PercentAssignmentsAbandonedRequirement(Requirement):
+ """
+ The percentage of assignments the Worker has abandoned (allowed the deadline to elapse), over all assignments the Worker has accepted. The value is an integer between 0 and 100.
+ """
+
+ def __init__(self, comparator, integer_value, required_to_preview=False):
+ Requirement.__init__(self, qualification_type_id="00000000000000000070", comparator=comparator, integer_value=integer_value, required_to_preview=required_to_preview)
+
+class PercentAssignmentsReturnedRequirement(Requirement):
+ """
+ The percentage of assignments the Worker has returned, over all assignments the Worker has accepted. The value is an integer between 0 and 100.
+ """
+
+ def __init__(self, comparator, integer_value, required_to_preview=False):
+ Requirement.__init__(self, qualification_type_id="000000000000000000E0", comparator=comparator, integer_value=integer_value, required_to_preview=required_to_preview)
+
+class PercentAssignmentsApprovedRequirement(Requirement):
+ """
+ The percentage of assignments the Worker has submitted that were subsequently approved by the Requester, over all assignments the Worker has submitted. The value is an integer between 0 and 100.
+ """
+
+ def __init__(self, comparator, integer_value, required_to_preview=False):
+ Requirement.__init__(self, qualification_type_id="000000000000000000L0", comparator=comparator, integer_value=integer_value, required_to_preview=required_to_preview)
+
+class PercentAssignmentsRejectedRequirement(Requirement):
+ """
+ The percentage of assignments the Worker has submitted that were subsequently rejected by the Requester, over all assignments the Worker has submitted. The value is an integer between 0 and 100.
+ """
+
+ def __init__(self, comparator, integer_value, required_to_preview=False):
+ Requirement.__init__(self, qualification_type_id="000000000000000000S0", comparator=comparator, integer_value=integer_value, required_to_preview=required_to_preview)
+
+class LocaleRequirement(Requirement):
+ """
+ A Qualification requirement based on the Worker's location. The Worker's location is specified by the Worker to Mechanical Turk when the Worker creates his account.
+ """
+
+ def __init__(self, comparator, locale, required_to_preview=False):
+ Requirement.__init__(self, qualification_type_id="00000000000000000071", comparator=comparator, integer_value=None, required_to_preview=required_to_preview)
+ self.locale = locale
+
+ def get_as_params(self):
+ params = {
+ "QualificationTypeId": self.qualification_type_id,
+ "Comparator": self.comparator,
+ 'LocaleValue.Country': self.locale,
+ }
+ if self.required_to_preview:
+ params['RequiredToPreview'] = "true"
+ return params
diff --git a/api/boto/mturk/question.py b/api/boto/mturk/question.py
new file mode 100644
index 0000000..89f1a45
--- /dev/null
+++ b/api/boto/mturk/question.py
@@ -0,0 +1,353 @@
+# 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.
+
+class Question(object):
+
+ QUESTION_XML_TEMPLATE = """%s%s%s%s%s"""
+ DISPLAY_NAME_XML_TEMPLATE = """%s"""
+
+ def __init__(self, identifier, content, answer_spec, is_required=False, display_name=None): #amount=0.0, currency_code='USD'):
+ self.identifier = identifier
+ self.content = content
+ self.answer_spec = answer_spec
+ self.is_required = is_required
+ self.display_name = display_name
+
+ def get_as_params(self, label='Question', identifier=None):
+
+ if identifier is None:
+ raise ValueError("identifier (QuestionIdentifier) is required per MTurk spec.")
+
+ return { label : self.get_as_xml() }
+
+ def get_as_xml(self):
+ # add the display name if required
+ display_name_xml = ''
+ if self.display_name:
+ display_name_xml = self.DISPLAY_NAME_XML_TEMPLATE %(self.display_name)
+
+ ret = Question.QUESTION_XML_TEMPLATE % (self.identifier,
+ display_name_xml,
+ str(self.is_required).lower(),
+ self.content.get_as_xml(),
+ self.answer_spec.get_as_xml())
+
+ return ret
+
+class ExternalQuestion(object):
+
+ EXTERNAL_QUESTIONFORM_SCHEMA_LOCATION = "http://mechanicalturk.amazonaws.com/AWSMechanicalTurkDataSchemas/2006-07-14/ExternalQuestion.xsd"
+ EXTERNAL_QUESTION_XML_TEMPLATE = """%s%s"""
+
+ def __init__(self, external_url, frame_height):
+ self.external_url = external_url
+ self.frame_height = frame_height
+
+ def get_as_params(self, label='ExternalQuestion'):
+ return { label : self.get_as_xml() }
+
+ def get_as_xml(self):
+ ret = ExternalQuestion.EXTERNAL_QUESTION_XML_TEMPLATE % (ExternalQuestion.EXTERNAL_QUESTIONFORM_SCHEMA_LOCATION,
+ self.external_url,
+ self.frame_height)
+ return ret
+
+class OrderedContent(object):
+ def __init__(self):
+ self.items = []
+
+ def append(self, field, value):
+ "Expects field type and value"
+ self.items.append((field, value))
+
+ def get_binary_xml(self, field, value):
+ return """
+
+
+ %s
+ %s
+
+ %s
+ %s
+""" % (value['binary_type'],
+ value['binary_subtype'],
+ value['binary'],
+ value['binary_alttext'])
+
+ def get_application_xml(self, field, value):
+ raise NotImplementedError("Application question content is not yet supported.")
+
+ def get_as_xml(self):
+ default_handler = lambda f,v: '<%s>%s%s>' % (f,v,f)
+ bulleted_list_handler = lambda _,list: '%s' % ''.join([('%s' % item) for item in list])
+ formatted_content_handler = lambda _,content: "" % content
+ application_handler = self.get_application_xml
+ binary_handler = self.get_binary_xml
+
+ children = ''
+ for (field,value) in self.items:
+ handler = default_handler
+ if field == 'List':
+ handler = bulleted_list_handler
+ elif field == 'Application':
+ handler = application_handler
+ elif field == 'Binary':
+ handler = binary_handler
+ elif field == 'FormattedContent':
+ handler = formatted_content_handler
+ children = children + handler(field, value)
+
+ return children
+
+class Overview(object):
+ OVERVIEW_XML_TEMPLATE = """%s"""
+
+ def __init__(self):
+ self.ordered_content = OrderedContent()
+
+ def append(self, field, value):
+ self.ordered_content.append(field,value)
+
+ def get_as_params(self, label='Overview'):
+ return { label : self.get_as_xml() }
+
+ def get_as_xml(self):
+ ret = Overview.OVERVIEW_XML_TEMPLATE % (self.ordered_content.get_as_xml())
+
+ return ret
+
+
+class QuestionForm(object):
+
+ QUESTIONFORM_SCHEMA_LOCATION = "http://mechanicalturk.amazonaws.com/AWSMechanicalTurkDataSchemas/2005-10-01/QuestionForm.xsd"
+ QUESTIONFORM_XML_TEMPLATE = """%s""" # % (ns, questions_xml)
+
+ def __init__(self, questions=None, overview=None):
+ if questions is None or type(questions) is not list:
+ raise ValueError("Must pass a list of Question instances to QuestionForm constructor")
+ else:
+ self.questions = questions
+ self.overview = overview
+
+
+ def get_as_xml(self):
+ if self.overview:
+ overview_xml = self.overview.get_as_xml()
+ questions_xml = "".join([q.get_as_xml() for q in self.questions])
+ qf_xml = overview_xml + questions_xml
+ return QuestionForm.QUESTIONFORM_XML_TEMPLATE % (QuestionForm.QUESTIONFORM_SCHEMA_LOCATION, qf_xml)
+
+ #def startElement(self, name, attrs, connection):
+ # return None
+ #
+ #def endElement(self, name, value, connection):
+ #
+ # #if name == 'Amount':
+ # # self.amount = float(value)
+ # #elif name == 'CurrencyCode':
+ # # self.currency_code = value
+ # #elif name == 'FormattedPrice':
+ # # self.formatted_price = value
+ #
+ # pass # What's this method for? I don't get it.
+
+class QuestionContent(object):
+ QUESTIONCONTENT_XML_TEMPLATE = """%s"""
+
+ def __init__(self):
+ self.ordered_content = OrderedContent()
+
+ def append(self, field, value):
+ self.ordered_content.append(field,value)
+
+ def get_as_xml(self):
+ ret = QuestionContent.QUESTIONCONTENT_XML_TEMPLATE % (self.ordered_content.get_as_xml())
+
+ return ret
+
+
+class AnswerSpecification(object):
+
+ ANSWERSPECIFICATION_XML_TEMPLATE = """%s"""
+
+ def __init__(self, spec):
+ self.spec = spec
+ def get_as_xml(self):
+ values = () # TODO
+ return AnswerSpecification.ANSWERSPECIFICATION_XML_TEMPLATE % self.spec.get_as_xml()
+
+class FreeTextAnswer(object):
+
+ FREETEXTANSWER_XML_TEMPLATE = """%s%s""" # (constraints, default)
+ FREETEXTANSWER_CONSTRAINTS_XML_TEMPLATE = """%s%s%s""" # (is_numeric_xml, length_xml, regex_xml)
+ FREETEXTANSWER_LENGTH_XML_TEMPLATE = """""" # (min_length_attr, max_length_attr)
+ FREETEXTANSWER_ISNUMERIC_XML_TEMPLATE = """""" # (min_value_attr, max_value_attr)
+ FREETEXTANSWER_DEFAULTTEXT_XML_TEMPLATE = """%s""" # (default)
+
+ def __init__(self, default=None, min_length=None, max_length=None, is_numeric=False, min_value=None, max_value=None, format_regex=None):
+ self.default = default
+ self.min_length = min_length
+ self.max_length = max_length
+ self.is_numeric = is_numeric
+ self.min_value = min_value
+ self.max_value = max_value
+ self.format_regex = format_regex
+
+ def get_as_xml(self):
+ is_numeric_xml = ""
+ if self.is_numeric:
+ min_value_attr = ""
+ max_value_attr = ""
+ if self.min_value:
+ min_value_attr = """minValue="%d" """ % self.min_value
+ if self.max_value:
+ max_value_attr = """maxValue="%d" """ % self.max_value
+ is_numeric_xml = FreeTextAnswer.FREETEXTANSWER_ISNUMERIC_XML_TEMPLATE % (min_value_attr, max_value_attr)
+
+ length_xml = ""
+ if self.min_length or self.max_length:
+ min_length_attr = ""
+ max_length_attr = ""
+ if self.min_length:
+ min_length_attr = """minLength="%d" """
+ if self.max_length:
+ max_length_attr = """maxLength="%d" """
+ length_xml = FreeTextAnswer.FREETEXTANSWER_LENGTH_XML_TEMPLATE % (min_length_attr, max_length_attr)
+
+ regex_xml = ""
+ if self.format_regex:
+ format_regex_attribs = '''regex="%s"''' %self.format_regex['regex']
+
+ error_text = self.format_regex.get('error_text', None)
+ if error_text:
+ format_regex_attribs += ' errorText="%s"' %error_text
+
+ flags = self.format_regex.get('flags', None)
+ if flags:
+ format_regex_attribs += ' flags="%s"' %flags
+
+ regex_xml = """""" %format_regex_attribs
+
+ constraints_xml = ""
+ if is_numeric_xml or length_xml or regex_xml:
+ constraints_xml = FreeTextAnswer.FREETEXTANSWER_CONSTRAINTS_XML_TEMPLATE % (is_numeric_xml, length_xml, regex_xml)
+
+ default_xml = ""
+ if self.default is not None:
+ default_xml = FreeTextAnswer.FREETEXTANSWER_DEFAULTTEXT_XML_TEMPLATE % self.default
+
+ return FreeTextAnswer.FREETEXTANSWER_XML_TEMPLATE % (constraints_xml, default_xml)
+
+class FileUploadAnswer(object):
+ FILEUPLOADANSWER_XML_TEMLPATE = """%d%d""" # (min, max)
+ DEFAULT_MIN_SIZE = 1024 # 1K (completely arbitrary!)
+ DEFAULT_MAX_SIZE = 5 * 1024 * 1024 # 5MB (completely arbitrary!)
+
+ def __init__(self, min=None, max=None):
+ self.min = min
+ self.max = max
+ if self.min is None:
+ self.min = FileUploadAnswer.DEFAULT_MIN_SIZE
+ if self.max is None:
+ self.max = FileUploadAnswer.DEFAULT_MAX_SIZE
+
+ def get_as_xml(self):
+ return FileUploadAnswer.FILEUPLOADANSWER_XML_TEMLPATE % (self.min, self.max)
+
+class SelectionAnswer(object):
+ """
+ A class to generate SelectionAnswer XML data structures.
+ Does not yet implement Binary selection options.
+ """
+ SELECTIONANSWER_XML_TEMPLATE = """%s%s%s""" # % (count_xml, style_xml, selections_xml)
+ SELECTION_XML_TEMPLATE = """%s%s""" # (identifier, value_xml)
+ SELECTION_VALUE_XML_TEMPLATE = """<%s>%s%s>""" # (type, value, type)
+ STYLE_XML_TEMPLATE = """%s""" # (style)
+ MIN_SELECTION_COUNT_XML_TEMPLATE = """%s""" # count
+ MAX_SELECTION_COUNT_XML_TEMPLATE = """%s""" # count
+ ACCEPTED_STYLES = ['radiobutton', 'dropdown', 'checkbox', 'list', 'combobox', 'multichooser']
+ OTHER_SELECTION_ELEMENT_NAME = 'OtherSelection'
+
+ def __init__(self, min=1, max=1, style=None, selections=None, type='text', other=False):
+
+ if style is not None:
+ if style in SelectionAnswer.ACCEPTED_STYLES:
+ self.style_suggestion = style
+ else:
+ raise ValueError("style '%s' not recognized; should be one of %s" % (style, ', '.join(SelectionAnswer.ACCEPTED_STYLES)))
+ else:
+ self.style_suggestion = None
+
+ if selections is None:
+ raise ValueError("SelectionAnswer.__init__(): selections must be a non-empty list of (content, identifier) tuples")
+ else:
+ self.selections = selections
+
+ self.min_selections = min
+ self.max_selections = max
+
+ assert len(selections) >= self.min_selections, "# of selections is less than minimum of %d" % self.min_selections
+ #assert len(selections) <= self.max_selections, "# of selections exceeds maximum of %d" % self.max_selections
+
+ self.type = type
+
+ self.other = other
+
+ def get_as_xml(self):
+ xml = ""
+ if self.type == 'text':
+ TYPE_TAG = "Text"
+ elif self.type == 'binary':
+ TYPE_TAG = "Binary"
+ else:
+ raise ValueError("illegal type: %s; must be either 'text' or 'binary'" % str(self.type))
+
+ # build list of elements
+ selections_xml = ""
+ for tpl in self.selections:
+ value_xml = SelectionAnswer.SELECTION_VALUE_XML_TEMPLATE % (TYPE_TAG, tpl[0], TYPE_TAG)
+ selection_xml = SelectionAnswer.SELECTION_XML_TEMPLATE % (tpl[1], value_xml)
+ selections_xml += selection_xml
+
+ if self.other:
+ # add OtherSelection element as xml if available
+ if hasattr(self.other, 'get_as_xml'):
+ assert type(self.other) == FreeTextAnswer, 'OtherSelection can only be a FreeTextAnswer'
+ selections_xml += self.other.get_as_xml().replace('FreeTextAnswer', 'OtherSelection')
+ else:
+ selections_xml += ""
+
+ if self.style_suggestion is not None:
+ style_xml = SelectionAnswer.STYLE_XML_TEMPLATE % self.style_suggestion
+ else:
+ style_xml = ""
+
+ if self.style_suggestion != 'radiobutton':
+ count_xml = SelectionAnswer.MIN_SELECTION_COUNT_XML_TEMPLATE %self.min_selections
+ count_xml += SelectionAnswer.MAX_SELECTION_COUNT_XML_TEMPLATE %self.max_selections
+ else:
+ count_xml = ""
+
+ ret = SelectionAnswer.SELECTIONANSWER_XML_TEMPLATE % (count_xml, style_xml, selections_xml)
+
+ # return XML
+ return ret
+
diff --git a/api/boto/mturk/test/all_tests.py b/api/boto/mturk/test/all_tests.py
new file mode 100644
index 0000000..a8f291a
--- /dev/null
+++ b/api/boto/mturk/test/all_tests.py
@@ -0,0 +1,8 @@
+import doctest
+
+# doctest.testfile("create_hit.doctest")
+# doctest.testfile("create_hit_binary.doctest")
+doctest.testfile("create_free_text_question_regex.doctest")
+# doctest.testfile("create_hit_from_hit_type.doctest")
+# doctest.testfile("search_hits.doctest")
+# doctest.testfile("reviewable_hits.doctest")
diff --git a/api/boto/mturk/test/cleanup_tests.py b/api/boto/mturk/test/cleanup_tests.py
new file mode 100644
index 0000000..7bdff90
--- /dev/null
+++ b/api/boto/mturk/test/cleanup_tests.py
@@ -0,0 +1,67 @@
+from boto.mturk.connection import MTurkConnection
+
+def cleanup():
+ """Remove any boto test related HIT's"""
+
+ conn = MTurkConnection(host='mechanicalturk.sandbox.amazonaws.com')
+ current_page = 1
+ page_size = 10
+ total_disabled = 0
+ ignored = []
+
+ while True:
+ # reset the total for this loop
+ disabled_count = 0
+
+ # search all the hits in the sandbox
+ search_rs = conn.search_hits(page_size=page_size, page_number=current_page)
+
+ # success?
+ if search_rs.status:
+ for hit in search_rs:
+ # delete any with Boto in the description
+ print 'hit id:%s Status:%s, desc:%s' %(hit.HITId, hit.HITStatus, hit.Description)
+ if hit.Description.find('Boto') != -1:
+ if hit.HITStatus != 'Reviewable':
+ print 'Disabling hit id:%s %s' %(hit.HITId, hit.Description)
+ disable_rs = conn.disable_hit(hit.HITId)
+ if disable_rs.status:
+ disabled_count += 1
+ # update the running total
+ total_disabled += 1
+ else:
+ print 'Error when disabling, code:%s, message:%s' %(disable_rs.Code, disable_rs.Message)
+ else:
+ print 'Disposing hit id:%s %s' %(hit.HITId, hit.Description)
+ dispose_rs = conn.dispose_hit(hit.HITId)
+ if dispose_rs.status:
+ disabled_count += 1
+ # update the running total
+ total_disabled += 1
+ else:
+ print 'Error when disposing, code:%s, message:%s' %(dispose_rs.Code, dispose_rs.Message)
+
+ else:
+ if hit.HITId not in ignored:
+ print 'ignored:%s' %hit.HITId
+ ignored.append(hit.HITId)
+
+ # any more results?
+ if int(search_rs.TotalNumResults) > current_page*page_size:
+ # if we have disabled any HITs on this page
+ # then we don't need to go to a new page
+ # otherwise we do
+ if not disabled_count:
+ current_page += 1
+ else:
+ # no, we're done
+ break
+ else:
+ print 'Error performing search, code:%s, message:%s' %(search_rs.Code, search_rs.Message)
+ break
+
+ total_ignored = len(ignored)
+ print 'Processed: %d HITs, disabled/disposed: %d, ignored: %d' %(total_ignored + total_disabled, total_disabled, total_ignored)
+
+if __name__ == '__main__':
+ cleanup()
diff --git a/api/boto/mturk/test/create_free_text_question_regex.doctest b/api/boto/mturk/test/create_free_text_question_regex.doctest
new file mode 100644
index 0000000..a10b7ed
--- /dev/null
+++ b/api/boto/mturk/test/create_free_text_question_regex.doctest
@@ -0,0 +1,92 @@
+>>> import uuid
+>>> import datetime
+>>> from boto.mturk.connection import MTurkConnection
+>>> from boto.mturk.question import Question, QuestionContent, AnswerSpecification, FreeTextAnswer
+
+>>> conn = MTurkConnection(host='mechanicalturk.sandbox.amazonaws.com')
+
+# create content for a question
+>>> qn_content = QuestionContent(title='Boto no hit type question content',
+... text='What is a boto no hit type?')
+
+# create a free text answer that is not quite so free!
+>>> ft_answer = FreeTextAnswer(format_regex=dict(regex="^[12][0-9]{3}-[01]?\d-[0-3]?\d$",
+... error_text="You must enter a date with the format yyyy-mm-dd.",
+... flags="i"),
+... default="This is not a valid format")
+
+# create the question specification
+>>> qn = Question(identifier=str(uuid.uuid4()),
+... content=qn_content,
+... answer_spec=AnswerSpecification(ft_answer))
+
+# now, create the actual HIT for the question without using a HIT type
+# NOTE - the response_groups are specified to get back additional information for testing
+>>> keywords=['boto', 'test', 'doctest']
+>>> create_hit_rs = conn.create_hit(question=qn,
+... lifetime=60*65,
+... max_assignments=2,
+... title='Boto create_hit title',
+... description='Boto create_hit description',
+... keywords=keywords,
+... reward=0.23,
+... duration=60*6,
+... approval_delay=60*60,
+... annotation='An annotation from boto create_hit test',
+... response_groups=['Minimal',
+... 'HITDetail',
+... 'HITQuestion',
+... 'HITAssignmentSummary',])
+
+# this is a valid request
+>>> create_hit_rs.status
+True
+
+# for the requested hit type id
+# the HIT Type Id is a unicode string
+>>> hit_type_id = create_hit_rs.HITTypeId
+>>> hit_type_id # doctest: +ELLIPSIS
+u'...'
+
+>>> create_hit_rs.MaxAssignments
+u'2'
+
+>>> create_hit_rs.AutoApprovalDelayInSeconds
+u'3600'
+
+# expiration should be very close to now + the lifetime in seconds
+>>> expected_datetime = datetime.datetime.utcnow() + datetime.timedelta(seconds=3900)
+>>> expiration_datetime = datetime.datetime.strptime(create_hit_rs.Expiration, '%Y-%m-%dT%H:%M:%SZ')
+>>> delta = expected_datetime - expiration_datetime
+>>> delta.seconds < 5
+True
+
+# duration is as specified for the HIT type
+>>> create_hit_rs.AssignmentDurationInSeconds
+u'360'
+
+# the reward has been set correctly (allow for float error here)
+>>> int(create_hit_rs[0].amount * 100)
+23
+
+>>> create_hit_rs[0].formatted_price
+u'$0.23'
+
+# only US currency supported at present
+>>> create_hit_rs[0].currency_code
+u'USD'
+
+# title is the HIT type title
+>>> create_hit_rs.Title
+u'Boto create_hit title'
+
+# title is the HIT type description
+>>> create_hit_rs.Description
+u'Boto create_hit description'
+
+# annotation is correct
+>>> create_hit_rs.RequesterAnnotation
+u'An annotation from boto create_hit test'
+
+>>> create_hit_rs.HITReviewStatus
+u'NotReviewed'
diff --git a/api/boto/mturk/test/create_hit.doctest b/api/boto/mturk/test/create_hit.doctest
new file mode 100644
index 0000000..22209d6
--- /dev/null
+++ b/api/boto/mturk/test/create_hit.doctest
@@ -0,0 +1,86 @@
+>>> import uuid
+>>> import datetime
+>>> from boto.mturk.connection import MTurkConnection
+>>> from boto.mturk.question import Question, QuestionContent, AnswerSpecification, FreeTextAnswer
+
+>>> conn = MTurkConnection(host='mechanicalturk.sandbox.amazonaws.com')
+
+# create content for a question
+>>> qn_content = QuestionContent(title='Boto no hit type question content',
+... text='What is a boto no hit type?')
+
+# create the question specification
+>>> qn = Question(identifier=str(uuid.uuid4()),
+... content=qn_content,
+... answer_spec=AnswerSpecification(FreeTextAnswer()))
+
+# now, create the actual HIT for the question without using a HIT type
+# NOTE - the response_groups are specified to get back additional information for testing
+>>> keywords=['boto', 'test', 'doctest']
+>>> create_hit_rs = conn.create_hit(question=qn,
+... lifetime=60*65,
+... max_assignments=2,
+... title='Boto create_hit title',
+... description='Boto create_hit description',
+... keywords=keywords,
+... reward=0.23,
+... duration=60*6,
+... approval_delay=60*60,
+... annotation='An annotation from boto create_hit test',
+... response_groups=['Minimal',
+... 'HITDetail',
+... 'HITQuestion',
+... 'HITAssignmentSummary',])
+
+# this is a valid request
+>>> create_hit_rs.status
+True
+
+# for the requested hit type id
+# the HIT Type Id is a unicode string
+>>> hit_type_id = create_hit_rs.HITTypeId
+>>> hit_type_id # doctest: +ELLIPSIS
+u'...'
+
+>>> create_hit_rs.MaxAssignments
+u'2'
+
+>>> create_hit_rs.AutoApprovalDelayInSeconds
+u'3600'
+
+# expiration should be very close to now + the lifetime in seconds
+>>> expected_datetime = datetime.datetime.utcnow() + datetime.timedelta(seconds=3900)
+>>> expiration_datetime = datetime.datetime.strptime(create_hit_rs.Expiration, '%Y-%m-%dT%H:%M:%SZ')
+>>> delta = expected_datetime - expiration_datetime
+>>> delta.seconds < 5
+True
+
+# duration is as specified for the HIT type
+>>> create_hit_rs.AssignmentDurationInSeconds
+u'360'
+
+# the reward has been set correctly (allow for float error here)
+>>> int(create_hit_rs[0].amount * 100)
+23
+
+>>> create_hit_rs[0].formatted_price
+u'$0.23'
+
+# only US currency supported at present
+>>> create_hit_rs[0].currency_code
+u'USD'
+
+# title is the HIT type title
+>>> create_hit_rs.Title
+u'Boto create_hit title'
+
+# title is the HIT type description
+>>> create_hit_rs.Description
+u'Boto create_hit description'
+
+# annotation is correct
+>>> create_hit_rs.RequesterAnnotation
+u'An annotation from boto create_hit test'
+
+>>> create_hit_rs.HITReviewStatus
+u'NotReviewed'
diff --git a/api/boto/mturk/test/create_hit_binary.doctest b/api/boto/mturk/test/create_hit_binary.doctest
new file mode 100644
index 0000000..3096083
--- /dev/null
+++ b/api/boto/mturk/test/create_hit_binary.doctest
@@ -0,0 +1,87 @@
+>>> import uuid
+>>> import datetime
+>>> from boto.mturk.connection import MTurkConnection
+>>> from boto.mturk.question import Question, QuestionContent, AnswerSpecification, FreeTextAnswer
+
+>>> conn = MTurkConnection(host='mechanicalturk.sandbox.amazonaws.com')
+
+# create content for a question
+>>> qn_content = QuestionContent(title='Boto no hit type question content',
+... text='What is a boto no hit type?',
+... binary='http://www.example.com/test1.jpg')
+
+# create the question specification
+>>> qn = Question(identifier=str(uuid.uuid4()),
+... content=qn_content,
+... answer_spec=AnswerSpecification(FreeTextAnswer()))
+
+# now, create the actual HIT for the question without using a HIT type
+# NOTE - the response_groups are specified to get back additional information for testing
+>>> keywords=['boto', 'test', 'doctest']
+>>> create_hit_rs = conn.create_hit(question=qn,
+... lifetime=60*65,
+... max_assignments=2,
+... title='Boto create_hit title',
+... description='Boto create_hit description',
+... keywords=keywords,
+... reward=0.23,
+... duration=60*6,
+... approval_delay=60*60,
+... annotation='An annotation from boto create_hit test',
+... response_groups=['Minimal',
+... 'HITDetail',
+... 'HITQuestion',
+... 'HITAssignmentSummary',])
+
+# this is a valid request
+>>> create_hit_rs.status
+True
+
+# for the requested hit type id
+# the HIT Type Id is a unicode string
+>>> hit_type_id = create_hit_rs.HITTypeId
+>>> hit_type_id # doctest: +ELLIPSIS
+u'...'
+
+>>> create_hit_rs.MaxAssignments
+u'2'
+
+>>> create_hit_rs.AutoApprovalDelayInSeconds
+u'3600'
+
+# expiration should be very close to now + the lifetime in seconds
+>>> expected_datetime = datetime.datetime.utcnow() + datetime.timedelta(seconds=3900)
+>>> expiration_datetime = datetime.datetime.strptime(create_hit_rs.Expiration, '%Y-%m-%dT%H:%M:%SZ')
+>>> delta = expected_datetime - expiration_datetime
+>>> delta.seconds < 5
+True
+
+# duration is as specified for the HIT type
+>>> create_hit_rs.AssignmentDurationInSeconds
+u'360'
+
+# the reward has been set correctly (allow for float error here)
+>>> int(create_hit_rs[0].amount * 100)
+23
+
+>>> create_hit_rs[0].formatted_price
+u'$0.23'
+
+# only US currency supported at present
+>>> create_hit_rs[0].currency_code
+u'USD'
+
+# title is the HIT type title
+>>> create_hit_rs.Title
+u'Boto create_hit title'
+
+# title is the HIT type description
+>>> create_hit_rs.Description
+u'Boto create_hit description'
+
+# annotation is correct
+>>> create_hit_rs.RequesterAnnotation
+u'An annotation from boto create_hit test'
+
+>>> create_hit_rs.HITReviewStatus
+u'NotReviewed'
diff --git a/api/boto/mturk/test/create_hit_external.py b/api/boto/mturk/test/create_hit_external.py
new file mode 100644
index 0000000..e7425d6
--- /dev/null
+++ b/api/boto/mturk/test/create_hit_external.py
@@ -0,0 +1,14 @@
+import uuid
+import datetime
+from boto.mturk.connection import MTurkConnection
+from boto.mturk.question import ExternalQuestion
+
+def test():
+ q = ExternalQuestion(external_url="http://websort.net/s/F3481C", frame_height=800)
+ conn = MTurkConnection(host='mechanicalturk.sandbox.amazonaws.com')
+ keywords=['boto', 'test', 'doctest']
+ create_hit_rs = conn.create_hit(question=q, lifetime=60*65,max_assignments=2,title="Boto External Question Test", keywords=keywords,reward = 0.05, duration=60*6,approval_delay=60*60, annotation='An annotation from boto external question test', response_groups=['Minimal','HITDetail','HITQuestion','HITAssignmentSummary',])
+ assert(create_hit_rs.status == True)
+
+if __name__ == "__main__":
+ test()
diff --git a/api/boto/mturk/test/create_hit_from_hit_type.doctest b/api/boto/mturk/test/create_hit_from_hit_type.doctest
new file mode 100644
index 0000000..144a677
--- /dev/null
+++ b/api/boto/mturk/test/create_hit_from_hit_type.doctest
@@ -0,0 +1,97 @@
+>>> import uuid
+>>> import datetime
+>>> from boto.mturk.connection import MTurkConnection
+>>> from boto.mturk.question import Question, QuestionContent, AnswerSpecification, FreeTextAnswer
+>>>
+>>> conn = MTurkConnection(host='mechanicalturk.sandbox.amazonaws.com')
+>>> keywords=['boto', 'test', 'doctest']
+>>> hit_type_rs = conn.register_hit_type('Boto Test HIT type',
+... 'HIT Type for testing Boto',
+... 0.12,
+... 60*6,
+... keywords=keywords,
+... approval_delay=60*60)
+
+# this was a valid request
+>>> hit_type_rs.status
+True
+
+# the HIT Type Id is a unicode string
+>>> hit_type_id = hit_type_rs.HITTypeId
+>>> hit_type_id # doctest: +ELLIPSIS
+u'...'
+
+# create content for a question
+>>> qn_content = QuestionContent(title='Boto question content create_hit_from_hit_type',
+... text='What is a boto create_hit_from_hit_type?')
+
+# create the question specification
+>>> qn = Question(identifier=str(uuid.uuid4()),
+... content=qn_content,
+... answer_spec=AnswerSpecification(FreeTextAnswer()))
+
+# now, create the actual HIT for the question using the HIT type
+# NOTE - the response_groups are specified to get back additional information for testing
+>>> create_hit_rs = conn.create_hit(hit_type=hit_type_rs.HITTypeId,
+... question=qn,
+... lifetime=60*65,
+... max_assignments=2,
+... annotation='An annotation from boto create_hit_from_hit_type test',
+... response_groups=['Minimal',
+... 'HITDetail',
+... 'HITQuestion',
+... 'HITAssignmentSummary',])
+
+# this is a valid request
+>>> create_hit_rs.status
+True
+
+# for the requested hit type id
+>>> create_hit_rs.HITTypeId == hit_type_id
+True
+
+# with the correct number of maximum assignments
+>>> create_hit_rs.MaxAssignments
+u'2'
+
+# and the approval delay
+>>> create_hit_rs.AutoApprovalDelayInSeconds
+u'3600'
+
+# expiration should be very close to now + the lifetime in seconds
+>>> expected_datetime = datetime.datetime.utcnow() + datetime.timedelta(seconds=3900)
+>>> expiration_datetime = datetime.datetime.strptime(create_hit_rs.Expiration, '%Y-%m-%dT%H:%M:%SZ')
+>>> delta = expected_datetime - expiration_datetime
+>>> delta.seconds < 5
+True
+
+# duration is as specified for the HIT type
+>>> create_hit_rs.AssignmentDurationInSeconds
+u'360'
+
+# the reward has been set correctly
+>>> create_hit_rs[0].amount
+0.12
+
+>>> create_hit_rs[0].formatted_price
+u'$0.12'
+
+# only US currency supported at present
+>>> create_hit_rs[0].currency_code
+u'USD'
+
+# title is the HIT type title
+>>> create_hit_rs.Title
+u'Boto Test HIT type'
+
+# title is the HIT type description
+>>> create_hit_rs.Description
+u'HIT Type for testing Boto'
+
+# annotation is correct
+>>> create_hit_rs.RequesterAnnotation
+u'An annotation from boto create_hit_from_hit_type test'
+
+# not reviewed yet
+>>> create_hit_rs.HITReviewStatus
+u'NotReviewed'
diff --git a/api/boto/mturk/test/create_hit_with_qualifications.py b/api/boto/mturk/test/create_hit_with_qualifications.py
new file mode 100644
index 0000000..f2149ee
--- /dev/null
+++ b/api/boto/mturk/test/create_hit_with_qualifications.py
@@ -0,0 +1,18 @@
+import uuid
+import datetime
+from boto.mturk.connection import MTurkConnection
+from boto.mturk.question import ExternalQuestion
+from boto.mturk.qualification import Qualifications, PercentAssignmentsApprovedRequirement
+
+def test():
+ q = ExternalQuestion(external_url="http://websort.net/s/F3481C", frame_height=800)
+ conn = MTurkConnection(host='mechanicalturk.sandbox.amazonaws.com')
+ keywords=['boto', 'test', 'doctest']
+ qualifications = Qualifications()
+ qualifications.add(PercentAssignmentsApprovedRequirement(comparator="GreaterThan", integer_value="95"))
+ create_hit_rs = conn.create_hit(question=q, lifetime=60*65,max_assignments=2,title="Boto External Question Test", keywords=keywords,reward = 0.05, duration=60*6,approval_delay=60*60, annotation='An annotation from boto external question test', qualifications=qualifications)
+ assert(create_hit_rs.status == True)
+ print create_hit_rs.HITTypeId
+
+if __name__ == "__main__":
+ test()
diff --git a/api/boto/mturk/test/reviewable_hits.doctest b/api/boto/mturk/test/reviewable_hits.doctest
new file mode 100644
index 0000000..0305901
--- /dev/null
+++ b/api/boto/mturk/test/reviewable_hits.doctest
@@ -0,0 +1,71 @@
+>>> from boto.mturk.connection import MTurkConnection
+>>> conn = MTurkConnection(host='mechanicalturk.sandbox.amazonaws.com')
+
+# should have some reviewable HIT's returned, especially if returning all HIT type's
+# NOTE: but only if your account has existing HIT's in the reviewable state
+>>> reviewable_rs = conn.get_reviewable_hits()
+
+# this is a valid request
+>>> reviewable_rs.status
+True
+
+>>> len(reviewable_rs) > 1
+True
+
+# should contain at least one HIT object
+>>> reviewable_rs # doctest: +ELLIPSIS
+[>> hit_id = reviewable_rs[0].HITId
+
+# check that we can retrieve the assignments for a HIT
+>>> assignments_rs = conn.get_assignments(hit_id)
+
+# this is a valid request
+>>> assignments_rs.status
+True
+
+>>> assignments_rs.NumResults >= 1
+True
+
+>>> len(assignments_rs) == int(assignments_rs.NumResults)
+True
+
+>>> assignments_rs.PageNumber
+u'1'
+
+>>> assignments_rs.TotalNumResults >= 1
+True
+
+# should contain at least one Assignment object
+>>> assignments_rs # doctest: +ELLIPSIS
+[>> assignment = assignments_rs[0]
+
+>>> assignment.HITId == hit_id
+True
+
+# should have a valid status
+>>> assignment.AssignmentStatus in ['Submitted', 'Approved', 'Rejected']
+True
+
+# should have returned at least one answer
+>>> len(assignment.answers) > 0
+True
+
+# should contain at least one set of QuestionFormAnswer objects
+>>> assignment.answers # doctest: +ELLIPSIS
+[[>> answer = assignment.answers[0][0]
+
+# answer should be a FreeTextAnswer
+>>> answer.FreeText # doctest: +ELLIPSIS
+u'...'
+
+# question identifier should be a unicode string
+>>> answer.QuestionIdentifier # doctest: +ELLIPSIS
+u'...'
+
diff --git a/api/boto/mturk/test/search_hits.doctest b/api/boto/mturk/test/search_hits.doctest
new file mode 100644
index 0000000..a2547ea
--- /dev/null
+++ b/api/boto/mturk/test/search_hits.doctest
@@ -0,0 +1,16 @@
+>>> from boto.mturk.connection import MTurkConnection
+>>> conn = MTurkConnection(host='mechanicalturk.sandbox.amazonaws.com')
+
+# should have some HIT's returned by a search (but only if your account has existing HIT's)
+>>> search_rs = conn.search_hits()
+
+# this is a valid request
+>>> search_rs.status
+True
+
+>>> len(search_rs) > 1
+True
+
+>>> search_rs # doctest: +ELLIPSIS
+[= 0:
+ method, version = update.split(':')
+ version = '-r%s' % version
+ else:
+ version = '-rHEAD'
+ location = boto.config.get('Boto', 'boto_location', '/usr/local/boto')
+ self.run('svn update %s %s' % (version, location))
+ else:
+ # first remove the symlink needed when running from subversion
+ self.run('rm /usr/local/lib/python2.5/site-packages/boto')
+ self.run('easy_install %s' % update)
+
+ def fetch_s3_file(self, s3_file):
+ try:
+ if s3_file.startswith('s3:'):
+ bucket_name, key_name = s3_file[len('s3:'):].split('/')
+ c = boto.connect_s3()
+ bucket = c.get_bucket(bucket_name)
+ key = bucket.get_key(key_name)
+ boto.log.info('Fetching %s/%s' % (bucket.name, key.name))
+ path = os.path.join(self.working_dir, key.name)
+ key.get_contents_to_filename(path)
+ except:
+ boto.log.exception('Problem Retrieving file: %s' % s3_file)
+ path = None
+ return path
+
+ def load_packages(self):
+ package_str = boto.config.get('Pyami', 'packages')
+ if package_str:
+ packages = package_str.split(',')
+ for package in packages:
+ package = package.strip()
+ if package.startswith('s3:'):
+ package = self.fetch_s3_file(package)
+ if package:
+ # if the "package" is really a .py file, it doesn't have to
+ # be installed, just being in the working dir is enough
+ if not package.endswith('.py'):
+ self.run('easy_install -Z %s' % package, exit_on_error=False)
+
+ def main(self):
+ self.create_working_dir()
+ self.load_boto()
+ self.load_packages()
+ self.notify('Bootstrap Completed for %s' % boto.config.get_instance('instance-id'))
+
+if __name__ == "__main__":
+ # because bootstrap starts before any logging configuration can be loaded from
+ # the boto config files, we will manually enable logging to /var/log/boto.log
+ boto.set_file_logger('bootstrap', '/var/log/boto.log')
+ bs = Bootstrap()
+ bs.main()
diff --git a/api/boto/pyami/config.py b/api/boto/pyami/config.py
new file mode 100644
index 0000000..831acc4
--- /dev/null
+++ b/api/boto/pyami/config.py
@@ -0,0 +1,204 @@
+# 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 StringIO, os, re
+import ConfigParser
+import boto
+
+BotoConfigPath = '/etc/boto.cfg'
+BotoConfigLocations = [BotoConfigPath]
+if 'HOME' in os.environ:
+ UserConfigPath = os.path.expanduser('~/.boto')
+ BotoConfigLocations.append(UserConfigPath)
+else:
+ UserConfigPath = None
+if 'BOTO_CONFIG' in os.environ:
+ BotoConfigLocations.append(os.path.expanduser(os.environ['BOTO_CONFIG']))
+
+class Config(ConfigParser.SafeConfigParser):
+
+ def __init__(self, path=None, fp=None, do_load=True):
+ ConfigParser.SafeConfigParser.__init__(self, {'working_dir' : '/mnt/pyami',
+ 'debug' : '0'})
+ if do_load:
+ if path:
+ self.load_from_path(path)
+ elif fp:
+ self.readfp(fp)
+ else:
+ self.read(BotoConfigLocations)
+ if "AWS_CREDENTIAL_FILE" in os.environ:
+ self.load_credential_file(os.path.expanduser(os.environ['AWS_CREDENTIAL_FILE']))
+
+ def load_credential_file(self, path):
+ """Load a credential file as is setup like the Java utilities"""
+ config = ConfigParser.ConfigParser()
+ c_data = StringIO.StringIO()
+ c_data.write("[Credentials]\n")
+ for line in open(path, "r").readlines():
+ c_data.write(line.replace("AWSAccessKeyId", "aws_access_key_id").replace("AWSSecretKey", "aws_secret_access_key"))
+ c_data.seek(0)
+ self.readfp(c_data)
+
+ def load_from_path(self, path):
+ file = open(path)
+ for line in file.readlines():
+ match = re.match("^#import[\s\t]*([^\s^\t]*)[\s\t]*$", line)
+ if match:
+ extended_file = match.group(1)
+ (dir, file) = os.path.split(path)
+ self.load_from_path(os.path.join(dir, extended_file))
+ self.read(path)
+
+ def save_option(self, path, section, option, value):
+ """
+ Write the specified Section.Option to the config file specified by path.
+ Replace any previous value. If the path doesn't exist, create it.
+ Also add the option the the in-memory config.
+ """
+ config = ConfigParser.SafeConfigParser()
+ config.read(path)
+ if not config.has_section(section):
+ config.add_section(section)
+ config.set(section, option, value)
+ fp = open(path, 'w')
+ config.write(fp)
+ fp.close()
+ if not self.has_section(section):
+ self.add_section(section)
+ self.set(section, option, value)
+
+ def save_user_option(self, section, option, value):
+ self.save_option(UserConfigPath, section, option, value)
+
+ def save_system_option(self, section, option, value):
+ self.save_option(BotoConfigPath, section, option, value)
+
+ def get_instance(self, name, default=None):
+ try:
+ val = self.get('Instance', name)
+ except:
+ val = default
+ return val
+
+ def get_user(self, name, default=None):
+ try:
+ val = self.get('User', name)
+ except:
+ val = default
+ return val
+
+ def getint_user(self, name, default=0):
+ try:
+ val = self.getint('User', name)
+ except:
+ val = default
+ return val
+
+ def get_value(self, section, name, default=None):
+ return self.get(section, name, default)
+
+ def get(self, section, name, default=None):
+ try:
+ val = ConfigParser.SafeConfigParser.get(self, section, name)
+ except:
+ val = default
+ return val
+
+ def getint(self, section, name, default=0):
+ try:
+ val = ConfigParser.SafeConfigParser.getint(self, section, name)
+ except:
+ val = int(default)
+ return val
+
+ def getfloat(self, section, name, default=0.0):
+ try:
+ val = ConfigParser.SafeConfigParser.getfloat(self, section, name)
+ except:
+ val = float(default)
+ return val
+
+ def getbool(self, section, name, default=False):
+ if self.has_option(section, name):
+ val = self.get(section, name)
+ if val.lower() == 'true':
+ val = True
+ else:
+ val = False
+ else:
+ val = default
+ return val
+
+ def setbool(self, section, name, value):
+ if value:
+ self.set(section, name, 'true')
+ else:
+ self.set(section, name, 'false')
+
+ def dump(self):
+ s = StringIO.StringIO()
+ self.write(s)
+ print s.getvalue()
+
+ def dump_safe(self, fp=None):
+ if not fp:
+ fp = StringIO.StringIO()
+ for section in self.sections():
+ fp.write('[%s]\n' % section)
+ for option in self.options(section):
+ if option == 'aws_secret_access_key':
+ fp.write('%s = xxxxxxxxxxxxxxxxxx\n' % option)
+ else:
+ fp.write('%s = %s\n' % (option, self.get(section, option)))
+
+ def dump_to_sdb(self, domain_name, item_name):
+ import simplejson
+ sdb = boto.connect_sdb()
+ domain = sdb.lookup(domain_name)
+ if not domain:
+ domain = sdb.create_domain(domain_name)
+ item = domain.new_item(item_name)
+ item.active = False
+ for section in self.sections():
+ d = {}
+ for option in self.options(section):
+ d[option] = self.get(section, option)
+ item[section] = simplejson.dumps(d)
+ item.save()
+
+ def load_from_sdb(self, domain_name, item_name):
+ import simplejson
+ sdb = boto.connect_sdb()
+ domain = sdb.lookup(domain_name)
+ item = domain.get_item(item_name)
+ for section in item.keys():
+ if not self.has_section(section):
+ self.add_section(section)
+ d = simplejson.loads(item[section])
+ for attr_name in d.keys():
+ attr_value = d[attr_name]
+ if attr_value == None:
+ attr_value = 'None'
+ if isinstance(attr_value, bool):
+ self.setbool(section, attr_name, attr_value)
+ else:
+ self.set(section, attr_name, attr_value)
diff --git a/api/boto/pyami/copybot.cfg b/api/boto/pyami/copybot.cfg
new file mode 100644
index 0000000..cbfdc5a
--- /dev/null
+++ b/api/boto/pyami/copybot.cfg
@@ -0,0 +1,60 @@
+#
+# Your AWS Credentials
+#
+[Credentials]
+aws_access_key_id =
+aws_secret_access_key =
+
+#
+# If you want to use a separate set of credentials when writing
+# to the destination bucket, put them here
+#dest_aws_access_key_id =
+#dest_aws_secret_access_key =
+
+#
+# Fill out this section if you want emails from CopyBot
+# when it starts and stops
+#
+[Notification]
+#smtp_host =
+#smtp_user =
+#smtp_pass =
+#smtp_from =
+#smtp_to =
+
+#
+# If you leave this section as is, it will automatically
+# update boto from subversion upon start up.
+# If you don't want that to happen, comment this out
+#
+[Boto]
+boto_location = /usr/local/boto
+boto_update = svn:HEAD
+
+#
+# This tells the Pyami code in boto what scripts
+# to run during startup
+#
+[Pyami]
+scripts = boto.pyami.copybot.CopyBot
+
+#
+# Source bucket and Destination Bucket, obviously.
+# If the Destination bucket does not exist, it will
+# attempt to create it.
+# If exit_on_completion is false, the instance
+# will keep running after the copy operation is
+# complete which might be handy for debugging.
+# If copy_acls is false, the ACL's will not be
+# copied with the objects to the new bucket.
+# If replace_dst is false, copybot will not
+# will only store the source file in the dest if
+# that file does not already exist. If it's true
+# it will replace it even if it does exist.
+#
+[CopyBot]
+src_bucket =
+dst_bucket =
+exit_on_completion = true
+copy_acls = true
+replace_dst = true
diff --git a/api/boto/pyami/copybot.py b/api/boto/pyami/copybot.py
new file mode 100644
index 0000000..ed397cb
--- /dev/null
+++ b/api/boto/pyami/copybot.py
@@ -0,0 +1,97 @@
+# 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 boto
+from boto.pyami.scriptbase import ScriptBase
+import os, StringIO
+
+class CopyBot(ScriptBase):
+
+ def __init__(self):
+ ScriptBase.__init__(self)
+ self.wdir = boto.config.get('Pyami', 'working_dir')
+ self.log_file = '%s.log' % self.instance_id
+ self.log_path = os.path.join(self.wdir, self.log_file)
+ boto.set_file_logger(self.name, self.log_path)
+ self.src_name = boto.config.get(self.name, 'src_bucket')
+ self.dst_name = boto.config.get(self.name, 'dst_bucket')
+ self.replace = boto.config.getbool(self.name, 'replace_dst', True)
+ s3 = boto.connect_s3()
+ self.src = s3.lookup(self.src_name)
+ if not self.src:
+ boto.log.error('Source bucket does not exist: %s' % self.src_name)
+ dest_access_key = boto.config.get(self.name, 'dest_aws_access_key_id', None)
+ if dest_access_key:
+ dest_secret_key = boto.config.get(self.name, 'dest_aws_secret_access_key', None)
+ s3 = boto.connect(dest_access_key, dest_secret_key)
+ self.dst = s3.lookup(self.dst_name)
+ if not self.dst:
+ self.dst = s3.create_bucket(self.dst_name)
+
+ def copy_bucket_acl(self):
+ if boto.config.get(self.name, 'copy_acls', True):
+ acl = self.src.get_xml_acl()
+ self.dst.set_xml_acl(acl)
+
+ def copy_key_acl(self, src, dst):
+ if boto.config.get(self.name, 'copy_acls', True):
+ acl = src.get_xml_acl()
+ dst.set_xml_acl(acl)
+
+ def copy_keys(self):
+ boto.log.info('src=%s' % self.src.name)
+ boto.log.info('dst=%s' % self.dst.name)
+ try:
+ for key in self.src:
+ if not self.replace:
+ exists = self.dst.lookup(key.name)
+ if exists:
+ boto.log.info('key=%s already exists in %s, skipping' % (key.name, self.dst.name))
+ continue
+ boto.log.info('copying %d bytes from key=%s' % (key.size, key.name))
+ prefix, base = os.path.split(key.name)
+ path = os.path.join(self.wdir, base)
+ key.get_contents_to_filename(path)
+ new_key = self.dst.new_key(key.name)
+ new_key.set_contents_from_filename(path)
+ self.copy_key_acl(key, new_key)
+ os.unlink(path)
+ except:
+ boto.log.exception('Error copying key: %s' % key.name)
+
+ def copy_log(self):
+ key = self.dst.new_key(self.log_file)
+ key.set_contents_from_filename(self.log_path)
+
+ def main(self):
+ fp = StringIO.StringIO()
+ boto.config.dump_safe(fp)
+ self.notify('%s (%s) Starting' % (self.name, self.instance_id), fp.getvalue())
+ if self.src and self.dst:
+ self.copy_keys()
+ if self.dst:
+ self.copy_log()
+ self.notify('%s (%s) Stopping' % (self.name, self.instance_id),
+ 'Copy Operation Complete')
+ if boto.config.getbool(self.name, 'exit_on_completion', True):
+ ec2 = boto.connect_ec2()
+ ec2.terminate_instances([self.instance_id])
+
diff --git a/api/boto/pyami/helloworld.py b/api/boto/pyami/helloworld.py
new file mode 100644
index 0000000..680873c
--- /dev/null
+++ b/api/boto/pyami/helloworld.py
@@ -0,0 +1,28 @@
+# 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.pyami.scriptbase import ScriptBase
+
+class HelloWorld(ScriptBase):
+
+ def main(self):
+ self.log('Hello World!!!')
+
diff --git a/api/boto/pyami/installers/__init__.py b/api/boto/pyami/installers/__init__.py
new file mode 100644
index 0000000..4dcf2f4
--- /dev/null
+++ b/api/boto/pyami/installers/__init__.py
@@ -0,0 +1,64 @@
+# 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.
+#
+from boto.pyami.scriptbase import ScriptBase
+import os
+
+class Installer(ScriptBase):
+ """
+ Abstract base class for installers
+ """
+
+ def add_cron(self, name, minute, hour, mday, month, wday, who, command, env=None):
+ """
+ Add an entry to the system crontab.
+ """
+ raise NotImplimented()
+
+ def add_init_script(self, file):
+ """
+ Add this file to the init.d directory
+ """
+
+ def add_env(self, key, value):
+ """
+ Add an environemnt variable
+ """
+ raise NotImplimented()
+
+ def stop(self, service_name):
+ """
+ Stop a service.
+ """
+ raise NotImplimented()
+
+ def start(self, service_name):
+ """
+ Start a service.
+ """
+ raise NotImplimented()
+
+ def install(self):
+ """
+ Do whatever is necessary to "install" the package.
+ """
+ raise NotImplimented()
+
diff --git a/api/boto/pyami/installers/ubuntu/__init__.py b/api/boto/pyami/installers/ubuntu/__init__.py
new file mode 100644
index 0000000..60ee658
--- /dev/null
+++ b/api/boto/pyami/installers/ubuntu/__init__.py
@@ -0,0 +1,22 @@
+# 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.
+#
+
diff --git a/api/boto/pyami/installers/ubuntu/apache.py b/api/boto/pyami/installers/ubuntu/apache.py
new file mode 100644
index 0000000..febc2df
--- /dev/null
+++ b/api/boto/pyami/installers/ubuntu/apache.py
@@ -0,0 +1,43 @@
+# Copyright (c) 2008 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.
+#
+from boto.pyami.installers.ubuntu.installer import Installer
+
+class Apache(Installer):
+ """
+ Install apache2, mod_python, and libapache2-svn
+ """
+
+ def install(self):
+ self.run("apt-get update")
+ self.run('apt-get -y install apache2', notify=True, exit_on_error=True)
+ self.run('apt-get -y install libapache2-mod-python', notify=True, exit_on_error=True)
+ self.run('a2enmod rewrite', notify=True, exit_on_error=True)
+ self.run('a2enmod ssl', notify=True, exit_on_error=True)
+ self.run('a2enmod proxy', notify=True, exit_on_error=True)
+ self.run('a2enmod proxy_ajp', notify=True, exit_on_error=True)
+
+ # Hard reboot the apache2 server to enable these module
+ self.stop("apache2")
+ self.start("apache2")
+
+ def main(self):
+ self.install()
diff --git a/api/boto/pyami/installers/ubuntu/ebs.py b/api/boto/pyami/installers/ubuntu/ebs.py
new file mode 100644
index 0000000..2cf0f22
--- /dev/null
+++ b/api/boto/pyami/installers/ubuntu/ebs.py
@@ -0,0 +1,203 @@
+# 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.
+#
+"""
+Automated installer to attach, format and mount an EBS volume.
+This installer assumes that you want the volume formatted as
+an XFS file system. To drive this installer, you need the
+following section in the boto config passed to the new instance.
+You also need to install dateutil by listing python-dateutil
+in the list of packages to be installed in the Pyami seciont
+of your boto config file.
+
+If there is already a device mounted at the specified mount point,
+the installer assumes that it is the ephemeral drive and unmounts
+it, remounts it as /tmp and chmods it to 777.
+
+Config file section::
+
+ [EBS]
+ volume_id =
+ logical_volume_name =
+ device =
+ mount_point =
+
+"""
+import boto
+from boto.manage.volume import Volume
+import os, time
+from boto.pyami.installers.ubuntu.installer import Installer
+from string import Template
+
+BackupScriptTemplate = """#!/usr/bin/env python
+# Backup EBS volume
+import boto
+from boto.pyami.scriptbase import ScriptBase
+import traceback
+
+class Backup(ScriptBase):
+
+ def main(self):
+ try:
+ ec2 = boto.connect_ec2()
+ self.run("/usr/sbin/xfs_freeze -f ${mount_point}")
+ snapshot = ec2.create_snapshot('${volume_id}')
+ boto.log.info("Snapshot created: %s " % snapshot)
+ except Exception, e:
+ self.notify(subject="${instance_id} Backup Failed", body=traceback.format_exc())
+ boto.log.info("Snapshot created: ${volume_id}")
+ except Exception, e:
+ self.notify(subject="${instance_id} Backup Failed", body=traceback.format_exc())
+ finally:
+ self.run("/usr/sbin/xfs_freeze -u ${mount_point}")
+
+if __name__ == "__main__":
+ b = Backup()
+ b.main()
+"""
+
+BackupCleanupScript= """#!/usr/bin/env python
+# Cleans Backups of EBS volumes
+
+for v in Volume.all():
+ v.trim_snapshot(True)
+"""
+
+class EBSInstaller(Installer):
+ """
+ Set up the EBS stuff
+ """
+
+ def __init__(self, config_file=None):
+ Installer.__init__(self, config_file)
+ self.instance_id = boto.config.get('Instance', 'instance-id')
+ self.device = boto.config.get('EBS', 'device', '/dev/sdp')
+ self.volume_id = boto.config.get('EBS', 'volume_id')
+ self.logical_volume_name = boto.config.get('EBS', 'logical_volume_name')
+ self.mount_point = boto.config.get('EBS', 'mount_point', '/ebs')
+
+ def attach(self):
+ ec2 = boto.connect_ec2()
+ if self.logical_volume_name:
+ # if a logical volume was specified, override the specified volume_id
+ # (if there was one) with the current AWS volume for the logical volume:
+ logical_volume = Volume.find(name = self.logical_volume_name).next()
+ self.volume_id = logical_volume._volume_id
+ volume = ec2.get_all_volumes([self.volume_id])[0]
+ # wait for the volume to be available. The volume may still be being created
+ # from a snapshot.
+ while volume.update() != 'available':
+ boto.log.info('Volume %s not yet available. Current status = %s.' % (volume.id, volume.status))
+ time.sleep(5)
+ ec2.attach_volume(self.volume_id, self.instance_id, self.device)
+ # now wait for the volume device to appear
+ while not os.path.exists(self.device):
+ boto.log.info('%s still does not exist, waiting 10 seconds' % self.device)
+ time.sleep(10)
+
+ def make_fs(self):
+ boto.log.info('make_fs...')
+ has_fs = self.run('fsck %s' % self.device)
+ if has_fs != 0:
+ self.run('mkfs -t xfs %s' % self.device)
+
+ def create_backup_script(self):
+ t = Template(BackupScriptTemplate)
+ s = t.substitute(volume_id=self.volume_id, instance_id=self.instance_id,
+ mount_point=self.mount_point)
+ fp = open('/usr/local/bin/ebs_backup', 'w')
+ fp.write(s)
+ fp.close()
+ self.run('chmod +x /usr/local/bin/ebs_backup')
+
+ def create_backup_cleanup_script(self):
+ fp = open('/usr/local/bin/ebs_backup_cleanup', 'w')
+ fp.write(BackupCleanupScript)
+ fp.close()
+ self.run('chmod +x /usr/local/bin/ebs_backup_cleanup')
+
+ def handle_mount_point(self):
+ boto.log.info('handle_mount_point')
+ if not os.path.isdir(self.mount_point):
+ boto.log.info('making directory')
+ # mount directory doesn't exist so create it
+ self.run("mkdir %s" % self.mount_point)
+ else:
+ boto.log.info('directory exists already')
+ self.run('mount -l')
+ lines = self.last_command.output.split('\n')
+ for line in lines:
+ t = line.split()
+ if t and t[2] == self.mount_point:
+ # something is already mounted at the mount point
+ # unmount that and mount it as /tmp
+ if t[0] != self.device:
+ self.run('umount %s' % self.mount_point)
+ self.run('mount %s /tmp' % t[0])
+ self.run('chmod 777 /tmp')
+ break
+ # Mount up our new EBS volume onto mount_point
+ self.run("mount %s %s" % (self.device, self.mount_point))
+ self.run('xfs_growfs %s' % self.mount_point)
+
+ def update_fstab(self):
+ f = open("/etc/fstab", "a")
+ f.write('%s\t%s\txfs\tdefaults 0 0\n' % (self.mount_point, self.device))
+ f.close()
+
+ def install(self):
+ # First, find and attach the volume
+ self.attach()
+
+ # Install the xfs tools
+ self.run('apt-get -y install xfsprogs xfsdump')
+
+ # Check to see if the filesystem was created or not
+ self.make_fs()
+
+ # create the /ebs directory for mounting
+ self.handle_mount_point()
+
+ # create the backup script
+ self.create_backup_script()
+
+ # Set up the backup script
+ minute = boto.config.get('EBS', 'backup_cron_minute', '0')
+ hour = boto.config.get('EBS', 'backup_cron_hour', '4,16')
+ self.add_cron("ebs_backup", "/usr/local/bin/ebs_backup", minute=minute, hour=hour)
+
+ # Set up the backup cleanup script
+ minute = boto.config.get('EBS', 'backup_cleanup_cron_minute')
+ hour = boto.config.get('EBS', 'backup_cleanup_cron_hour')
+ if (minute != None) and (hour != None):
+ self.create_backup_cleanup_script();
+ self.add_cron("ebs_backup_cleanup", "/usr/local/bin/ebs_backup_cleanup", minute=minute, hour=hour)
+
+ # Set up the fstab
+ self.update_fstab()
+
+ def main(self):
+ if not os.path.exists(self.device):
+ self.install()
+ else:
+ boto.log.info("Device %s is already attached, skipping EBS Installer" % self.device)
diff --git a/api/boto/pyami/installers/ubuntu/installer.py b/api/boto/pyami/installers/ubuntu/installer.py
new file mode 100644
index 0000000..0169950
--- /dev/null
+++ b/api/boto/pyami/installers/ubuntu/installer.py
@@ -0,0 +1,96 @@
+# 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 boto.pyami.installers
+import os
+import os.path
+import stat
+import boto
+import random
+from pwd import getpwnam
+
+class Installer(boto.pyami.installers.Installer):
+ """
+ Base Installer class for Ubuntu-based AMI's
+ """
+ def add_cron(self, name, command, minute="*", hour="*", mday="*", month="*", wday="*", who="root", env=None):
+ """
+ Write a file to /etc/cron.d to schedule a command
+ env is a dict containing environment variables you want to set in the file
+ name will be used as the name of the file
+ """
+ if minute == 'random':
+ minute = str(random.randrange(60))
+ if hour == 'random':
+ hour = str(random.randrange(24))
+ fp = open('/etc/cron.d/%s' % name, "w")
+ if env:
+ for key, value in env.items():
+ fp.write('%s=%s\n' % (key, value))
+ fp.write('%s %s %s %s %s %s %s\n' % (minute, hour, mday, month, wday, who, command))
+ fp.close()
+
+ def add_init_script(self, file, name):
+ """
+ Add this file to the init.d directory
+ """
+ f_path = os.path.join("/etc/init.d", name)
+ f = open(f_path, "w")
+ f.write(file)
+ f.close()
+ os.chmod(f_path, stat.S_IREAD| stat.S_IWRITE | stat.S_IEXEC)
+ self.run("/usr/sbin/update-rc.d %s defaults" % name)
+
+ def add_env(self, key, value):
+ """
+ Add an environemnt variable
+ For Ubuntu, the best place is /etc/environment. Values placed here do
+ not need to be exported.
+ """
+ boto.log.info('Adding env variable: %s=%s' % (key, value))
+ if not os.path.exists("/etc/environment.orig"):
+ self.run('cp /etc/environment /etc/environment.orig', notify=False, exit_on_error=False)
+ fp = open('/etc/environment', 'a')
+ fp.write('\n%s="%s"' % (key, value))
+ fp.close()
+ os.environ[key] = value
+
+ def stop(self, service_name):
+ self.run('/etc/init.d/%s stop' % service_name)
+
+ def start(self, service_name):
+ self.run('/etc/init.d/%s start' % service_name)
+
+ def create_user(self, user):
+ """
+ Create a user on the local system
+ """
+ self.run("useradd -m %s" % user)
+ usr = getpwnam(user)
+ return usr
+
+
+ def install(self):
+ """
+ This is the only method you need to override
+ """
+ raise NotImplimented()
+
diff --git a/api/boto/pyami/installers/ubuntu/mysql.py b/api/boto/pyami/installers/ubuntu/mysql.py
new file mode 100644
index 0000000..490e5db
--- /dev/null
+++ b/api/boto/pyami/installers/ubuntu/mysql.py
@@ -0,0 +1,109 @@
+# 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 installer will install mysql-server on an Ubuntu machine.
+In addition to the normal installation done by apt-get, it will
+also configure the new MySQL server to store it's data files in
+a different location. By default, this is /mnt but that can be
+configured in the [MySQL] section of the boto config file passed
+to the instance.
+"""
+from boto.pyami.installers.ubuntu.installer import Installer
+import os
+import boto
+from boto.utils import ShellCommand
+from ConfigParser import SafeConfigParser
+import time
+
+ConfigSection = """
+[MySQL]
+root_password =
+data_dir =
+"""
+
+class MySQL(Installer):
+
+ def install(self):
+ self.run('apt-get update')
+ self.run('apt-get -y install mysql-server', notify=True, exit_on_error=True)
+
+# def set_root_password(self, password=None):
+# if not password:
+# password = boto.config.get('MySQL', 'root_password')
+# if password:
+# self.run('mysqladmin -u root password %s' % password)
+# return password
+
+ def change_data_dir(self, password=None):
+ data_dir = boto.config.get('MySQL', 'data_dir', '/mnt')
+ fresh_install = False;
+ is_mysql_running_command = ShellCommand('mysqladmin ping') # exit status 0 if mysql is running
+ is_mysql_running_command.run()
+ if is_mysql_running_command.getStatus() == 0:
+ # mysql is running. This is the state apt-get will leave it in. If it isn't running,
+ # that means mysql was already installed on the AMI and there's no need to stop it,
+ # saving 40 seconds on instance startup.
+ time.sleep(10) #trying to stop mysql immediately after installing it fails
+ # We need to wait until mysql creates the root account before we kill it
+ # or bad things will happen
+ i = 0
+ while self.run("echo 'quit' | mysql -u root") != 0 and i<5:
+ time.sleep(5)
+ i = i + 1
+ self.run('/etc/init.d/mysql stop')
+ self.run("pkill -9 mysql")
+
+ mysql_path = os.path.join(data_dir, 'mysql')
+ if not os.path.exists(mysql_path):
+ self.run('mkdir %s' % mysql_path)
+ fresh_install = True;
+ self.run('chown -R mysql:mysql %s' % mysql_path)
+ fp = open('/etc/mysql/conf.d/use_mnt.cnf', 'w')
+ fp.write('# created by pyami\n')
+ fp.write('# use the %s volume for data\n' % data_dir)
+ fp.write('[mysqld]\n')
+ fp.write('datadir = %s\n' % mysql_path)
+ fp.write('log_bin = %s\n' % os.path.join(mysql_path, 'mysql-bin.log'))
+ fp.close()
+ if fresh_install:
+ self.run('cp -pr /var/lib/mysql/* %s/' % mysql_path)
+ self.start('mysql')
+ else:
+ #get the password ubuntu expects to use:
+ config_parser = SafeConfigParser()
+ config_parser.read('/etc/mysql/debian.cnf')
+ password = config_parser.get('client', 'password')
+ # start the mysql deamon, then mysql with the required grant statement piped into it:
+ self.start('mysql')
+ time.sleep(10) #time for mysql to start
+ grant_command = "echo \"GRANT ALL PRIVILEGES ON *.* TO 'debian-sys-maint'@'localhost' IDENTIFIED BY '%s' WITH GRANT OPTION;\" | mysql" % password
+ while self.run(grant_command) != 0:
+ time.sleep(5)
+ # leave mysqld running
+
+ def main(self):
+ self.install()
+ # change_data_dir runs 'mysql -u root' which assumes there is no mysql password, i
+ # and changing that is too ugly to be worth it:
+ #self.set_root_password()
+ self.change_data_dir()
+
diff --git a/api/boto/pyami/installers/ubuntu/trac.py b/api/boto/pyami/installers/ubuntu/trac.py
new file mode 100644
index 0000000..c97ddd2
--- /dev/null
+++ b/api/boto/pyami/installers/ubuntu/trac.py
@@ -0,0 +1,137 @@
+# Copyright (c) 2008 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.
+#
+from boto.pyami.installers.ubuntu.installer import Installer
+import boto
+import os
+
+class Trac(Installer):
+ """
+ Install Trac and DAV-SVN
+ Sets up a Vhost pointing to [Trac]->home
+ Using the config parameter [Trac]->hostname
+ Sets up a trac environment for every directory found under [Trac]->data_dir
+
+ [Trac]
+ name = My Foo Server
+ hostname = trac.foo.com
+ home = /mnt/sites/trac
+ data_dir = /mnt/trac
+ svn_dir = /mnt/subversion
+ server_admin = root@foo.com
+ sdb_auth_domain = users
+ # Optional
+ SSLCertificateFile = /mnt/ssl/foo.crt
+ SSLCertificateKeyFile = /mnt/ssl/foo.key
+ SSLCertificateChainFile = /mnt/ssl/FooCA.crt
+
+ """
+
+ def install(self):
+ self.run('apt-get -y install trac', notify=True, exit_on_error=True)
+ self.run('apt-get -y install libapache2-svn', notify=True, exit_on_error=True)
+ self.run("a2enmod ssl")
+ self.run("a2enmod python")
+ self.run("a2enmod dav_svn")
+ self.run("a2enmod rewrite")
+
+ def setup_vhost(self):
+ domain = boto.config.get("Trac", "hostname").strip()
+ if domain:
+ cnf = open("/etc/apache2/sites-available/%s" % domain, "w")
+ cnf.write("NameVirtualHost *:80\n")
+ if boto.config.get("Trac", "SSLCertificateFile"):
+ cnf.write("NameVirtualHost *:443\n\n")
+ cnf.write("\n")
+ cnf.write("\tServerAdmin %s\n" % boto.config.get("Trac", "server_admin").strip())
+ cnf.write("\tServerName %s\n" % domain)
+ cnf.write("\tRewriteEngine On\n")
+ cnf.write("\tRewriteRule ^(.*)$ https://%s$1\n" % domain)
+ cnf.write("\n\n")
+
+ cnf.write("\n")
+ else:
+ cnf.write("\n")
+
+ cnf.write("\tServerAdmin %s\n" % boto.config.get("Trac", "server_admin").strip())
+ cnf.write("\tServerName %s\n" % domain)
+ cnf.write("\tDocumentRoot %s\n" % boto.config.get("Trac", "home").strip())
+
+ cnf.write("\t\n" % boto.config.get("Trac", "home").strip())
+ cnf.write("\t\tOptions FollowSymLinks Indexes MultiViews\n")
+ cnf.write("\t\tAllowOverride All\n")
+ cnf.write("\t\tOrder allow,deny\n")
+ cnf.write("\t\tallow from all\n")
+ cnf.write("\t\n")
+
+ cnf.write("\t\n")
+ cnf.write("\t\tAuthType Basic\n")
+ cnf.write("\t\tAuthName \"%s\"\n" % boto.config.get("Trac", "name"))
+ cnf.write("\t\tRequire valid-user\n")
+ cnf.write("\t\tAuthBasicAuthoritative off\n")
+ cnf.write("\t\tAuthUserFile /dev/null\n")
+ cnf.write("\t\tPythonAuthenHandler marajo.web.authen_handler\n")
+ cnf.write("\t\tPythonOption SDBDomain %s\n" % boto.config.get("Trac", "sdb_auth_domain"))
+ cnf.write("\t\n")
+
+ data_dir = boto.config.get("Trac", "data_dir")
+ for env in os.listdir(data_dir):
+ if(env[0] != "."):
+ cnf.write("\t\n" % env)
+ cnf.write("\t\tSetHandler mod_python\n")
+ cnf.write("\t\tPythonInterpreter main_interpreter\n")
+ cnf.write("\t\tPythonHandler trac.web.modpython_frontend\n")
+ cnf.write("\t\tPythonOption TracEnv %s/%s\n" % (data_dir, env))
+ cnf.write("\t\tPythonOption TracUriRoot /trac%s\n" % env)
+ cnf.write("\t\n")
+
+ svn_dir = boto.config.get("Trac", "svn_dir")
+ for env in os.listdir(svn_dir):
+ if(env[0] != "."):
+ cnf.write("\t\n" % env)
+ cnf.write("\t\tDAV svn\n")
+ cnf.write("\t\tSVNPath %s/%s\n" % (svn_dir, env))
+ cnf.write("\t\n")
+
+ cnf.write("\tErrorLog /var/log/apache2/error.log\n")
+ cnf.write("\tLogLevel warn\n")
+ cnf.write("\tCustomLog /var/log/apache2/access.log combined\n")
+ cnf.write("\tServerSignature On\n")
+ SSLCertificateFile = boto.config.get("Trac", "SSLCertificateFile")
+ if SSLCertificateFile:
+ cnf.write("\tSSLEngine On\n")
+ cnf.write("\tSSLCertificateFile %s\n" % SSLCertificateFile)
+
+ SSLCertificateKeyFile = boto.config.get("Trac", "SSLCertificateKeyFile")
+ if SSLCertificateKeyFile:
+ cnf.write("\tSSLCertificateKeyFile %s\n" % SSLCertificateKeyFile)
+
+ SSLCertificateChainFile = boto.config.get("Trac", "SSLCertificateChainFile")
+ if SSLCertificateChainFile:
+ cnf.write("\tSSLCertificateChainFile %s\n" % SSLCertificateChainFile)
+ cnf.write("\n")
+ cnf.close()
+ self.run("a2ensite %s" % domain)
+ self.run("/etc/init.d/apache2 force-reload")
+
+ def main(self):
+ self.install()
+ self.setup_vhost()
diff --git a/api/boto/pyami/launch_ami.py b/api/boto/pyami/launch_ami.py
new file mode 100755
index 0000000..c49c2a3
--- /dev/null
+++ b/api/boto/pyami/launch_ami.py
@@ -0,0 +1,176 @@
+#!/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, imp, time
+import boto
+from boto.utils import get_instance_userdata
+
+usage_string = """
+SYNOPSIS
+ launch_ami.py -a ami_id [-b script_bucket] [-s script_name]
+ [-m module] [-c class_name] [-r]
+ [-g group] [-k key_name] [-n num_instances]
+ [-w] [extra_data]
+ Where:
+ ami_id - the id of the AMI you wish to launch
+ module - The name of the Python module containing the class you
+ want to run when the instance is started. If you use this
+ option the Python module must already be stored on the
+ instance in a location that is on the Python path.
+ script_file - The name of a local Python module that you would like
+ to have copied to S3 and then run on the instance
+ when it is started. The specified module must be
+ import'able (i.e. in your local Python path). It
+ will then be copied to the specified bucket in S3
+ (see the -b option). Once the new instance(s)
+ start up the script will be copied from S3 and then
+ run locally on the instance.
+ class_name - The name of the class to be instantiated within the
+ module or script file specified.
+ script_bucket - the name of the bucket in which the script will be
+ stored
+ group - the name of the security group the instance will run in
+ key_name - the name of the keypair to use when launching the AMI
+ num_instances - how many instances of the AMI to launch (default 1)
+ input_queue_name - Name of SQS to read input messages from
+ output_queue_name - Name of SQS to write output messages to
+ extra_data - additional name-value pairs that will be passed as
+ userdata to the newly launched instance. These should
+ be of the form "name=value"
+ The -r option reloads the Python module to S3 without launching
+ another instance. This can be useful during debugging to allow
+ you to test a new version of your script without shutting down
+ your instance and starting up another one.
+ The -w option tells the script to run synchronously, meaning to
+ wait until the instance is actually up and running. It then prints
+ the IP address and internal and external DNS names before exiting.
+"""
+
+def usage():
+ print usage_string
+ sys.exit()
+
+def main():
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'a:b:c:g:hi:k:m:n:o:rs:w',
+ ['ami', 'bucket', 'class', 'group', 'help',
+ 'inputqueue', 'keypair', 'module',
+ 'numinstances', 'outputqueue',
+ 'reload', 'script_name', 'wait'])
+ except:
+ usage()
+ params = {'module_name' : None,
+ 'script_name' : None,
+ 'class_name' : None,
+ 'script_bucket' : None,
+ 'group' : 'default',
+ 'keypair' : None,
+ 'ami' : None,
+ 'num_instances' : 1,
+ 'input_queue_name' : None,
+ 'output_queue_name' : None}
+ reload = None
+ wait = None
+ for o, a in opts:
+ if o in ('-a', '--ami'):
+ params['ami'] = a
+ if o in ('-b', '--bucket'):
+ params['script_bucket'] = a
+ if o in ('-c', '--class'):
+ params['class_name'] = a
+ if o in ('-g', '--group'):
+ params['group'] = a
+ if o in ('-h', '--help'):
+ usage()
+ if o in ('-i', '--inputqueue'):
+ params['input_queue_name'] = a
+ if o in ('-k', '--keypair'):
+ params['keypair'] = a
+ if o in ('-m', '--module'):
+ params['module_name'] = a
+ if o in ('-n', '--num_instances'):
+ params['num_instances'] = int(a)
+ if o in ('-o', '--outputqueue'):
+ params['output_queue_name'] = a
+ if o in ('-r', '--reload'):
+ reload = True
+ if o in ('-s', '--script'):
+ params['script_name'] = a
+ if o in ('-w', '--wait'):
+ wait = True
+
+ # check required fields
+ required = ['ami']
+ for pname in required:
+ if not params.get(pname, None):
+ print '%s is required' % pname
+ usage()
+ if params['script_name']:
+ # first copy the desired module file to S3 bucket
+ if reload:
+ print 'Reloading module %s to S3' % params['script_name']
+ else:
+ print 'Copying module %s to S3' % params['script_name']
+ l = imp.find_module(params['script_name'])
+ c = boto.connect_s3()
+ bucket = c.get_bucket(params['script_bucket'])
+ key = bucket.new_key(params['script_name']+'.py')
+ key.set_contents_from_file(l[0])
+ params['script_md5'] = key.md5
+ # we have everything we need, now build userdata string
+ l = []
+ for k, v in params.items():
+ if v:
+ l.append('%s=%s' % (k, v))
+ c = boto.connect_ec2()
+ l.append('aws_access_key_id=%s' % c.aws_access_key_id)
+ l.append('aws_secret_access_key=%s' % c.aws_secret_access_key)
+ for kv in args:
+ l.append(kv)
+ s = '|'.join(l)
+ if not reload:
+ rs = c.get_all_images([params['ami']])
+ img = rs[0]
+ r = img.run(user_data=s, key_name=params['keypair'],
+ security_groups=[params['group']],
+ max_count=params.get('num_instances', 1))
+ print 'AMI: %s - %s (Started)' % (params['ami'], img.location)
+ print 'Reservation %s contains the following instances:' % r.id
+ for i in r.instances:
+ print '\t%s' % i.id
+ if wait:
+ running = False
+ while not running:
+ time.sleep(30)
+ [i.update() for i in r.instances]
+ status = [i.state for i in r.instances]
+ print status
+ if status.count('running') == len(r.instances):
+ running = True
+ for i in r.instances:
+ print 'Instance: %s' % i.ami_launch_index
+ print 'Public DNS Name: %s' % i.public_dns_name
+ print 'Private DNS Name: %s' % i.private_dns_name
+
+if __name__ == "__main__":
+ main()
+
diff --git a/api/boto/pyami/scriptbase.py b/api/boto/pyami/scriptbase.py
new file mode 100644
index 0000000..6fe92aa
--- /dev/null
+++ b/api/boto/pyami/scriptbase.py
@@ -0,0 +1,44 @@
+import os, sys, time, traceback
+import smtplib
+from boto.utils import ShellCommand, get_ts
+import boto
+import boto.utils
+
+class ScriptBase:
+
+ def __init__(self, config_file=None):
+ self.instance_id = boto.config.get('Instance', 'instance-id', 'default')
+ self.name = self.__class__.__name__
+ self.ts = get_ts()
+ if config_file:
+ boto.config.read(config_file)
+
+ def notify(self, subject, body=''):
+ boto.utils.notify(subject, body)
+
+ def mkdir(self, path):
+ if not os.path.isdir(path):
+ try:
+ os.mkdir(path)
+ except:
+ boto.log.error('Error creating directory: %s' % path)
+
+ def umount(self, path):
+ if os.path.ismount(path):
+ self.run('umount %s' % path)
+
+ def run(self, command, notify=True, exit_on_error=False):
+ self.last_command = ShellCommand(command)
+ if self.last_command.status != 0:
+ boto.log.error('Error running command: "%s". Output: "%s"' % (command, self.last_command.output))
+ if notify:
+ self.notify('Error encountered', \
+ 'Error running the following command:\n\t%s\n\nCommand output:\n\t%s' % \
+ (command, self.last_command.output))
+ if exit_on_error:
+ sys.exit(-1)
+ return self.last_command.status
+
+ def main(self):
+ pass
+
diff --git a/api/boto/pyami/startup.py b/api/boto/pyami/startup.py
new file mode 100644
index 0000000..d6f1376
--- /dev/null
+++ b/api/boto/pyami/startup.py
@@ -0,0 +1,59 @@
+# 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 os, sys, traceback, StringIO
+import boto
+from boto.utils import find_class
+from boto import config
+from boto.pyami.scriptbase import ScriptBase
+from boto.utils import find_class
+
+class Startup(ScriptBase):
+
+ def run_scripts(self):
+ scripts = config.get('Pyami', 'scripts')
+ if scripts:
+ for script in scripts.split(','):
+ script = script.strip(" ")
+ try:
+ pos = script.rfind('.')
+ if pos > 0:
+ mod_name = script[0:pos]
+ cls_name = script[pos+1:]
+ cls = find_class(mod_name, cls_name)
+ boto.log.info('Running Script: %s' % script)
+ s = cls()
+ s.main()
+ else:
+ boto.log.warning('Trouble parsing script: %s' % script)
+ except Exception, e:
+ boto.log.exception('Problem Running Script: %s' % script)
+
+ def main(self):
+ self.run_scripts()
+ self.notify('Startup Completed for %s' % config.get('Instance', 'instance-id'))
+
+if __name__ == "__main__":
+ if not config.has_section('loggers'):
+ boto.set_file_logger('startup', '/var/log/boto.log')
+ sys.path.append(config.get('Pyami', 'working_dir'))
+ su = Startup()
+ su.main()
diff --git a/api/boto/rds/__init__.py b/api/boto/rds/__init__.py
new file mode 100644
index 0000000..92b7199
--- /dev/null
+++ b/api/boto/rds/__init__.py
@@ -0,0 +1,813 @@
+# 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.
+#
+
+import xml.sax
+import base64
+import time
+import boto
+import boto.utils
+import urllib
+from boto.connection import AWSQueryConnection
+from boto import handler
+from boto.resultset import ResultSet
+from boto.rds.dbinstance import DBInstance
+from boto.rds.dbsecuritygroup import DBSecurityGroup
+from boto.rds.parametergroup import ParameterGroup
+from boto.rds.dbsnapshot import DBSnapshot
+from boto.rds.event import Event
+
+#boto.set_stream_logger('rds')
+
+class RDSConnection(AWSQueryConnection):
+
+ DefaultHost = 'rds.amazonaws.com'
+ APIVersion = '2009-10-16'
+ SignatureVersion = '2'
+
+ def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
+ is_secure=True, port=None, proxy=None, proxy_port=None,
+ proxy_user=None, proxy_pass=None, host=DefaultHost, 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, self.DefaultHost, debug,
+ https_connection_factory, path)
+
+ # DB Instance methods
+
+ def get_all_dbinstances(self, instance_id=None, max_records=None,
+ marker=None):
+ """
+ Retrieve all the DBInstances in your account.
+
+ :type instance_id: str
+ :param instance_id: DB Instance identifier. If supplied, only information
+ this instance will be returned. Otherwise, info
+ about all DB Instances will be returned.
+
+ :type max_records: int
+ :param max_records: The maximum number of records to be returned.
+ If more results are available, a MoreToken will
+ be returned in the response that can be used to
+ retrieve additional records. Default is 100.
+
+ :type marker: str
+ :param marker: The marker provided by a previous request.
+
+ :rtype: list
+ :return: A list of :class:`boto.rds.dbinstance.DBInstance`
+ """
+ params = {}
+ if instance_id:
+ params['DBInstanceIdentifier'] = instance_id
+ if max_records:
+ params['MaxRecords'] = max_records
+ if marker:
+ params['Marker'] = marker
+ return self.get_list('DescribeDBInstances', params, [('DBInstance', DBInstance)])
+
+ def create_dbinstance(self, id, allocated_storage, instance_class,
+ master_username, master_password, port=3306,
+ engine='MySQL5.1', db_name=None, param_group=None,
+ security_groups=None, availability_zone=None,
+ preferred_maintenance_window=None,
+ backup_retention_period=None,
+ preferred_backup_window=None):
+ """
+ Create a new DBInstance.
+
+ :type id: str
+ :param id: Unique identifier for the new instance.
+ Must contain 1-63 alphanumeric characters.
+ First character must be a letter.
+ May not end with a hyphen or contain two consecutive hyphens
+
+ :type allocated_storage: int
+ :param allocated_storage: Initially allocated storage size, in GBs.
+ Valid values are [5-1024]
+
+ :type instance_class: str
+ :param instance_class: The compute and memory capacity of the DBInstance.
+ Valid values are:
+ db.m1.small | db.m1.large | db.m1.xlarge |
+ db.m2.2xlarge | db.m2.4xlarge
+
+ :type engine: str
+. :param engine: Name of database engine. Must be MySQL5.1 for now.
+
+ :type master_username: str
+ :param master_username: Name of master user for the DBInstance.
+ Must be 1-15 alphanumeric characters, first must be
+ a letter.
+
+ :type master_password: str
+ :param master_password: Password of master user for the DBInstance.
+ Must be 4-16 alphanumeric characters.
+
+ :type port: int
+ :param port: Port number on which database accepts connections.
+ Valid values [1115-65535]. Defaults to 3306.
+
+ :type db_name: str
+ :param db_name: Name of a database to create when the DBInstance
+ is created. Default is to create no databases.
+
+ :type param_group: str
+ :param param_group: Name of DBParameterGroup to associate with
+ this DBInstance. If no groups are specified
+ no parameter groups will be used.
+
+ :type security_groups: list of str or list of DBSecurityGroup objects
+ :param security_groups: List of names of DBSecurityGroup to authorize on
+ this DBInstance.
+
+ :type availability_zone: str
+ :param availability_zone: Name of the availability zone to place
+ DBInstance into.
+
+ :type preferred_maintenance_window: str
+ :param preferred_maintenance_window: The weekly time range (in UTC) during
+ which maintenance can occur.
+ Default is Sun:05:00-Sun:09:00
+
+ :type backup_retention_period: int
+ :param backup_retention_period: The number of days for which automated
+ backups are retained. Setting this to
+ zero disables automated backups.
+
+ :type preferred_backup_window: str
+ :param preferred_backup_window: The daily time range during which
+ automated backups are created (if
+ enabled). Must be in h24:mi-hh24:mi
+ format (UTC).
+
+ :rtype: :class:`boto.rds.dbinstance.DBInstance`
+ :return: The new db instance.
+ """
+ params = {'DBInstanceIdentifier' : id,
+ 'AllocatedStorage' : allocated_storage,
+ 'DBInstanceClass' : instance_class,
+ 'Engine' : engine,
+ 'MasterUsername' : master_username,
+ 'MasterUserPassword' : master_password}
+ if port:
+ params['Port'] = port
+ if db_name:
+ params['DBName'] = db_name
+ if param_group:
+ params['DBParameterGroup'] = param_group
+ if security_groups:
+ l = []
+ for group in security_groups:
+ if isinstance(group, DBSecurityGroup):
+ l.append(group.name)
+ else:
+ l.append(group)
+ self.build_list_params(params, l, 'DBSecurityGroups.member')
+ if availability_zone:
+ params['AvailabilityZone'] = availability_zone
+ if preferred_maintenance_window:
+ params['PreferredMaintenanceWindow'] = preferred_maintenance_window
+ if backup_retention_period:
+ params['BackupRetentionPeriod'] = backup_retention_period
+ if preferred_backup_window:
+ params['PreferredBackupWindow'] = preferred_backup_window
+
+ return self.get_object('CreateDBInstance', params, DBInstance)
+
+ def modify_dbinstance(self, id, param_group=None, security_groups=None,
+ preferred_maintenance_window=None,
+ master_password=None, allocated_storage=None,
+ backup_retention_period=None,
+ preferred_backup_window=None,
+ apply_immediately=False):
+ """
+ Modify an existing DBInstance.
+
+ :type id: str
+ :param id: Unique identifier for the new instance.
+
+ :type security_groups: list of str or list of DBSecurityGroup objects
+ :param security_groups: List of names of DBSecurityGroup to authorize on
+ this DBInstance.
+
+ :type preferred_maintenance_window: str
+ :param preferred_maintenance_window: The weekly time range (in UTC) during
+ which maintenance can occur.
+ Default is Sun:05:00-Sun:09:00
+
+ :type master_password: str
+ :param master_password: Password of master user for the DBInstance.
+ Must be 4-15 alphanumeric characters.
+
+ :type allocated_storage: int
+ :param allocated_storage: The new allocated storage size, in GBs.
+ Valid values are [5-1024]
+
+ :type instance_class: str
+ :param instance_class: The compute and memory capacity of the DBInstance.
+ Changes will be applied at next maintenance
+ window unless apply_immediately is True.
+ Valid values are:
+ db.m1.small | db.m1.large | db.m1.xlarge |
+ db.m2.2xlarge | db.m2.4xlarge
+
+ :type apply_immediately: bool
+ :param apply_immediately: If true, the modifications will be applied
+ as soon as possible rather than waiting for
+ the next preferred maintenance window.
+
+ :type backup_retention_period: int
+ :param backup_retention_period: The number of days for which automated
+ backups are retained. Setting this to
+ zero disables automated backups.
+
+ :type preferred_backup_window: str
+ :param preferred_backup_window: The daily time range during which
+ automated backups are created (if
+ enabled). Must be in h24:mi-hh24:mi
+ format (UTC).
+
+ :rtype: :class:`boto.rds.dbinstance.DBInstance`
+ :return: The modified db instance.
+ """
+ params = {'DBInstanceIdentifier' : id}
+ if param_group:
+ params['DBParameterGroupName'] = param_group
+ if security_groups:
+ l = []
+ for group in security_groups:
+ if isinstance(group, DBSecurityGroup):
+ l.append(group.name)
+ else:
+ l.append(group)
+ self.build_list_params(params, l, 'DBSecurityGroups.member')
+ if preferred_maintenance_window:
+ params['PreferredMaintenanceWindow'] = preferred_maintenance_window
+ if master_password:
+ params['MasterUserPassword'] = master_password
+ if allocated_storage:
+ params['AllocatedStorage'] = allocated_storage
+ if backup_retention_period:
+ params['BackupRetentionPeriod'] = backup_retention_period
+ if preferred_backup_window:
+ params['PreferredBackupWindow'] = preferred_backup_window
+ if apply_immediately:
+ params['ApplyImmediately'] = 'true'
+
+ return self.get_object('ModifyDBInstance', params, DBInstance)
+
+ def delete_dbinstance(self, id, skip_final_snapshot=False,
+ final_snapshot_id=''):
+ """
+ Delete an existing DBInstance.
+
+ :type id: str
+ :param id: Unique identifier for the new instance.
+
+ :type skip_final_snapshot: bool
+ :param skip_final_snapshot: This parameter determines whether a final
+ db snapshot is created before the instance
+ is deleted. If True, no snapshot is created.
+ If False, a snapshot is created before
+ deleting the instance.
+
+ :type final_snapshot_id: str
+ :param final_snapshot_id: If a final snapshot is requested, this
+ is the identifier used for that snapshot.
+
+ :rtype: :class:`boto.rds.dbinstance.DBInstance`
+ :return: The deleted db instance.
+ """
+ params = {'DBInstanceIdentifier' : id}
+ if skip_final_snapshot:
+ params['SkipFinalSnapshot'] = 'true'
+ else:
+ params['SkipFinalSnapshot'] = 'false'
+ params['FinalDBSnapshotIdentifier'] = final_snapshot_id
+ return self.get_object('DeleteDBInstance', params, DBInstance)
+
+ # DBParameterGroup methods
+
+ def get_all_dbparameter_groups(self, groupname=None, max_records=None,
+ marker=None):
+ """
+ Get all parameter groups associated with your account in a region.
+
+ :type groupname: str
+ :param groupname: The name of the DBParameter group to retrieve.
+ If not provided, all DBParameter groups will be returned.
+
+ :type max_records: int
+ :param max_records: The maximum number of records to be returned.
+ If more results are available, a MoreToken will
+ be returned in the response that can be used to
+ retrieve additional records. Default is 100.
+
+ :type marker: str
+ :param marker: The marker provided by a previous request.
+
+ :rtype: list
+ :return: A list of :class:`boto.ec2.parametergroup.ParameterGroup`
+ """
+ params = {}
+ if groupname:
+ params['DBParameterGroupName'] = groupname
+ if max_records:
+ params['MaxRecords'] = max_records
+ if marker:
+ params['Marker'] = marker
+ return self.get_list('DescribeDBParameterGroups', params,
+ [('DBParameterGroup', ParameterGroup)])
+
+ def get_all_dbparameters(self, groupname, source=None,
+ max_records=None, marker=None):
+ """
+ Get all parameters associated with a ParameterGroup
+
+ :type groupname: str
+ :param groupname: The name of the DBParameter group to retrieve.
+
+ :type source: str
+ :param source: Specifies which parameters to return.
+ If not specified, all parameters will be returned.
+ Valid values are: user|system|engine-default
+
+ :type max_records: int
+ :param max_records: The maximum number of records to be returned.
+ If more results are available, a MoreToken will
+ be returned in the response that can be used to
+ retrieve additional records. Default is 100.
+
+ :type marker: str
+ :param marker: The marker provided by a previous request.
+
+ :rtype: :class:`boto.ec2.parametergroup.ParameterGroup`
+ :return: The ParameterGroup
+ """
+ params = {'DBParameterGroupName' : groupname}
+ if source:
+ params['Source'] = source
+ if max_records:
+ params['MaxRecords'] = max_records
+ if marker:
+ params['Marker'] = marker
+ pg = self.get_object('DescribeDBParameters', params, ParameterGroup)
+ pg.name = groupname
+ return pg
+
+ def create_parameter_group(self, name, engine='MySQL5.1', description=''):
+ """
+ Create a new dbparameter group for your account.
+
+ :type name: string
+ :param name: The name of the new dbparameter group
+
+ :type engine: str
+ :param engine: Name of database engine. Must be MySQL5.1 for now.
+
+ :type description: string
+ :param description: The description of the new security group
+
+ :rtype: :class:`boto.rds.dbsecuritygroup.DBSecurityGroup`
+ :return: The newly created DBSecurityGroup
+ """
+ params = {'DBParameterGroupName': name,
+ 'Engine': engine,
+ 'Description' : description}
+ return self.get_object('CreateDBParameterGroup', params, ParameterGroup)
+
+ def modify_parameter_group(self, name, parameters=None):
+ """
+ Modify a parameter group for your account.
+
+ :type name: string
+ :param name: The name of the new parameter group
+
+ :type parameters: list of :class:`boto.rds.parametergroup.Parameter`
+ :param parameters: The new parameters
+
+ :rtype: :class:`boto.rds.parametergroup.ParameterGroup`
+ :return: The newly created ParameterGroup
+ """
+ params = {'DBParameterGroupName': name}
+ for i in range(0, len(parameters)):
+ parameter = parameters[i]
+ parameter.merge(params, i+1)
+ return self.get_list('ModifyDBParameterGroup', params, ParameterGroup)
+
+ def reset_parameter_group(self, name, reset_all_params=False, parameters=None):
+ """
+ Resets some or all of the parameters of a ParameterGroup to the
+ default value
+
+ :type key_name: string
+ :param key_name: The name of the ParameterGroup to reset
+
+ :type parameters: list of :class:`boto.rds.parametergroup.Parameter`
+ :param parameters: The parameters to reset. If not supplied, all parameters
+ will be reset.
+ """
+ params = {'DBParameterGroupName':name}
+ if reset_all_params:
+ params['ResetAllParameters'] = 'true'
+ else:
+ params['ResetAllParameters'] = 'false'
+ for i in range(0, len(parameters)):
+ parameter = parameters[i]
+ parameter.merge(params, i+1)
+ return self.get_status('ResetDBParameterGroup', params)
+
+ def delete_parameter_group(self, name):
+ """
+ Delete a DBSecurityGroup from your account.
+
+ :type key_name: string
+ :param key_name: The name of the DBSecurityGroup to delete
+ """
+ params = {'DBParameterGroupName':name}
+ return self.get_status('DeleteDBParameterGroup', params)
+
+ # DBSecurityGroup methods
+
+ def get_all_dbsecurity_groups(self, groupname=None, max_records=None,
+ marker=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 max_records: int
+ :param max_records: The maximum number of records to be returned.
+ If more results are available, a MoreToken will
+ be returned in the response that can be used to
+ retrieve additional records. Default is 100.
+
+ :type marker: str
+ :param marker: The marker provided by a previous request.
+
+ :rtype: list
+ :return: A list of :class:`boto.rds.dbsecuritygroup.DBSecurityGroup`
+ """
+ params = {}
+ if groupname:
+ params['DBSecurityGroupName'] = groupname
+ if max_records:
+ params['MaxRecords'] = max_records
+ if marker:
+ params['Marker'] = marker
+ return self.get_list('DescribeDBSecurityGroups', params,
+ [('DBSecurityGroup', DBSecurityGroup)])
+
+ def create_dbsecurity_group(self, name, description=None):
+ """
+ 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.rds.dbsecuritygroup.DBSecurityGroup`
+ :return: The newly created DBSecurityGroup
+ """
+ params = {'DBSecurityGroupName':name}
+ if description:
+ params['DBSecurityGroupDescription'] = description
+ group = self.get_object('CreateDBSecurityGroup', params, DBSecurityGroup)
+ group.name = name
+ group.description = description
+ return group
+
+ def delete_dbsecurity_group(self, name):
+ """
+ Delete a DBSecurityGroup from your account.
+
+ :type key_name: string
+ :param key_name: The name of the DBSecurityGroup to delete
+ """
+ params = {'DBSecurityGroupName':name}
+ return self.get_status('DeleteDBSecurityGroup', params)
+
+ def authorize_dbsecurity_group(self, group_name, cidr_ip=None,
+ ec2_security_group_name=None,
+ ec2_security_group_owner_id=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 a CIDR block but not both.
+
+ :type group_name: string
+ :param group_name: The name of the security group you are adding
+ the rule to.
+
+ :type ec2_security_group_name: string
+ :param ec2_security_group_name: The name of the EC2 security group you are
+ granting access to.
+
+ :type ec2_security_group_owner_id: string
+ :param ec2_security_group_owner_id: The ID of the owner of the EC2 security
+ group you are granting access to.
+
+ :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.
+ """
+ params = {'DBSecurityGroupName':group_name}
+ if ec2_security_group_name:
+ params['EC2SecurityGroupName'] = ec2_security_group_name
+ if ec2_security_group_owner_id:
+ params['EC2SecurityGroupOwnerId'] = ec2_security_group_owner_id
+ if cidr_ip:
+ params['CIDRIP'] = urllib.quote(cidr_ip)
+ return self.get_object('AuthorizeDBSecurityGroupIngress', params, DBSecurityGroup)
+
+ def revoke_security_group(self, group_name, ec2_security_group_name=None,
+ ec2_security_group_owner_id=None, cidr_ip=None):
+ """
+ Remove an existing rule from an existing security group.
+ You need to pass in either ec2_security_group_name and
+ ec2_security_group_owner_id OR a CIDR block.
+
+ :type group_name: string
+ :param group_name: The name of the security group you are removing
+ the rule from.
+
+ :type ec2_security_group_name: string
+ :param ec2_security_group_name: The name of the EC2 security group you are
+ granting access to.
+
+ :type ec2_security_group_owner_id: string
+ :param ec2_security_group_owner_id: The ID of the owner of the EC2 security
+ group you are granting access to.
+
+ :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.
+ """
+ params = {'DBSecurityGroupName':group_name}
+ if ec2_security_group_name:
+ params['EC2SecurityGroupName'] = ec2_security_group_name
+ if ec2_security_group_owner_id:
+ params['EC2SecurityGroupOwnerId'] = ec2_security_group_owner_id
+ if cidr_ip:
+ params['CIDRIP'] = cidr_ip
+ return self.get_object('RevokeDBSecurityGroupIngress', params, DBSecurityGroup)
+
+ # DBSnapshot methods
+
+ def get_all_dbsnapshots(self, snapshot_id=None, instance_id=None,
+ max_records=None, marker=None):
+ """
+ Get information about DB Snapshots.
+
+ :type snapshot_id: str
+ :param snapshot_id: The unique identifier of an RDS snapshot.
+ If not provided, all RDS snapshots will be returned.
+
+ :type instance_id: str
+ :param instance_id: The identifier of a DBInstance. If provided,
+ only the DBSnapshots related to that instance will
+ be returned.
+ If not provided, all RDS snapshots will be returned.
+
+ :type max_records: int
+ :param max_records: The maximum number of records to be returned.
+ If more results are available, a MoreToken will
+ be returned in the response that can be used to
+ retrieve additional records. Default is 100.
+
+ :type marker: str
+ :param marker: The marker provided by a previous request.
+
+ :rtype: list
+ :return: A list of :class:`boto.rds.dbsnapshot.DBSnapshot`
+ """
+ params = {}
+ if snapshot_id:
+ params['DBSnapshotIdentifier'] = snapshot_id
+ if instance_id:
+ params['DBInstanceIdentifier'] = instance_id
+ if max_records:
+ params['MaxRecords'] = max_records
+ if marker:
+ params['Marker'] = marker
+ return self.get_list('DescribeDBSnapshots', params,
+ [('DBSnapshots', DBSnapshot)])
+
+ def create_dbsnapshot(self, snapshot_id, dbinstance_id):
+ """
+ Create a new DB snapshot.
+
+ :type snapshot_id: string
+ :param snapshot_id: The identifier for the DBSnapshot
+
+ :type dbinstance_id: string
+ :param dbinstance_id: The source identifier for the RDS instance from
+ which the snapshot is created.
+
+ :rtype: :class:`boto.rds.dbsnapshot.DBSnapshot`
+ :return: The newly created DBSnapshot
+ """
+ params = {'DBSnapshotIdentifier' : snapshot_id,
+ 'DBInstanceIdentifier' : dbinstance_id}
+ return self.get_object('CreateDBSnapshot', params, DBSnapshot)
+
+ def delete_dbsnapshot(self, identifier):
+ """
+ Delete a DBSnapshot
+
+ :type identifier: string
+ :param identifier: The identifier of the DBSnapshot to delete
+ """
+ params = {'DBSnapshotIdentifier' : identifier}
+ return self.get_object('DeleteDBSnapshot', params, DBSnapshot)
+
+ def restore_dbinstance_from_dbsnapshot(self, identifier, instance_id,
+ instance_class, port=None,
+ availability_zone=None):
+
+ """
+ Create a new DBInstance from a DB snapshot.
+
+ :type identifier: string
+ :param identifier: The identifier for the DBSnapshot
+
+ :type instance_id: string
+ :param instance_id: The source identifier for the RDS instance from
+ which the snapshot is created.
+
+ :type instance_class: str
+ :param instance_class: The compute and memory capacity of the DBInstance.
+ Valid values are:
+ db.m1.small | db.m1.large | db.m1.xlarge |
+ db.m2.2xlarge | db.m2.4xlarge
+
+ :type port: int
+ :param port: Port number on which database accepts connections.
+ Valid values [1115-65535]. Defaults to 3306.
+
+ :type availability_zone: str
+ :param availability_zone: Name of the availability zone to place
+ DBInstance into.
+
+ :rtype: :class:`boto.rds.dbinstance.DBInstance`
+ :return: The newly created DBInstance
+ """
+ params = {'DBSnapshotIdentifier' : identifier,
+ 'DBInstanceIdentifier' : instance_id,
+ 'DBInstanceClass' : instance_class}
+ if port:
+ params['Port'] = port
+ if availability_zone:
+ params['AvailabilityZone'] = availability_zone
+ return self.get_object('RestoreDBInstanceFromDBSnapshot',
+ params, DBInstance)
+
+ def restore_dbinstance_from_point_in_time(self, source_instance_id,
+ target_instance_id,
+ use_latest=False,
+ restore_time=None,
+ dbinstance_class=None,
+ port=None,
+ availability_zone=None):
+
+ """
+ Create a new DBInstance from a point in time.
+
+ :type source_instance_id: string
+ :param source_instance_id: The identifier for the source DBInstance.
+
+ :type target_instance_id: string
+ :param target_instance_id: The identifier of the new DBInstance.
+
+ :type use_latest: bool
+ :param use_latest: If True, the latest snapshot availabile will
+ be used.
+
+ :type restore_time: datetime
+ :param restore_time: The date and time to restore from. Only
+ used if use_latest is False.
+
+ :type instance_class: str
+ :param instance_class: The compute and memory capacity of the DBInstance.
+ Valid values are:
+ db.m1.small | db.m1.large | db.m1.xlarge |
+ db.m2.2xlarge | db.m2.4xlarge
+
+ :type port: int
+ :param port: Port number on which database accepts connections.
+ Valid values [1115-65535]. Defaults to 3306.
+
+ :type availability_zone: str
+ :param availability_zone: Name of the availability zone to place
+ DBInstance into.
+
+ :rtype: :class:`boto.rds.dbinstance.DBInstance`
+ :return: The newly created DBInstance
+ """
+ params = {'SourceDBInstanceIdentifier' : source_instance_id,
+ 'TargetDBInstanceIdentifier' : target_instance_id}
+ if use_latest:
+ params['UseLatestRestorableTime'] = 'true'
+ elif restore_time:
+ params['RestoreTime'] = restore_time.isoformat()
+ if instance_class:
+ params['DBInstanceClass'] = instance_class
+ if port:
+ params['Port'] = port
+ if availability_zone:
+ params['AvailabilityZone'] = availability_zone
+ return self.get_object('RestoreDBInstanceToPointInTime',
+ params, DBInstance)
+
+ # Events
+
+ def get_all_events(self, source_identifier=None, source_type=None,
+ start_time=None, end_time=None,
+ max_records=None, marker=None):
+ """
+ Get information about events related to your DBInstances,
+ DBSecurityGroups and DBParameterGroups.
+
+ :type source_identifier: str
+ :param source_identifier: If supplied, the events returned will be
+ limited to those that apply to the identified
+ source. The value of this parameter depends
+ on the value of source_type. If neither
+ parameter is specified, all events in the time
+ span will be returned.
+
+ :type source_type: str
+ :param source_type: Specifies how the source_identifier should
+ be interpreted. Valid values are:
+ b-instance | db-security-group |
+ db-parameter-group | db-snapshot
+
+ :type start_time: datetime
+ :param start_time: The beginning of the time interval for events.
+ If not supplied, all available events will
+ be returned.
+
+ :type end_time: datetime
+ :param end_time: The ending of the time interval for events.
+ If not supplied, all available events will
+ be returned.
+
+ :type max_records: int
+ :param max_records: The maximum number of records to be returned.
+ If more results are available, a MoreToken will
+ be returned in the response that can be used to
+ retrieve additional records. Default is 100.
+
+ :type marker: str
+ :param marker: The marker provided by a previous request.
+
+ :rtype: list
+ :return: A list of class:`boto.rds.event.Event`
+ """
+ params = {}
+ if source_identifier and source_type:
+ params['SourceIdentifier'] = source_identifier
+ params['SourceType'] = source_type
+ if start_time:
+ params['StartTime'] = start_time.isoformat()
+ if end_time:
+ params['EndTime'] = end_time.isoformat()
+ if max_records:
+ params['MaxRecords'] = max_records
+ if marker:
+ params['Marker'] = marker
+ return self.get_list('DescribeEvents', params, [('Event', Event)])
+
+
diff --git a/api/boto/rds/dbinstance.py b/api/boto/rds/dbinstance.py
new file mode 100644
index 0000000..23e1c98
--- /dev/null
+++ b/api/boto/rds/dbinstance.py
@@ -0,0 +1,136 @@
+# 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.rds.dbsecuritygroup import DBSecurityGroup
+from boto.rds.parametergroup import ParameterGroup
+
+class DBInstance(object):
+ """
+ Represents a RDS DBInstance
+ """
+
+ def __init__(self, connection=None, id=None):
+ self.connection = connection
+ self.id = id
+ self.create_time = None
+ self.engine = None
+ self.status = None
+ self.allocated_storage = None
+ self.endpoint = None
+ self.instance_class = None
+ self.master_username = None
+ self.parameter_group = None
+ self.security_group = None
+ self.availability_zone = None
+ self.backup_retention_period = None
+ self.preferred_backup_window = None
+ self.preferred_maintenance_window = None
+ self.latest_restorable_time = None
+ self._in_endpoint = False
+ self._port = None
+ self._address = None
+
+ def __repr__(self):
+ return 'DBInstance:%s' % self.id
+
+ def startElement(self, name, attrs, connection):
+ if name == 'Endpoint':
+ self._in_endpoint = True
+ elif name == 'DBParameterGroup':
+ self.parameter_group = ParameterGroup(self.connection)
+ return self.parameter_group
+ elif name == 'DBSecurityGroup':
+ self.security_group = DBSecurityGroup(self.connection)
+ return self.security_group
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'DBInstanceIdentifier':
+ self.id = value
+ elif name == 'DBInstanceStatus':
+ self.status = value
+ elif name == 'InstanceCreateTime':
+ self.create_time = value
+ elif name == 'Engine':
+ self.engine = value
+ elif name == 'DBInstanceStatus':
+ self.status = value
+ elif name == 'AllocatedStorage':
+ self.allocated_storage = int(value)
+ elif name == 'DBInstanceClass':
+ self.instance_class = value
+ elif name == 'MasterUsername':
+ self.master_username = value
+ elif name == 'Port':
+ if self._in_endpoint:
+ self._port = int(value)
+ elif name == 'Address':
+ if self._in_endpoint:
+ self._address = value
+ elif name == 'Endpoint':
+ self.endpoint = (self._address, self._port)
+ self._in_endpoint = False
+ elif name == 'AvailabilityZone':
+ self.availability_zone = value
+ elif name == 'BackupRetentionPeriod':
+ self.backup_retention_period = value
+ elif name == 'LatestRestorableTime':
+ self.latest_restorable_time = value
+ elif name == 'PreferredMaintenanceWindow':
+ self.preferred_maintenance_window = value
+ elif name == 'PreferredBackupWindow':
+ self.preferred_backup_window = value
+ else:
+ setattr(self, name, value)
+
+ def snapshot(self, snapshot_id):
+ """
+ Create a new DB snapshot of this DBInstance.
+
+ :type identifier: string
+ :param identifier: The identifier for the DBSnapshot
+
+ :rtype: :class:`boto.rds.dbsnapshot.DBSnapshot`
+ :return: The newly created DBSnapshot
+ """
+ return self.connection.create_dbsnapshot(snapshot_id, self.id)
+
+ def stop(self, skip_final_snapshot, final_snapshot_id):
+ """
+ Delete this DBInstance.
+
+ :type skip_final_snapshot: bool
+ :param skip_final_snapshot: This parameter determines whether a final
+ db snapshot is created before the instance
+ is deleted. If True, no snapshot is created.
+ If False, a snapshot is created before
+ deleting the instance.
+
+ :type final_snapshot_id: str
+ :param final_snapshot_id: If a final snapshot is requested, this
+ is the identifier used for that snapshot.
+
+ :rtype: :class:`boto.rds.dbinstance.DBInstance`
+ :return: The deleted db instance.
+ """
+ return self.connection.delete_dbinstance(self.id,
+ skip_final_snapshot,
+ final_snapshot_id)
diff --git a/api/boto/rds/dbsecuritygroup.py b/api/boto/rds/dbsecuritygroup.py
new file mode 100644
index 0000000..9ec6cc0
--- /dev/null
+++ b/api/boto/rds/dbsecuritygroup.py
@@ -0,0 +1,159 @@
+# 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.
+
+"""
+Represents an DBSecurityGroup
+"""
+from boto.ec2.securitygroup import SecurityGroup
+
+class DBSecurityGroup(object):
+
+ def __init__(self, connection=None, owner_id=None,
+ name=None, description=None):
+ self.connection = connection
+ self.owner_id = owner_id
+ self.name = name
+ self.description = description
+ self.ec2_groups = []
+ self.ip_ranges = []
+
+ def __repr__(self):
+ return 'DBSecurityGroup:%s' % self.name
+
+ def startElement(self, name, attrs, connection):
+ if name == 'IPRange':
+ cidr = IPRange(self)
+ self.ip_ranges.append(cidr)
+ return cidr
+ elif name == 'EC2SecurityGroup':
+ ec2_grp = EC2SecurityGroup(self)
+ self.ec2_groups.append(ec2_grp)
+ return ec2_grp
+ else:
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'OwnerId':
+ self.owner_id = value
+ elif name == 'DBSecurityGroupName':
+ self.name = value
+ elif name == 'DBSecurityGroupDescription':
+ self.description = value
+ elif name == 'IPRanges':
+ pass
+ else:
+ setattr(self, name, value)
+
+ def delete(self):
+ return self.connection.delete_dbsecurity_group(self.name)
+
+ def authorize(self, cidr_ip=None, ec2_group=None):
+ """
+ Add a new rule to this DBSecurity group.
+ You need to pass in either a CIDR block to authorize or
+ and EC2 SecurityGroup.
+
+ @type cidr_ip: string
+ @param cidr_ip: A valid CIDR IP range to authorize
+
+ @type ec2_group: :class:`boto.ec2.securitygroup.SecurityGroup>`b
+
+ @rtype: bool
+ @return: True if successful.
+ """
+ if isinstance(ec2_group, SecurityGroup):
+ group_name = ec2_group.name
+ group_owner_id = ec2_group.owner_id
+ else:
+ group_name = None
+ group_owner_id = None
+ return self.connection.authorize_dbsecurity_group(self.name,
+ cidr_ip,
+ group_name,
+ group_owner_id)
+
+ def revoke(self, cidr_ip=None, ec2_group=None):
+ """
+ Revoke access to a CIDR range or EC2 SecurityGroup
+ You need to pass in either a CIDR block to authorize or
+ and EC2 SecurityGroup.
+
+ @type cidr_ip: string
+ @param cidr_ip: A valid CIDR IP range to authorize
+
+ @type ec2_group: :class:`boto.ec2.securitygroup.SecurityGroup>`b
+
+ @rtype: bool
+ @return: True if successful.
+ """
+ if isinstance(ec2_group, SecurityGroup):
+ group_name = ec2_group.name
+ group_owner_id = ec2_group.owner_id
+ else:
+ group_name = None
+ group_owner_id = None
+ return self.connection.revoke_dbsecurity_group(self.name,
+ cidr_ip,
+ group_name,
+ group_owner_id)
+
+class IPRange(object):
+
+ def __init__(self, parent=None):
+ self.parent = parent
+ self.cidr_ip = None
+ self.status = None
+
+ def __repr__(self):
+ return 'IPRange:%s' % self.cidr_ip
+
+ def startElement(self, name, attrs, connection):
+ pass
+
+ def endElement(self, name, value, connection):
+ if name == 'CIDRIP':
+ self.cidr_ip = value
+ elif name == 'Status':
+ self.status = value
+ else:
+ setattr(self, name, value)
+
+class EC2SecurityGroup(object):
+
+ def __init__(self, parent=None):
+ self.parent = parent
+ self.name = None
+ self.owner_id = None
+
+ def __repr__(self):
+ return 'EC2SecurityGroup:%s' % self.name
+
+ def startElement(self, name, attrs, connection):
+ pass
+
+ def endElement(self, name, value, connection):
+ if name == 'EC2SecurityGroupName':
+ self.name = value
+ elif name == 'EC2SecurityGroupOwnerId':
+ self.owner_id = value
+ else:
+ setattr(self, name, value)
+
diff --git a/api/boto/rds/dbsnapshot.py b/api/boto/rds/dbsnapshot.py
new file mode 100644
index 0000000..78d0230
--- /dev/null
+++ b/api/boto/rds/dbsnapshot.py
@@ -0,0 +1,74 @@
+# 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 DBSnapshot(object):
+ """
+ Represents a RDS DB Snapshot
+ """
+
+ def __init__(self, connection=None, id=None):
+ self.connection = connection
+ self.id = id
+ self.engine = None
+ self.snapshot_create_time = None
+ self.instance_create_time = None
+ self.port = None
+ self.status = None
+ self.availability_zone = None
+ self.master_username = None
+ self.allocated_storage = None
+ self.instance_id = None
+ self.availability_zone = None
+
+ def __repr__(self):
+ return 'DBSnapshot:%s' % self.id
+
+ def startElement(self, name, attrs, connection):
+ pass
+
+ def endElement(self, name, value, connection):
+ if name == 'Engine':
+ self.engine = value
+ elif name == 'InstanceCreateTime':
+ self.instance_create_time = value
+ elif name == 'SnapshotCreateTime':
+ self.snapshot_create_time = value
+ elif name == 'DBInstanceIdentifier':
+ self.instance_id = value
+ elif name == 'DBSnapshotIdentifier':
+ self.id = value
+ elif name == 'Port':
+ self.port = int(value)
+ elif name == 'Status':
+ self.status = value
+ elif name == 'AvailabilityZone':
+ self.availability_zone = value
+ elif name == 'MasterUsername':
+ self.master_username = value
+ elif name == 'AllocatedStorage':
+ self.allocated_storage = int(value)
+ elif name == 'SnapshotTime':
+ self.time = value
+ else:
+ setattr(self, name, value)
+
+
+
diff --git a/api/boto/rds/event.py b/api/boto/rds/event.py
new file mode 100644
index 0000000..a91f8f0
--- /dev/null
+++ b/api/boto/rds/event.py
@@ -0,0 +1,49 @@
+# 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 Event(object):
+
+ def __init__(self, connection=None):
+ self.connection = connection
+ self.message = None
+ self.source_identifier = None
+ self.source_type = None
+ self.engine = None
+ self.date = None
+
+ def __repr__(self):
+ return '"%s"' % self.message
+
+ def startElement(self, name, attrs, connection):
+ pass
+
+ def endElement(self, name, value, connection):
+ if name == 'SourceIdentifier':
+ self.source_identifier = value
+ elif name == 'SourceType':
+ self.source_type = value
+ elif name == 'Message':
+ self.message = value
+ elif name == 'Date':
+ self.date = value
+ else:
+ setattr(self, name, value)
+
diff --git a/api/boto/rds/parametergroup.py b/api/boto/rds/parametergroup.py
new file mode 100644
index 0000000..081e263
--- /dev/null
+++ b/api/boto/rds/parametergroup.py
@@ -0,0 +1,201 @@
+# 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 ParameterGroup(dict):
+
+ def __init__(self, connection=None):
+ dict.__init__(self)
+ self.connection = connection
+ self.name = None
+ self.description = None
+ self.engine = None
+ self._current_param = None
+
+ def __repr__(self):
+ return 'ParameterGroup:%s' % self.name
+
+ def startElement(self, name, attrs, connection):
+ if name == 'Parameter':
+ if self._current_param:
+ self[self._current_param.name] = self._current_param
+ self._current_param = Parameter(self)
+ return self._current_param
+
+ def endElement(self, name, value, connection):
+ if name == 'DBParameterGroupName':
+ self.name = value
+ elif name == 'Description':
+ self.description = value
+ elif name == 'Engine':
+ self.engine = value
+ else:
+ setattr(self, name, value)
+
+ def modifiable(self):
+ mod = []
+ for key in self:
+ p = self[key]
+ if p.is_modifiable:
+ mod.append(p)
+ return mod
+
+ def get_params(self):
+ pg = self.connection.get_all_dbparameters(self.name)
+ self.update(pg)
+
+ def add_param(self, name, value, apply_method):
+ param = Parameter()
+ param.name = name
+ param.value = value
+ param.apply_method = apply_method
+ self.params.append(param)
+
+class Parameter(object):
+ """
+ Represents a RDS Parameter
+ """
+
+ ValidTypes = {'integer' : int,
+ 'string' : str,
+ 'boolean' : bool}
+ ValidSources = ['user', 'system', 'engine-default']
+ ValidApplyTypes = ['static', 'dynamic']
+ ValidApplyMethods = ['immediate', 'pending-reboot']
+
+ def __init__(self, group=None, name=None):
+ self.group = group
+ self.name = name
+ self._value = None
+ self.type = str
+ self.source = None
+ self.is_modifiable = True
+ self.description = None
+ self.apply_method = None
+ self.allowed_values = None
+
+ def __repr__(self):
+ return 'Parameter:%s' % self.name
+
+ def startElement(self, name, attrs, connection):
+ pass
+
+ def endElement(self, name, value, connection):
+ if name == 'ParameterName':
+ self.name = value
+ elif name == 'ParameterValue':
+ self._value = value
+ elif name == 'DataType':
+ if value in self.ValidTypes:
+ self.type = value
+ elif name == 'Source':
+ if value in self.ValidSources:
+ self.source = value
+ elif name == 'IsModifiable':
+ if value.lower() == 'true':
+ self.is_modifiable = True
+ else:
+ self.is_modifiable = False
+ elif name == 'Description':
+ self.description = value
+ elif name == 'ApplyType':
+ if value in self.ValidApplyTypes:
+ self.apply_type = value
+ elif name == 'AllowedValues':
+ self.allowed_values = value
+ else:
+ setattr(self, name, value)
+
+ def merge(self, d, i):
+ prefix = 'Parameters.member.%d.' % i
+ if self.name:
+ d[prefix+'ParameterName'] = self.name
+ if self._value:
+ d[prefix+'ParameterValue'] = self._value
+ if self.apply_type:
+ d[prefix+'ApplyMethod'] = self.apply_method
+
+ def _set_string_value(self, value):
+ if not isinstance(value, str) or isinstance(value, unicode):
+ raise ValueError, 'value must be of type str'
+ if self.allowed_values:
+ choices = self.allowed_values.split(',')
+ if value not in choices:
+ raise ValueError, 'value must be in %s' % self.allowed_values
+ set._value = value
+
+ def _set_integer_value(self, value):
+ if isinstance(value, str) or isinstance(value, unicode):
+ value = int(value)
+ if isinstance(value, int) or isinstance(value, long):
+ if self.allowed_values:
+ min, max = self.allowed_values.split('-')
+ if value < int(min) or value > int(max):
+ raise ValueError, 'range is %s' % self.allowed_values
+ self._value = value
+ else:
+ raise ValueError, 'value must be integer'
+
+ def _set_boolean_value(self, value):
+ if isinstance(value, bool):
+ self._value = value
+ elif isinstance(value, str) or isinstance(value, unicode):
+ if value.lower() == 'true':
+ self._value = True
+ else:
+ self._value = False
+ else:
+ raise ValueError, 'value must be boolean'
+
+ def set_value(self, value):
+ if self.type == 'string':
+ self._set_string_value(value)
+ elif self.type == 'integer':
+ self._set_integer_value(value)
+ elif self.type == 'boolean':
+ self._set_boolean_value(value)
+ else:
+ raise TypeError, 'unknown type (%s)' % self.type
+
+ def get_value(self):
+ if self._value == None:
+ return self._value
+ if self.type == 'string':
+ return self._value
+ elif self.type == 'integer':
+ if not isinstance(self._value, int) and not isinstance(self._value, long):
+ self._set_integer_value(self._value)
+ return self._value
+ elif self.type == 'boolean':
+ if not isinstance(self._value, bool):
+ self._set_boolean_value(self._value)
+ return self._value
+ else:
+ raise TypeError, 'unknown type (%s)' % self.type
+
+ value = property(get_value, set_value, 'The value of the parameter')
+
+ def apply(self, immediate=False):
+ if immediate:
+ self.apply_method = 'immediate'
+ else:
+ self.apply_method = 'pending-reboot'
+ self.group.connection.modify_parameter_group(self.group.name, [self])
+
diff --git a/api/boto/resultset.py b/api/boto/resultset.py
new file mode 100644
index 0000000..aab1b68
--- /dev/null
+++ b/api/boto/resultset.py
@@ -0,0 +1,130 @@
+# 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.
+
+class ResultSet(list):
+ """
+ The ResultSet is used to pass results back from the Amazon services
+ to the client. It has an ugly but workable mechanism for parsing
+ the XML results from AWS. Because I don't really want any dependencies
+ on external libraries, I'm using the standard SAX parser that comes
+ with Python. The good news is that it's quite fast and efficient but
+ it makes some things rather difficult.
+
+ You can pass in, as the marker_elem parameter, a list of tuples.
+ Each tuple contains a string as the first element which represents
+ the XML element that the resultset needs to be on the lookout for
+ and a Python class as the second element of the tuple. Each time the
+ specified element is found in the XML, a new instance of the class
+ will be created and popped onto the stack.
+
+ """
+
+ def __init__(self, marker_elem=None):
+ list.__init__(self)
+ if isinstance(marker_elem, list):
+ self.markers = marker_elem
+ else:
+ self.markers = []
+ self.marker = None
+ self.is_truncated = False
+ self.next_token = None
+ self.status = True
+
+ def startElement(self, name, attrs, connection):
+ for t in self.markers:
+ if name == t[0]:
+ obj = t[1](connection)
+ self.append(obj)
+ return obj
+ return None
+
+ def to_boolean(self, value, true_value='true'):
+ if value == true_value:
+ return True
+ else:
+ return False
+
+ def endElement(self, name, value, connection):
+ if name == 'IsTruncated':
+ self.is_truncated = self.to_boolean(value)
+ elif name == 'Marker':
+ self.marker = value
+ elif name == 'Prefix':
+ self.prefix = value
+ elif name == 'return':
+ self.status = self.to_boolean(value)
+ elif name == 'StatusCode':
+ self.status = self.to_boolean(value, 'Success')
+ elif name == 'ItemName':
+ self.append(value)
+ elif name == 'NextToken':
+ self.next_token = value
+ elif name == 'BoxUsage':
+ try:
+ connection.box_usage += float(value)
+ except:
+ pass
+ elif name == 'IsValid':
+ self.status = self.to_boolean(value, 'True')
+ else:
+ setattr(self, name, value)
+
+class BooleanResult(object):
+
+ def __init__(self, marker_elem=None):
+ self.status = True
+ self.request_id = None
+ self.box_usage = None
+
+ def __repr__(self):
+ if self.status:
+ return 'True'
+ else:
+ return 'False'
+
+ def __nonzero__(self):
+ return self.status
+
+ def startElement(self, name, attrs, connection):
+ return None
+
+ def to_boolean(self, value, true_value='true'):
+ if value == true_value:
+ return True
+ else:
+ return False
+
+ def endElement(self, name, value, connection):
+ if name == 'return':
+ self.status = self.to_boolean(value)
+ elif name == 'StatusCode':
+ self.status = self.to_boolean(value, 'Success')
+ elif name == 'IsValid':
+ self.status = self.to_boolean(value, 'True')
+ elif name == 'RequestId':
+ self.request_id = value
+ elif name == 'requestId':
+ self.request_id = value
+ elif name == 'BoxUsage':
+ self.request_id = value
+ else:
+ setattr(self, name, value)
+
diff --git a/api/boto/s3/__init__.py b/api/boto/s3/__init__.py
new file mode 100644
index 0000000..be2de1d
--- /dev/null
+++ b/api/boto/s3/__init__.py
@@ -0,0 +1,31 @@
+# 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 boto
+
+boto.check_extensions(__name__, __path__)
+
+from connection import S3Connection as Connection
+from key import Key
+from bucket import Bucket
+
+__all__ = ['Connection', 'Key', 'Bucket']
diff --git a/api/boto/s3/acl.py b/api/boto/s3/acl.py
new file mode 100644
index 0000000..702551e
--- /dev/null
+++ b/api/boto/s3/acl.py
@@ -0,0 +1,161 @@
+# 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.s3.user import User
+import StringIO
+
+CannedACLStrings = ['private', 'public-read',
+ 'public-read-write', 'authenticated-read']
+
+class Policy:
+
+ def __init__(self, parent=None):
+ self.parent = parent
+ self.acl = None
+
+ def __repr__(self):
+ grants = []
+ for g in self.acl.grants:
+ if g.id == self.owner.id:
+ grants.append("%s (owner) = %s" % (g.display_name, g.permission))
+ else:
+ if g.type == 'CanonicalUser':
+ u = g.display_name
+ elif g.type == 'Group':
+ u = g.uri
+ else:
+ u = g.email
+ grants.append("%s = %s" % (u, g.permission))
+ return "" % ", ".join(grants)
+
+ def startElement(self, name, attrs, connection):
+ if name == 'Owner':
+ self.owner = User(self)
+ return self.owner
+ elif name == 'AccessControlList':
+ self.acl = ACL(self)
+ return self.acl
+ else:
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'Owner':
+ pass
+ elif name == 'AccessControlList':
+ pass
+ else:
+ setattr(self, name, value)
+
+ def to_xml(self):
+ s = ''
+ s += self.owner.to_xml()
+ s += self.acl.to_xml()
+ s += ''
+ return s
+
+class ACL:
+
+ def __init__(self, policy=None):
+ self.policy = policy
+ self.grants = []
+
+ def add_grant(self, grant):
+ self.grants.append(grant)
+
+ def add_email_grant(self, permission, email_address):
+ grant = Grant(permission=permission, type='AmazonCustomerByEmail',
+ email_address=email_address)
+ self.grants.append(grant)
+
+ def add_user_grant(self, permission, user_id):
+ grant = Grant(permission=permission, type='CanonicalUser', id=user_id)
+ self.grants.append(grant)
+
+ def startElement(self, name, attrs, connection):
+ if name == 'Grant':
+ self.grants.append(Grant(self))
+ return self.grants[-1]
+ else:
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'Grant':
+ pass
+ else:
+ setattr(self, name, value)
+
+ def to_xml(self):
+ s = ''
+ for grant in self.grants:
+ s += grant.to_xml()
+ s += ''
+ return s
+
+class Grant:
+
+ NameSpace = 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
+
+ def __init__(self, permission=None, type=None, id=None,
+ display_name=None, uri=None, email_address=None):
+ self.permission = permission
+ self.id = id
+ self.display_name = display_name
+ self.uri = uri
+ self.email_address = email_address
+ self.type = type
+
+ def startElement(self, name, attrs, connection):
+ if name == 'Grantee':
+ self.type = attrs['xsi:type']
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'ID':
+ self.id = value
+ elif name == 'DisplayName':
+ self.display_name = value
+ elif name == 'URI':
+ self.uri = value
+ elif name == 'EmailAddress':
+ self.email_address = value
+ elif name == 'Grantee':
+ pass
+ elif name == 'Permission':
+ self.permission = value
+ else:
+ setattr(self, name, value)
+
+ def to_xml(self):
+ s = ''
+ s += '' % (self.NameSpace, self.type)
+ if self.type == 'CanonicalUser':
+ s += '%s' % self.id
+ s += '%s' % self.display_name
+ elif self.type == 'Group':
+ s += '%s' % self.uri
+ else:
+ s += '%s' % self.email_address
+ s += ''
+ s += '%s' % self.permission
+ s += ''
+ return s
+
+
diff --git a/api/boto/s3/bucket.py b/api/boto/s3/bucket.py
new file mode 100644
index 0000000..297f0a2
--- /dev/null
+++ b/api/boto/s3/bucket.py
@@ -0,0 +1,495 @@
+# 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 boto
+from boto import handler
+from boto.resultset import ResultSet
+from boto.s3.acl import Policy, CannedACLStrings, ACL, Grant
+from boto.s3.user import User
+from boto.s3.key import Key
+from boto.s3.prefix import Prefix
+from boto.exception import S3ResponseError, S3PermissionsError, S3CopyError
+from boto.s3.bucketlistresultset import BucketListResultSet
+import boto.utils
+import xml.sax
+import urllib
+
+S3Permissions = ['READ', 'WRITE', 'READ_ACP', 'WRITE_ACP', 'FULL_CONTROL']
+
+class Bucket:
+
+ BucketLoggingBody = """
+
+
+ %s
+ %s
+
+ """
+
+ EmptyBucketLoggingBody = """
+
+ """
+
+ LoggingGroup = 'http://acs.amazonaws.com/groups/s3/LogDelivery'
+
+ BucketPaymentBody = """
+
+ %s
+ """
+
+ def __init__(self, connection=None, name=None, key_class=Key):
+ self.name = name
+ self.connection = connection
+ self.key_class = key_class
+
+ def __repr__(self):
+ return '' % self.name
+
+ def __iter__(self):
+ return iter(BucketListResultSet(self))
+
+ def __contains__(self, key_name):
+ return not (self.get_key(key_name) is None)
+
+ def startElement(self, name, attrs, connection):
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'Name':
+ self.name = value
+ elif name == 'CreationDate':
+ self.creation_date = value
+ else:
+ setattr(self, name, value)
+
+ def set_key_class(self, key_class):
+ """
+ Set the Key class associated with this bucket. By default, this
+ would be the boto.s3.key.Key class but if you want to subclass that
+ for some reason this allows you to associate your new class with a
+ bucket so that when you call bucket.new_key() or when you get a listing
+ of keys in the bucket you will get an instances of your key class
+ rather than the default.
+
+ :type key_class: class
+ :param key_class: A subclass of Key that can be more specific
+ """
+ self.key_class = key_class
+
+ def lookup(self, key_name, headers=None):
+ """
+ Deprecated: Please use get_key method.
+
+ :type key_name: string
+ :param key_name: The name of the key to retrieve
+
+ :rtype: :class:`boto.s3.key.Key`
+ :returns: A Key object from this bucket.
+ """
+ return self.get_key(key_name, headers=headers)
+
+ def get_key(self, key_name, headers=None):
+ """
+ Check to see if a particular key exists within the bucket. This
+ method uses a HEAD request to check for the existance of the key.
+ Returns: An instance of a Key object or None
+
+ :type key_name: string
+ :param key_name: The name of the key to retrieve
+
+ :rtype: :class:`boto.s3.key.Key`
+ :returns: A Key object from this bucket.
+ """
+ response = self.connection.make_request('HEAD', self.name, key_name, headers=headers)
+ if response.status == 200:
+ body = response.read()
+ k = self.key_class(self)
+ k.metadata = boto.utils.get_aws_metadata(response.msg)
+ k.etag = response.getheader('etag')
+ k.content_type = response.getheader('content-type')
+ k.content_encoding = response.getheader('content-encoding')
+ k.last_modified = response.getheader('last-modified')
+ k.size = int(response.getheader('content-length'))
+ k.name = key_name
+ return k
+ else:
+ if response.status == 404:
+ body = response.read()
+ return None
+ else:
+ raise S3ResponseError(response.status, response.reason, '')
+
+ def list(self, prefix='', delimiter='', marker='', headers=None):
+ """
+ List key objects within a bucket. This returns an instance of an
+ BucketListResultSet that automatically handles all of the result
+ paging, etc. from S3. You just need to keep iterating until
+ there are no more results.
+ Called with no arguments, this will return an iterator object across
+ all keys within the bucket.
+
+ :type prefix: string
+ :param prefix: allows you to limit the listing to a particular
+ prefix. For example, if you call the method with prefix='/foo/'
+ then the iterator will only cycle through the keys that begin with
+ the string '/foo/'.
+
+ :type delimiter: string
+ :param delimiter: can be used in conjunction with the prefix
+ to allow you to organize and browse your keys hierarchically. See:
+ http://docs.amazonwebservices.com/AmazonS3/2006-03-01/
+ for more details.
+
+ :type marker: string
+ :param marker: The "marker" of where you are in the result set
+
+ :rtype: :class:`boto.s3.bucketlistresultset.BucketListResultSet`
+ :return: an instance of a BucketListResultSet that handles paging, etc
+ """
+ return BucketListResultSet(self, prefix, delimiter, marker, headers)
+
+ def get_all_keys(self, headers=None, **params):
+ """
+ A lower-level method for listing contents of a bucket. This closely models the actual S3
+ API and requires you to manually handle the paging of results. For a higher-level method
+ that handles the details of paging for you, you can use the list method.
+
+ :type maxkeys: int
+ :param maxkeys: The maximum number of keys to retrieve
+
+ :type prefix: string
+ :param prefix: The prefix of the keys you want to retrieve
+
+ :type marker: string
+ :param marker: The "marker" of where you are in the result set
+
+ :type delimiter: string
+ :param delimiter: "If this optional, Unicode string parameter is included with your request, then keys that contain the same string between the prefix and the first occurrence of the delimiter will be rolled up into a single result element in the CommonPrefixes collection. These rolled-up keys are not returned elsewhere in the response."
+
+ :rtype: ResultSet
+ :return: The result from S3 listing the keys requested
+
+ """
+ l = []
+ for k,v in params.items():
+ if k == 'maxkeys':
+ k = 'max-keys'
+ if isinstance(v, unicode):
+ v = v.encode('utf-8')
+ if v is not None:
+ l.append('%s=%s' % (urllib.quote(k), urllib.quote(str(v))))
+ if len(l):
+ s = '&'.join(l)
+ else:
+ s = None
+ response = self.connection.make_request('GET', self.name,
+ headers=headers, query_args=s)
+ body = response.read()
+ boto.log.debug(body)
+ if response.status == 200:
+ rs = ResultSet([('Contents', self.key_class),
+ ('CommonPrefixes', Prefix)])
+ h = handler.XmlHandler(rs, self)
+ xml.sax.parseString(body, h)
+ return rs
+ else:
+ raise S3ResponseError(response.status, response.reason, body)
+
+ def new_key(self, key_name=None):
+ """
+ Creates a new key
+
+ :type key_name: string
+ :param key_name: The name of the key to create
+
+ :rtype: :class:`boto.s3.key.Key` or subclass
+ :returns: An instance of the newly created key object
+ """
+ return self.key_class(self, key_name)
+
+ def generate_url(self, expires_in, method='GET', headers=None, force_http=False):
+ return self.connection.generate_url(expires_in, method, self.name, headers=headers,
+ force_http=force_http)
+
+ def delete_key(self, key_name, headers=None):
+ """
+ Deletes a key from the bucket.
+
+ :type key_name: string
+ :param key_name: The key name to delete
+ """
+ response = self.connection.make_request('DELETE', self.name, key_name, headers=headers)
+ body = response.read()
+ if response.status != 204:
+ raise S3ResponseError(response.status, response.reason, body)
+
+ def copy_key(self, new_key_name, src_bucket_name, src_key_name, metadata=None):
+ """
+ Create a new key in the bucket by copying another existing key.
+
+ :type new_key_name: string
+ :param new_key_name: The name of the new key
+
+ :type src_bucket_name: string
+ :param src_bucket_name: The name of the source bucket
+
+ :type src_key_name: string
+ :param src_key_name: The name of the source key
+
+ :type metadata: dict
+ :param metadata: Metadata to be associated with new key.
+ If metadata is supplied, it will replace the
+ metadata of the source key being copied.
+ If no metadata is supplied, the source key's
+ metadata will be copied to the new key.
+
+ :rtype: :class:`boto.s3.key.Key` or subclass
+ :returns: An instance of the newly created key object
+ """
+ src = '%s/%s' % (src_bucket_name, urllib.quote(src_key_name))
+ if metadata:
+ headers = {'x-amz-copy-source' : src,
+ 'x-amz-metadata-directive' : 'REPLACE'}
+ headers = boto.utils.merge_meta(headers, metadata)
+ else:
+ headers = {'x-amz-copy-source' : src,
+ 'x-amz-metadata-directive' : 'COPY'}
+ response = self.connection.make_request('PUT', self.name, new_key_name,
+ headers=headers)
+ body = response.read()
+ if response.status == 200:
+ key = self.new_key(new_key_name)
+ h = handler.XmlHandler(key, self)
+ xml.sax.parseString(body, h)
+ if hasattr(key, 'Error'):
+ raise S3CopyError(key.Code, key.Message, body)
+ return key
+ else:
+ raise S3ResponseError(response.status, response.reason, body)
+
+ def set_canned_acl(self, acl_str, key_name='', headers=None):
+ assert acl_str in CannedACLStrings
+
+ if headers:
+ headers['x-amz-acl'] = acl_str
+ else:
+ headers={'x-amz-acl': acl_str}
+
+ response = self.connection.make_request('PUT', self.name, key_name,
+ headers=headers, query_args='acl')
+ body = response.read()
+ if response.status != 200:
+ raise S3ResponseError(response.status, response.reason, body)
+
+ def get_xml_acl(self, key_name='', headers=None):
+ response = self.connection.make_request('GET', self.name, key_name,
+ query_args='acl', headers=headers)
+ body = response.read()
+ if response.status != 200:
+ raise S3ResponseError(response.status, response.reason, body)
+ return body
+
+ def set_xml_acl(self, acl_str, key_name='', headers=None):
+ response = self.connection.make_request('PUT', self.name, key_name,
+ data=acl_str, query_args='acl', headers=headers)
+ body = response.read()
+ if response.status != 200:
+ raise S3ResponseError(response.status, response.reason, body)
+
+ def set_acl(self, acl_or_str, key_name='', headers=None):
+ if isinstance(acl_or_str, Policy):
+ self.set_xml_acl(acl_or_str.to_xml(), key_name, headers=headers)
+ else:
+ self.set_canned_acl(acl_or_str, key_name, headers=headers)
+
+ def get_acl(self, key_name='', headers=None):
+ response = self.connection.make_request('GET', self.name, key_name,
+ query_args='acl', headers=headers)
+ body = response.read()
+ if response.status == 200:
+ policy = Policy(self)
+ h = handler.XmlHandler(policy, self)
+ xml.sax.parseString(body, h)
+ return policy
+ else:
+ raise S3ResponseError(response.status, response.reason, body)
+
+ def make_public(self, recursive=False, headers=None):
+ self.set_canned_acl('public-read', headers=headers)
+ if recursive:
+ for key in self:
+ self.set_canned_acl('public-read', key.name, headers=headers)
+
+ def add_email_grant(self, permission, email_address, recursive=False, headers=None):
+ """
+ Convenience method that provides a quick way to add an email grant to a bucket.
+ This method retrieves the current ACL, creates a new grant based on the parameters
+ passed in, adds that grant to the ACL and then PUT's the new ACL back to S3.
+
+ :param permission: The permission being granted. Should be one of: (READ, WRITE, READ_ACP, WRITE_ACP, FULL_CONTROL).
+ See http://docs.amazonwebservices.com/AmazonS3/2006-03-01/UsingAuthAccess.html for more details on permissions.
+ :type permission: string
+
+ :param email_address: The email address associated with the AWS account your are granting
+ the permission to.
+ :type email_address: string
+
+ :param recursive: A boolean value to controls whether the command will apply the
+ grant to all keys within the bucket or not. The default value is False.
+ By passing a True value, the call will iterate through all keys in the
+ bucket and apply the same grant to each key.
+ CAUTION: If you have a lot of keys, this could take a long time!
+ :type recursive: boolean
+ """
+ if permission not in S3Permissions:
+ raise S3PermissionsError('Unknown Permission: %s' % permission)
+ policy = self.get_acl(headers=headers)
+ policy.acl.add_email_grant(permission, email_address)
+ self.set_acl(policy, headers=headers)
+ if recursive:
+ for key in self:
+ key.add_email_grant(permission, email_address, headers=headers)
+
+ def add_user_grant(self, permission, user_id, recursive=False, headers=None):
+ """
+ Convenience method that provides a quick way to add a canonical user grant to a bucket.
+ This method retrieves the current ACL, creates a new grant based on the parameters
+ passed in, adds that grant to the ACL and then PUT's the new ACL back to S3.
+
+ :type permission: string
+ :param permission: The permission being granted. Should be one of:
+ READ|WRITE|READ_ACP|WRITE_ACP|FULL_CONTROL
+ See http://docs.amazonwebservices.com/AmazonS3/2006-03-01/UsingAuthAccess.html
+ for more details on permissions.
+
+ :type user_id: string
+ :param user_id: The canonical user id associated with the AWS account your are granting
+ the permission to.
+
+ :type recursive: bool
+ :param recursive: A boolean value that controls whether the command will apply the
+ grant to all keys within the bucket or not. The default value is False.
+ By passing a True value, the call will iterate through all keys in the
+ bucket and apply the same grant to each key.
+ CAUTION: If you have a lot of keys, this could take a long time!
+ """
+ if permission not in S3Permissions:
+ raise S3PermissionsError('Unknown Permission: %s' % permission)
+ policy = self.get_acl(headers=headers)
+ policy.acl.add_user_grant(permission, user_id)
+ self.set_acl(policy, headers=headers)
+ if recursive:
+ for key in self:
+ key.add_user_grant(permission, user_id, headers=headers)
+
+ def list_grants(self, headers=None):
+ policy = self.get_acl(headers=headers)
+ return policy.acl.grants
+
+ def get_location(self):
+ """
+ Returns the LocationConstraint for the bucket.
+
+ :rtype: str
+ :return: The LocationConstraint for the bucket or the empty string if
+ no constraint was specified when bucket was created.
+ """
+ response = self.connection.make_request('GET', self.name,
+ query_args='location')
+ body = response.read()
+ if response.status == 200:
+ rs = ResultSet(self)
+ h = handler.XmlHandler(rs, self)
+ xml.sax.parseString(body, h)
+ return rs.LocationConstraint
+ else:
+ raise S3ResponseError(response.status, response.reason, body)
+
+ def enable_logging(self, target_bucket, target_prefix='', headers=None):
+ if isinstance(target_bucket, Bucket):
+ target_bucket = target_bucket.name
+ body = self.BucketLoggingBody % (target_bucket, target_prefix)
+ response = self.connection.make_request('PUT', self.name, data=body,
+ query_args='logging', headers=headers)
+ body = response.read()
+ if response.status == 200:
+ return True
+ else:
+ raise S3ResponseError(response.status, response.reason, body)
+
+ def disable_logging(self, headers=None):
+ body = self.EmptyBucketLoggingBody
+ response = self.connection.make_request('PUT', self.name, data=body,
+ query_args='logging', headers=headers)
+ body = response.read()
+ if response.status == 200:
+ return True
+ else:
+ raise S3ResponseError(response.status, response.reason, body)
+
+ def get_logging_status(self, headers=None):
+ response = self.connection.make_request('GET', self.name,
+ query_args='logging', headers=headers)
+ body = response.read()
+ if response.status == 200:
+ return body
+ else:
+ raise S3ResponseError(response.status, response.reason, body)
+
+ def set_as_logging_target(self, headers=None):
+ policy = self.get_acl(headers=headers)
+ g1 = Grant(permission='WRITE', type='Group', uri=self.LoggingGroup)
+ g2 = Grant(permission='READ_ACP', type='Group', uri=self.LoggingGroup)
+ policy.acl.add_grant(g1)
+ policy.acl.add_grant(g2)
+ self.set_acl(policy, headers=headers)
+
+ def disable_logging(self, headers=None):
+ body = self.EmptyBucketLoggingBody
+ response = self.connection.make_request('PUT', self.name, data=body,
+ query_args='logging', headers=headers)
+ body = response.read()
+ if response.status == 200:
+ return True
+ else:
+ raise S3ResponseError(response.status, response.reason, body)
+
+ def get_request_payment(self, headers=None):
+ response = self.connection.make_request('GET', self.name,
+ query_args='requestPayment', headers=headers)
+ body = response.read()
+ if response.status == 200:
+ return body
+ else:
+ raise S3ResponseError(response.status, response.reason, body)
+
+ def set_request_payment(self, payer='BucketOwner', headers=None):
+ body = self.BucketPaymentBody % payer
+ response = self.connection.make_request('PUT', self.name, data=body,
+ query_args='requestPayment', headers=headers)
+ body = response.read()
+ if response.status == 200:
+ return True
+ else:
+ raise S3ResponseError(response.status, response.reason, body)
+
+ def delete(self, headers=None):
+ return self.connection.delete_bucket(self.name, headers=headers)
diff --git a/api/boto/s3/bucketlistresultset.py b/api/boto/s3/bucketlistresultset.py
new file mode 100644
index 0000000..66ed4ee
--- /dev/null
+++ b/api/boto/s3/bucketlistresultset.py
@@ -0,0 +1,57 @@
+# 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.
+
+def bucket_lister(bucket, prefix='', delimiter='', marker='', headers=None):
+ """
+ A generator function for listing keys in a bucket.
+ """
+ more_results = True
+ k = None
+ while more_results:
+ rs = bucket.get_all_keys(prefix=prefix, marker=marker,
+ delimiter=delimiter, headers=headers)
+ for k in rs:
+ yield k
+ if k:
+ marker = k.name
+ more_results= rs.is_truncated
+
+class BucketListResultSet:
+ """
+ A resultset for listing keys within a bucket. Uses the bucket_lister
+ generator function and implements the iterator interface. This
+ transparently handles the results paging from S3 so even if you have
+ many thousands of keys within the bucket you can iterate over all
+ keys in a reasonably efficient manner.
+ """
+
+ def __init__(self, bucket=None, prefix='', delimiter='', marker='', headers=None):
+ self.bucket = bucket
+ self.prefix = prefix
+ self.delimiter = delimiter
+ self.marker = marker
+ self.headers = headers
+
+ def __iter__(self):
+ return bucket_lister(self.bucket, prefix=self.prefix,
+ delimiter=self.delimiter, marker=self.marker, headers=self.headers)
+
+
diff --git a/api/boto/s3/connection.py b/api/boto/s3/connection.py
new file mode 100644
index 0000000..e366f7e
--- /dev/null
+++ b/api/boto/s3/connection.py
@@ -0,0 +1,343 @@
+# 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 xml.sax
+import urllib, base64
+import time
+import boto.utils
+import types
+from boto.connection import AWSAuthConnection
+from boto import handler
+from boto.s3.bucket import Bucket
+from boto.s3.key import Key
+from boto.resultset import ResultSet
+from boto.exception import S3ResponseError, S3CreateError, BotoClientError
+
+def assert_case_insensitive(f):
+ def wrapper(*args, **kwargs):
+ if len(args) == 3 and not (args[2].islower() or args[2].isalnum()):
+ raise BotoClientError("Bucket names cannot contain upper-case " \
+ "characters when using either the sub-domain or virtual " \
+ "hosting calling format.")
+ return f(*args, **kwargs)
+ return wrapper
+
+class _CallingFormat:
+ def build_url_base(self, protocol, server, bucket, key=''):
+ url_base = '%s://' % protocol
+ url_base += self.build_host(server, bucket)
+ url_base += self.build_path_base(bucket, key)
+ return url_base
+
+ def build_host(self, server, bucket):
+ if bucket == '':
+ return server
+ else:
+ return self.get_bucket_server(server, bucket)
+
+ def build_auth_path(self, bucket, key=''):
+ path = ''
+ if bucket != '':
+ path = '/' + bucket
+ return path + '/%s' % urllib.quote(key)
+
+ def build_path_base(self, bucket, key=''):
+ return '/%s' % urllib.quote(key)
+
+class SubdomainCallingFormat(_CallingFormat):
+ @assert_case_insensitive
+ def get_bucket_server(self, server, bucket):
+ return '%s.%s' % (bucket, server)
+
+class VHostCallingFormat(_CallingFormat):
+ @assert_case_insensitive
+ def get_bucket_server(self, server, bucket):
+ return bucket
+
+class OrdinaryCallingFormat(_CallingFormat):
+ def get_bucket_server(self, server, bucket):
+ return server
+
+ def build_path_base(self, bucket, key=''):
+ path_base = '/'
+ if bucket:
+ path_base += "%s/" % bucket
+ return path_base + urllib.quote(key)
+
+class Location:
+ DEFAULT = ''
+ EU = 'EU'
+
+class S3Connection(AWSAuthConnection):
+
+ DefaultHost = 's3.amazonaws.com'
+ QueryString = 'Signature=%s&Expires=%d&AWSAccessKeyId=%s'
+
+ 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=DefaultHost, debug=0, https_connection_factory=None,
+ calling_format=SubdomainCallingFormat(), path='/'):
+ self.calling_format = calling_format
+ AWSAuthConnection.__init__(self, host,
+ aws_access_key_id, aws_secret_access_key,
+ is_secure, port, proxy, proxy_port, proxy_user, proxy_pass,
+ debug=debug, https_connection_factory=https_connection_factory,
+ path=path)
+
+ def __iter__(self):
+ return self.get_all_buckets()
+
+ def __contains__(self, bucket_name):
+ return not (self.lookup(bucket_name) is None)
+
+ def build_post_policy(self, expiration_time, conditions):
+ """
+ Taken from the AWS book Python examples and modified for use with boto
+ """
+ if type(expiration_time) != time.struct_time:
+ raise 'Policy document must include a valid expiration Time object'
+
+ # Convert conditions object mappings to condition statements
+
+ return '{"expiration": "%s",\n"conditions": [%s]}' % \
+ (time.strftime(boto.utils.ISO8601, expiration_time), ",".join(conditions))
+
+
+ def build_post_form_args(self, bucket_name, key, expires_in = 6000,
+ acl = None, success_action_redirect = None, max_content_length = None,
+ http_method = "http", fields=None, conditions=None):
+ """
+ Taken from the AWS book Python examples and modified for use with boto
+ This only returns the arguments required for the post form, not the actual form
+ This does not return the file input field which also needs to be added
+
+ :param bucket_name: Bucket to submit to
+ :type bucket_name: string
+
+ :param key: Key name, optionally add ${filename} to the end to attach the submitted filename
+ :type key: string
+
+ :param expires_in: Time (in seconds) before this expires, defaults to 6000
+ :type expires_in: integer
+
+ :param acl: ACL rule to use, if any
+ :type acl: :class:`boto.s3.acl.ACL`
+
+ :param success_action_redirect: URL to redirect to on success
+ :type success_action_redirect: string
+
+ :param max_content_length: Maximum size for this file
+ :type max_content_length: integer
+
+ :type http_method: string
+ :param http_method: HTTP Method to use, "http" or "https"
+
+
+ :rtype: dict
+ :return: A dictionary containing field names/values as well as a url to POST to
+
+ .. code-block:: python
+
+ {
+ "action": action_url_to_post_to,
+ "fields": [
+ {
+ "name": field_name,
+ "value": field_value
+ },
+ {
+ "name": field_name2,
+ "value": field_value2
+ }
+ ]
+ }
+
+ """
+ if fields == None:
+ fields = []
+ if conditions == None:
+ conditions = []
+ expiration = time.gmtime(int(time.time() + expires_in))
+
+ # Generate policy document
+ conditions.append('{"bucket": "%s"}' % bucket_name)
+ if key.endswith("${filename}"):
+ conditions.append('["starts-with", "$key", "%s"]' % key[:-len("${filename}")])
+ else:
+ conditions.append('{"key": "%s"}' % key)
+ if acl:
+ conditions.append('{"acl": "%s"}' % acl)
+ fields.append({ "name": "acl", "value": acl})
+ if success_action_redirect:
+ conditions.append('{"success_action_redirect": "%s"}' % success_action_redirect)
+ fields.append({ "name": "success_action_redirect", "value": success_action_redirect})
+ if max_content_length:
+ conditions.append('["content-length-range", 0, %i]' % max_content_length)
+ fields.append({"name":'content-length-range', "value": "0,%i" % max_content_length})
+
+ policy = self.build_post_policy(expiration, conditions)
+
+ # Add the base64-encoded policy document as the 'policy' field
+ policy_b64 = base64.b64encode(policy)
+ fields.append({"name": "policy", "value": policy_b64})
+
+ # Add the AWS access key as the 'AWSAccessKeyId' field
+ fields.append({"name": "AWSAccessKeyId", "value": self.aws_access_key_id})
+
+ # Add signature for encoded policy document as the 'AWSAccessKeyId' field
+ hmac_copy = self.hmac.copy()
+ hmac_copy.update(policy_b64)
+ signature = base64.encodestring(hmac_copy.digest()).strip()
+ fields.append({"name": "signature", "value": signature})
+ fields.append({"name": "key", "value": key})
+
+ # HTTPS protocol will be used if the secure HTTP option is enabled.
+ url = '%s://%s.s3.amazonaws.com/' % (http_method, bucket_name)
+
+ return {"action": url, "fields": fields}
+
+
+ def generate_url(self, expires_in, method, bucket='', key='',
+ headers=None, query_auth=True, force_http=False):
+ if not headers:
+ headers = {}
+ expires = int(time.time() + expires_in)
+ auth_path = self.calling_format.build_auth_path(bucket, key)
+ canonical_str = boto.utils.canonical_string(method, auth_path,
+ headers, expires)
+ hmac_copy = self.hmac.copy()
+ hmac_copy.update(canonical_str)
+ b64_hmac = base64.encodestring(hmac_copy.digest()).strip()
+ encoded_canonical = urllib.quote_plus(b64_hmac)
+ path = self.calling_format.build_path_base(bucket, key)
+ if query_auth:
+ query_part = '?' + self.QueryString % (encoded_canonical, expires,
+ self.aws_access_key_id)
+ if 'x-amz-security-token' in headers:
+ query_part += '&x-amz-security-token=%s' % urllib.quote(headers['x-amz-security-token']);
+ else:
+ query_part = ''
+ if force_http:
+ protocol = 'http'
+ port = 80
+ else:
+ protocol = self.protocol
+ port = self.port
+ return self.calling_format.build_url_base(protocol, self.server_name(port),
+ bucket, key) + query_part
+
+ def get_all_buckets(self, headers=None):
+ response = self.make_request('GET')
+ body = response.read()
+ if response.status > 300:
+ raise S3ResponseError(response.status, response.reason, body, headers=headers)
+ rs = ResultSet([('Bucket', Bucket)])
+ h = handler.XmlHandler(rs, self)
+ xml.sax.parseString(body, h)
+ return rs
+
+ def get_canonical_user_id(self, headers=None):
+ """
+ Convenience method that returns the "CanonicalUserID" of the user who's credentials
+ are associated with the connection. The only way to get this value is to do a GET
+ request on the service which returns all buckets associated with the account. As part
+ of that response, the canonical userid is returned. This method simply does all of
+ that and then returns just the user id.
+
+ :rtype: string
+ :return: A string containing the canonical user id.
+ """
+ rs = self.get_all_buckets(headers=headers)
+ return rs.ID
+
+ def get_bucket(self, bucket_name, validate=True, headers=None):
+ bucket = Bucket(self, bucket_name)
+ if validate:
+ rs = bucket.get_all_keys(headers, maxkeys=0)
+ return bucket
+
+ def lookup(self, bucket_name, validate=True, headers=None):
+ try:
+ bucket = self.get_bucket(bucket_name, validate, headers=headers)
+ except:
+ bucket = None
+ return bucket
+
+ def create_bucket(self, bucket_name, headers=None, location=Location.DEFAULT, policy=None):
+ """
+ Creates a new located bucket. By default it's in the USA. You can pass
+ Location.EU to create an European bucket.
+
+ :type bucket_name: string
+ :param bucket_name: The name of the new bucket
+
+ :type headers: dict
+ :param headers: Additional headers to pass along with the request to AWS.
+
+ :type location: :class:`boto.s3.connection.Location`
+ :param location: The location of the new bucket
+
+ :type policy: :class:`boto.s3.acl.CannedACLStrings`
+ :param policy: A canned ACL policy that will be applied to the new key in S3.
+
+ """
+ if policy:
+ if headers:
+ headers['x-amz-acl'] = policy
+ else:
+ headers = {'x-amz-acl' : policy}
+ if location == Location.DEFAULT:
+ data = ''
+ else:
+ data = '' + \
+ location + ''
+ response = self.make_request('PUT', bucket_name, headers=headers,
+ data=data)
+ body = response.read()
+ if response.status == 409:
+ raise S3CreateError(response.status, response.reason, body)
+ if response.status == 200:
+ return Bucket(self, bucket_name)
+ else:
+ raise S3ResponseError(response.status, response.reason, body)
+
+ def delete_bucket(self, bucket, headers=None):
+ response = self.make_request('DELETE', bucket, headers=headers)
+ body = response.read()
+ if response.status != 204:
+ raise S3ResponseError(response.status, response.reason, body)
+
+ def make_request(self, method, bucket='', key='', headers=None, data='',
+ query_args=None, sender=None):
+ if isinstance(bucket, Bucket):
+ bucket = bucket.name
+ if isinstance(key, Key):
+ key = key.name
+ path = self.calling_format.build_path_base(bucket, key)
+ auth_path = self.calling_format.build_auth_path(bucket, key)
+ host = self.calling_format.build_host(self.server_name(), bucket)
+ if query_args:
+ path += '?' + query_args
+ auth_path += '?' + query_args
+ return AWSAuthConnection.make_request(self, method, path, headers,
+ data, host, auth_path, sender)
+
diff --git a/api/boto/s3/key.py b/api/boto/s3/key.py
new file mode 100644
index 0000000..ada4352
--- /dev/null
+++ b/api/boto/s3/key.py
@@ -0,0 +1,764 @@
+# 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 mimetypes
+import os
+import rfc822
+import StringIO
+import base64
+import boto.utils
+from boto.exception import S3ResponseError, S3DataError, BotoClientError
+from boto.s3.user import User
+from boto import UserAgent, config
+try:
+ from hashlib import md5
+except ImportError:
+ from md5 import md5
+
+class Key(object):
+
+ DefaultContentType = 'application/octet-stream'
+
+ BufferSize = 8192
+
+ def __init__(self, bucket=None, name=None):
+ self.bucket = bucket
+ self.name = name
+ self.metadata = {}
+ self.content_type = self.DefaultContentType
+ self.content_encoding = None
+ self.filename = None
+ self.etag = None
+ self.last_modified = None
+ self.owner = None
+ self.storage_class = None
+ self.md5 = None
+ self.base64md5 = None
+ self.path = None
+ self.resp = None
+ self.mode = None
+ self.size = None
+
+ def __repr__(self):
+ if self.bucket:
+ return '' % (self.bucket.name, self.name)
+ else:
+ return '' % self.name
+
+ def __getattr__(self, name):
+ if name == 'key':
+ return self.name
+ else:
+ raise AttributeError
+
+ def __setattr__(self, name, value):
+ if name == 'key':
+ self.__dict__['name'] = value
+ else:
+ self.__dict__[name] = value
+
+ def __iter__(self):
+ return self
+
+ def open_read(self, headers=None, query_args=None):
+ """
+ Open this key for reading
+
+ :type headers: dict
+ :param headers: Headers to pass in the web request
+
+ :type query_args: string
+ :param query_args: Arguments to pass in the query string (ie, 'torrent')
+ """
+ if self.resp == None:
+ self.mode = 'r'
+
+ self.resp = self.bucket.connection.make_request('GET', self.bucket.name, self.name, headers, query_args=query_args)
+ if self.resp.status < 199 or self.resp.status > 299:
+ raise S3ResponseError(self.resp.status, self.resp.reason)
+ response_headers = self.resp.msg
+ self.metadata = boto.utils.get_aws_metadata(response_headers)
+ for name,value in response_headers.items():
+ if name.lower() == 'content-length':
+ self.size = int(value)
+ elif name.lower() == 'etag':
+ self.etag = value
+ elif name.lower() == 'content-type':
+ self.content_type = value
+ elif name.lower() == 'content-encoding':
+ self.content_encoding = value
+ elif name.lower() == 'last-modified':
+ self.last_modified = value
+
+ def open_write(self, headers=None):
+ """
+ Open this key for writing.
+ Not yet implemented
+
+ :type headers: dict
+ :param headers: Headers to pass in the write request
+ """
+ raise BotoClientError('Not Implemented')
+
+ def open(self, mode='r', headers=None, query_args=None):
+ if mode == 'r':
+ self.mode = 'r'
+ self.open_read(headers=headers, query_args=query_args)
+ elif mode == 'w':
+ self.mode = 'w'
+ self.open_write(headers=headers)
+ else:
+ raise BotoClientError('Invalid mode: %s' % mode)
+
+ closed = False
+ def close(self):
+ if self.resp:
+ self.resp.read()
+ self.resp = None
+ self.mode = None
+ self.closed = True
+
+ def next(self):
+ """
+ By providing a next method, the key object supports use as an iterator.
+ For example, you can now say:
+
+ for bytes in key:
+ write bytes to a file or whatever
+
+ All of the HTTP connection stuff is handled for you.
+ """
+ self.open_read()
+ data = self.resp.read(self.BufferSize)
+ if not data:
+ self.close()
+ raise StopIteration
+ return data
+
+ def read(self, size=0):
+ if size == 0:
+ size = self.BufferSize
+ self.open_read()
+ data = self.resp.read(size)
+ if not data:
+ self.close()
+ return data
+
+ def copy(self, dst_bucket, dst_key, metadata=None):
+ """
+ Copy this Key to another bucket.
+
+ :type dst_bucket: string
+ :param dst_bucket: The name of the destination bucket
+
+ :type dst_key: string
+ :param dst_key: The name of the destinatino key
+
+ :type metadata: dict
+ :param metadata: Metadata to be associated with new key.
+ If metadata is supplied, it will replace the
+ metadata of the source key being copied.
+ If no metadata is supplied, the source key's
+ metadata will be copied to the new key.
+
+ :rtype: :class:`boto.s3.key.Key` or subclass
+ :returns: An instance of the newly created key object
+ """
+ dst_bucket = self.bucket.connection.lookup(dst_bucket)
+ return dst_bucket.copy_key(dst_key, self.bucket.name, self.name, metadata)
+
+ def startElement(self, name, attrs, connection):
+ if name == 'Owner':
+ self.owner = User(self)
+ return self.owner
+ else:
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'Key':
+ self.name = value.encode('utf-8')
+ elif name == 'ETag':
+ self.etag = value
+ elif name == 'LastModified':
+ self.last_modified = value
+ elif name == 'Size':
+ self.size = int(value)
+ elif name == 'StorageClass':
+ self.storage_class = value
+ elif name == 'Owner':
+ pass
+ else:
+ setattr(self, name, value)
+
+ def exists(self):
+ """
+ Returns True if the key exists
+
+ :rtype: bool
+ :return: Whether the key exists on S3
+ """
+ return bool(self.bucket.lookup(self.name))
+
+ def delete(self):
+ """
+ Delete this key from S3
+ """
+ return self.bucket.delete_key(self.name)
+
+ def get_metadata(self, name):
+ return self.metadata.get(name)
+
+ def set_metadata(self, name, value):
+ self.metadata[name] = value
+
+ def update_metadata(self, d):
+ self.metadata.update(d)
+
+ # convenience methods for setting/getting ACL
+ def set_acl(self, acl_str, headers=None):
+ if self.bucket != None:
+ self.bucket.set_acl(acl_str, self.name, headers=headers)
+
+ def get_acl(self, headers=None):
+ if self.bucket != None:
+ return self.bucket.get_acl(self.name, headers=headers)
+
+ def get_xml_acl(self, headers=None):
+ if self.bucket != None:
+ return self.bucket.get_xml_acl(self.name, headers=headers)
+
+ def set_xml_acl(self, acl_str, headers=None):
+ if self.bucket != None:
+ return self.bucket.set_xml_acl(acl_str, self.name, headers=headers)
+
+ def set_canned_acl(self, acl_str, headers=None):
+ return self.bucket.set_canned_acl(acl_str, self.name, headers)
+
+ def make_public(self, headers=None):
+ return self.bucket.set_canned_acl('public-read', self.name, headers)
+
+ def generate_url(self, expires_in, method='GET', headers=None,
+ query_auth=True, force_http=False):
+ """
+ Generate a URL to access this key.
+
+ :type expires_in: int
+ :param expires_in: How long the url is valid for, in seconds
+
+ :type method: string
+ :param method: The method to use for retrieving the file (default is GET)
+
+ :type headers: dict
+ :param headers: Any headers to pass along in the request
+
+ :type query_auth: bool
+ :param query_auth:
+
+ :rtype: string
+ :return: The URL to access the key
+ """
+ return self.bucket.connection.generate_url(expires_in, method,
+ self.bucket.name, self.name,
+ headers, query_auth, force_http)
+
+ def send_file(self, fp, headers=None, cb=None, num_cb=10):
+ """
+ Upload a file to a key into a bucket on S3.
+
+ :type fp: file
+ :param fp: The file pointer to upload
+
+ :type headers: dict
+ :param headers: The headers to pass along with the PUT request
+
+ :type cb: function
+ :param cb: a callback function that will be called to report
+ progress on the upload. The callback should accept two integer
+ parameters, the first representing the number of bytes that have
+ been successfully transmitted to S3 and the second representing
+ the total number of bytes that need to be transmitted.
+
+ :type cb: int
+ :param num_cb: (optional) If a callback is specified with the cb parameter
+ this parameter determines the granularity of the callback by defining
+ the maximum number of times the callback will be called during the file transfer.
+
+ """
+ def sender(http_conn, method, path, data, headers):
+ http_conn.putrequest(method, path)
+ for key in headers:
+ http_conn.putheader(key, headers[key])
+ http_conn.endheaders()
+ fp.seek(0)
+ save_debug = self.bucket.connection.debug
+ self.bucket.connection.debug = 0
+ if cb:
+ if num_cb > 2:
+ cb_count = self.size / self.BufferSize / (num_cb-2)
+ else:
+ cb_count = 0
+ i = total_bytes = 0
+ cb(total_bytes, self.size)
+ l = fp.read(self.BufferSize)
+ while len(l) > 0:
+ http_conn.send(l)
+ if cb:
+ total_bytes += len(l)
+ i += 1
+ if i == cb_count:
+ cb(total_bytes, self.size)
+ i = 0
+ l = fp.read(self.BufferSize)
+ if cb:
+ cb(total_bytes, self.size)
+ response = http_conn.getresponse()
+ body = response.read()
+ fp.seek(0)
+ self.bucket.connection.debug = save_debug
+ if response.status == 500 or response.status == 503 or \
+ response.getheader('location'):
+ # we'll try again
+ return response
+ elif response.status >= 200 and response.status <= 299:
+ self.etag = response.getheader('etag')
+ if self.etag != '"%s"' % self.md5:
+ raise S3DataError('ETag from S3 did not match computed MD5')
+ return response
+ else:
+ raise S3ResponseError(response.status, response.reason, body)
+
+ if not headers:
+ headers = {}
+ else:
+ headers = headers.copy()
+ headers['User-Agent'] = UserAgent
+ headers['Content-MD5'] = self.base64md5
+ if headers.has_key('Content-Type'):
+ self.content_type = headers['Content-Type']
+ elif self.path:
+ self.content_type = mimetypes.guess_type(self.path)[0]
+ if self.content_type == None:
+ self.content_type = self.DefaultContentType
+ headers['Content-Type'] = self.content_type
+ else:
+ headers['Content-Type'] = self.content_type
+ headers['Content-Length'] = str(self.size)
+ headers['Expect'] = '100-Continue'
+ headers = boto.utils.merge_meta(headers, self.metadata)
+ return self.bucket.connection.make_request('PUT', self.bucket.name,
+ self.name, headers, sender=sender)
+
+ def compute_md5(self, fp):
+ """
+ :type fp: file
+ :param fp: File pointer to the file to MD5 hash. The file pointer will be
+ reset to the beginning of the file before the method returns.
+
+ :rtype: tuple
+ :return: A tuple containing the hex digest version of the MD5 hash
+ as the first element and the base64 encoded version of the
+ plain digest as the second element.
+ """
+ m = md5()
+ fp.seek(0)
+ s = fp.read(self.BufferSize)
+ while s:
+ m.update(s)
+ s = fp.read(self.BufferSize)
+ hex_md5 = m.hexdigest()
+ base64md5 = base64.encodestring(m.digest())
+ if base64md5[-1] == '\n':
+ base64md5 = base64md5[0:-1]
+ self.size = fp.tell()
+ fp.seek(0)
+ return (hex_md5, base64md5)
+
+ def set_contents_from_file(self, fp, headers=None, replace=True, cb=None, num_cb=10,
+ policy=None, md5=None):
+ """
+ Store an object in S3 using the name of the Key object as the
+ key in S3 and the contents of the file pointed to by 'fp' as the
+ contents.
+
+ :type fp: file
+ :param fp: the file whose contents to upload
+
+ :type headers: dict
+ :param headers: additional HTTP headers that will be sent with the PUT request.
+
+ :type replace: bool
+ :param replace: If this parameter is False, the method
+ will first check to see if an object exists in the
+ bucket with the same key. If it does, it won't
+ overwrite it. The default value is True which will
+ overwrite the object.
+
+ :type cb: function
+ :param cb: a callback function that will be called to report
+ progress on the upload. The callback should accept two integer
+ parameters, the first representing the number of bytes that have
+ been successfully transmitted to S3 and the second representing
+ the total number of bytes that need to be transmitted.
+
+ :type cb: int
+ :param num_cb: (optional) If a callback is specified with the cb parameter
+ this parameter determines the granularity of the callback by defining
+ the maximum number of times the callback will be called during the file transfer.
+
+ :type policy: :class:`boto.s3.acl.CannedACLStrings`
+ :param policy: A canned ACL policy that will be applied to the new key in S3.
+
+ :type md5: A tuple containing the hexdigest version of the MD5 checksum of the
+ file as the first element and the Base64-encoded version of the plain
+ checksum as the second element. This is the same format returned by
+ the compute_md5 method.
+ :param md5: If you need to compute the MD5 for any reason prior to upload,
+ it's silly to have to do it twice so this param, if present, will be
+ used as the MD5 values of the file. Otherwise, the checksum will be computed.
+ """
+ if policy:
+ if headers:
+ headers['x-amz-acl'] = policy
+ else:
+ headers = {'x-amz-acl' : policy}
+ if hasattr(fp, 'name'):
+ self.path = fp.name
+ if self.bucket != None:
+ if not md5:
+ md5 = self.compute_md5(fp)
+ self.md5 = md5[0]
+ self.base64md5 = md5[1]
+ if self.name == None:
+ self.name = self.md5
+ if not replace:
+ k = self.bucket.lookup(self.name)
+ if k:
+ return
+ self.send_file(fp, headers, cb, num_cb)
+
+ def set_contents_from_filename(self, filename, headers=None, replace=True, cb=None, num_cb=10,
+ policy=None, md5=None):
+ """
+ Store an object in S3 using the name of the Key object as the
+ key in S3 and the contents of the file named by 'filename'.
+ See set_contents_from_file method for details about the
+ parameters.
+
+ :type filename: string
+ :param filename: The name of the file that you want to put onto S3
+
+ :type headers: dict
+ :param headers: Additional headers to pass along with the request to AWS.
+
+ :type replace: bool
+ :param replace: If True, replaces the contents of the file if it already exists.
+
+ :type cb: function
+ :param cb: (optional) a callback function that will be called to report
+ progress on the download. The callback should accept two integer
+ parameters, the first representing the number of bytes that have
+ been successfully transmitted from S3 and the second representing
+ the total number of bytes that need to be transmitted.
+
+ :type cb: int
+ :param num_cb: (optional) If a callback is specified with the cb parameter
+ this parameter determines the granularity of the callback by defining
+ the maximum number of times the callback will be called during the file transfer.
+
+ :type policy: :class:`boto.s3.acl.CannedACLStrings`
+ :param policy: A canned ACL policy that will be applied to the new key in S3.
+
+ :type md5: A tuple containing the hexdigest version of the MD5 checksum of the
+ file as the first element and the Base64-encoded version of the plain
+ checksum as the second element. This is the same format returned by
+ the compute_md5 method.
+ :param md5: If you need to compute the MD5 for any reason prior to upload,
+ it's silly to have to do it twice so this param, if present, will be
+ used as the MD5 values of the file. Otherwise, the checksum will be computed.
+ """
+ fp = open(filename, 'rb')
+ self.set_contents_from_file(fp, headers, replace, cb, num_cb, policy)
+ fp.close()
+
+ def set_contents_from_string(self, s, headers=None, replace=True, cb=None, num_cb=10,
+ policy=None, md5=None):
+ """
+ Store an object in S3 using the name of the Key object as the
+ key in S3 and the string 's' as the contents.
+ See set_contents_from_file method for details about the
+ parameters.
+
+ :type headers: dict
+ :param headers: Additional headers to pass along with the request to AWS.
+
+ :type replace: bool
+ :param replace: If True, replaces the contents of the file if it already exists.
+
+ :type cb: function
+ :param cb: (optional) a callback function that will be called to report
+ progress on the download. The callback should accept two integer
+ parameters, the first representing the number of bytes that have
+ been successfully transmitted from S3 and the second representing
+ the total number of bytes that need to be transmitted.
+
+ :type cb: int
+ :param num_cb: (optional) If a callback is specified with the cb parameter
+ this parameter determines the granularity of the callback by defining
+ the maximum number of times the callback will be called during the file transfer.
+
+ :type policy: :class:`boto.s3.acl.CannedACLStrings`
+ :param policy: A canned ACL policy that will be applied to the new key in S3.
+
+ :type md5: A tuple containing the hexdigest version of the MD5 checksum of the
+ file as the first element and the Base64-encoded version of the plain
+ checksum as the second element. This is the same format returned by
+ the compute_md5 method.
+ :param md5: If you need to compute the MD5 for any reason prior to upload,
+ it's silly to have to do it twice so this param, if present, will be
+ used as the MD5 values of the file. Otherwise, the checksum will be computed.
+ """
+ fp = StringIO.StringIO(s)
+ self.set_contents_from_file(fp, headers, replace, cb, num_cb, policy)
+ fp.close()
+
+ def get_file(self, fp, headers=None, cb=None, num_cb=10, torrent=False):
+ """
+ Retrieves a file from an S3 Key
+
+ :type fp: file
+ :param fp: File pointer to put the data into
+
+ :type headers: string
+ :param: headers to send when retrieving the files
+
+ :type cb: function
+ :param cb: (optional) a callback function that will be called to report
+ progress on the download. The callback should accept two integer
+ parameters, the first representing the number of bytes that have
+ been successfully transmitted from S3 and the second representing
+ the total number of bytes that need to be transmitted.
+
+
+ :type cb: int
+ :param num_cb: (optional) If a callback is specified with the cb parameter
+ this parameter determines the granularity of the callback by defining
+ the maximum number of times the callback will be called during the file transfer.
+
+ :type torrent: bool
+ :param torrent: Flag for whether to get a torrent for the file
+ """
+ if cb:
+ if num_cb > 2:
+ cb_count = self.size / self.BufferSize / (num_cb-2)
+ else:
+ cb_count = 0
+ i = total_bytes = 0
+ cb(total_bytes, self.size)
+ save_debug = self.bucket.connection.debug
+ if self.bucket.connection.debug == 1:
+ self.bucket.connection.debug = 0
+
+ if torrent: torrent = "torrent"
+ self.open('r', headers, query_args=torrent)
+ for bytes in self:
+ fp.write(bytes)
+ if cb:
+ total_bytes += len(bytes)
+ i += 1
+ if i == cb_count:
+ cb(total_bytes, self.size)
+ i = 0
+ if cb:
+ cb(total_bytes, self.size)
+ self.close()
+ self.bucket.connection.debug = save_debug
+
+ def get_torrent_file(self, fp, headers=None, cb=None, num_cb=10):
+ """
+ Get a torrent file (see to get_file)
+
+ :type fp: file
+ :param fp: The file pointer of where to put the torrent
+
+ :type headers: dict
+ :param headers: Headers to be passed
+
+ :type cb: function
+ :param cb: Callback function to call on retrieved data
+
+ :type cb: int
+ :param num_cb: (optional) If a callback is specified with the cb parameter
+ this parameter determines the granularity of the callback by defining
+ the maximum number of times the callback will be called during the file transfer.
+
+ """
+ return self.get_file(fp, headers, cb, num_cb, torrent=True)
+
+ def get_contents_to_file(self, fp, headers=None, cb=None, num_cb=10, torrent=False):
+ """
+ Retrieve an object from S3 using the name of the Key object as the
+ key in S3. Write the contents of the object to the file pointed
+ to by 'fp'.
+
+ :type fp: File -like object
+ :param fp:
+
+ :type headers: dict
+ :param headers: additional HTTP headers that will be sent with the GET request.
+
+ :type cb: function
+ :param cb: (optional) a callback function that will be called to report
+ progress on the download. The callback should accept two integer
+ parameters, the first representing the number of bytes that have
+ been successfully transmitted from S3 and the second representing
+ the total number of bytes that need to be transmitted.
+
+
+ :type cb: int
+ :param num_cb: (optional) If a callback is specified with the cb parameter
+ this parameter determines the granularity of the callback by defining
+ the maximum number of times the callback will be called during the file transfer.
+
+ :type torrent: bool
+ :param torrent: If True, returns the contents of a torrent file as a string.
+
+ """
+ if self.bucket != None:
+ self.get_file(fp, headers, cb, num_cb, torrent=torrent)
+
+ def get_contents_to_filename(self, filename, headers=None, cb=None, num_cb=10, torrent=False):
+ """
+ Retrieve an object from S3 using the name of the Key object as the
+ key in S3. Store contents of the object to a file named by 'filename'.
+ See get_contents_to_file method for details about the
+ parameters.
+
+ :type filename: string
+ :param filename: The filename of where to put the file contents
+
+ :type headers: dict
+ :param headers: Any additional headers to send in the request
+
+ :type cb: function
+ :param cb: (optional) a callback function that will be called to report
+ progress on the download. The callback should accept two integer
+ parameters, the first representing the number of bytes that have
+ been successfully transmitted from S3 and the second representing
+ the total number of bytes that need to be transmitted.
+
+
+ :type cb: int
+ :param num_cb: (optional) If a callback is specified with the cb parameter
+ this parameter determines the granularity of the callback by defining
+ the maximum number of times the callback will be called during the file transfer.
+
+ :type torrent: bool
+ :param torrent: If True, returns the contents of a torrent file as a string.
+
+ """
+ fp = open(filename, 'wb')
+ self.get_contents_to_file(fp, headers, cb, num_cb, torrent=torrent)
+ fp.close()
+ # if last_modified date was sent from s3, try to set file's timestamp
+ if self.last_modified != None:
+ try:
+ modified_tuple = rfc822.parsedate_tz(self.last_modified)
+ modified_stamp = int(rfc822.mktime_tz(modified_tuple))
+ os.utime(fp.name, (modified_stamp, modified_stamp))
+ except Exception: pass
+
+ def get_contents_as_string(self, headers=None, cb=None, num_cb=10, torrent=False):
+ """
+ Retrieve an object from S3 using the name of the Key object as the
+ key in S3. Return the contents of the object as a string.
+ See get_contents_to_file method for details about the
+ parameters.
+
+ :type headers: dict
+ :param headers: Any additional headers to send in the request
+
+ :type cb: function
+ :param cb: (optional) a callback function that will be called to report
+ progress on the download. The callback should accept two integer
+ parameters, the first representing the number of bytes that have
+ been successfully transmitted from S3 and the second representing
+ the total number of bytes that need to be transmitted.
+
+ :type cb: int
+ :param num_cb: (optional) If a callback is specified with the cb parameter
+ this parameter determines the granularity of the callback by defining
+ the maximum number of times the callback will be called during the file transfer.
+
+
+ :type cb: int
+ :param num_cb: (optional) If a callback is specified with the cb parameter
+ this parameter determines the granularity of the callback by defining
+ the maximum number of times the callback will be called during the file transfer.
+
+ :type torrent: bool
+ :param torrent: If True, returns the contents of a torrent file as a string.
+
+ :rtype: string
+ :returns: The contents of the file as a string
+ """
+ fp = StringIO.StringIO()
+ self.get_contents_to_file(fp, headers, cb, num_cb, torrent=torrent)
+ return fp.getvalue()
+
+ def add_email_grant(self, permission, email_address):
+ """
+ Convenience method that provides a quick way to add an email grant to a key.
+ This method retrieves the current ACL, creates a new grant based on the parameters
+ passed in, adds that grant to the ACL and then PUT's the new ACL back to S3.
+
+ :type permission: string
+ :param permission: The permission being granted. Should be one of:
+ READ|WRITE|READ_ACP|WRITE_ACP|FULL_CONTROL
+ See http://docs.amazonwebservices.com/AmazonS3/2006-03-01/UsingAuthAccess.html
+ for more details on permissions.
+
+ :type email_address: string
+ :param email_address: The email address associated with the AWS account your are granting
+ the permission to.
+ """
+ policy = self.get_acl()
+ policy.acl.add_email_grant(permission, email_address)
+ self.set_acl(policy)
+
+ def add_user_grant(self, permission, user_id):
+ """
+ Convenience method that provides a quick way to add a canonical user grant to a key.
+ This method retrieves the current ACL, creates a new grant based on the parameters
+ passed in, adds that grant to the ACL and then PUT's the new ACL back to S3.
+
+ :type permission: string
+ :param permission: The permission being granted. Should be one of:
+ READ|WRITE|READ_ACP|WRITE_ACP|FULL_CONTROL
+ See http://docs.amazonwebservices.com/AmazonS3/2006-03-01/UsingAuthAccess.html
+ for more details on permissions.
+
+ :type user_id: string
+ :param user_id: The canonical user id associated with the AWS account your are granting
+ the permission to.
+ """
+ policy = self.get_acl()
+ policy.acl.add_user_grant(permission, user_id)
+ self.set_acl(policy)
diff --git a/api/boto/s3/prefix.py b/api/boto/s3/prefix.py
new file mode 100644
index 0000000..fc0f26a
--- /dev/null
+++ b/api/boto/s3/prefix.py
@@ -0,0 +1,35 @@
+# 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.
+
+class Prefix:
+ def __init__(self, bucket=None, name=None):
+ self.bucket = bucket
+ self.name = name
+
+ def startElement(self, name, attrs, connection):
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'Prefix':
+ self.name = value
+ else:
+ setattr(self, name, value)
+
diff --git a/api/boto/s3/user.py b/api/boto/s3/user.py
new file mode 100644
index 0000000..f45f038
--- /dev/null
+++ b/api/boto/s3/user.py
@@ -0,0 +1,49 @@
+# 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.
+
+class User:
+ def __init__(self, parent=None, id='', display_name=''):
+ if parent:
+ parent.owner = self
+ self.type = None
+ self.id = id
+ self.display_name = display_name
+
+ def startElement(self, name, attrs, connection):
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'DisplayName':
+ self.display_name = value
+ elif name == 'ID':
+ self.id = value
+ else:
+ setattr(self, name, value)
+
+ def to_xml(self, element_name='Owner'):
+ if self.type:
+ s = '<%s xsi:type="%s">' % (element_name, self.type)
+ else:
+ s = '<%s>' % element_name
+ s += '%s' % self.id
+ s += '%s' % self.display_name
+ s += '%s>' % element_name
+ return s
diff --git a/api/boto/sdb/__init__.py b/api/boto/sdb/__init__.py
new file mode 100644
index 0000000..42af6a9
--- /dev/null
+++ b/api/boto/sdb/__init__.py
@@ -0,0 +1,41 @@
+# 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
+from regioninfo import SDBRegionInfo
+
+def regions():
+ """
+ Get all available regions for the SDB service.
+
+ :rtype: list
+ :return: A list of :class:`boto.sdb.regioninfo.RegionInfo`
+ """
+ return [SDBRegionInfo(name='us-east-1', endpoint='sdb.amazonaws.com'),
+ SDBRegionInfo(name='eu-west-1', endpoint='sdb.eu-west-1.amazonaws.com'),
+ SDBRegionInfo(name='us-west-1', endpoint='sdb.us-west-1.amazonaws.com')]
+
+def connect_to_region(region_name):
+ for region in regions():
+ if region.name == region_name:
+ return region.connect()
+ return None
diff --git a/api/boto/sdb/connection.py b/api/boto/sdb/connection.py
new file mode 100644
index 0000000..28e130a
--- /dev/null
+++ b/api/boto/sdb/connection.py
@@ -0,0 +1,471 @@
+# 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 urllib
+import xml.sax
+import threading
+import boto
+from boto import handler
+from boto.connection import AWSQueryConnection
+from boto.sdb.domain import Domain, DomainMetaData
+from boto.sdb.item import Item
+from boto.sdb.regioninfo import SDBRegionInfo
+from boto.exception import SDBResponseError
+from boto.resultset import ResultSet
+import warnings
+
+class ItemThread(threading.Thread):
+
+ def __init__(self, name, domain_name, item_names):
+ threading.Thread.__init__(self, name=name)
+ print 'starting %s with %d items' % (name, len(item_names))
+ self.domain_name = domain_name
+ self.conn = SDBConnection()
+ self.item_names = item_names
+ self.items = []
+
+ def run(self):
+ for item_name in self.item_names:
+ item = self.conn.get_attributes(self.domain_name, item_name)
+ self.items.append(item)
+
+class SDBConnection(AWSQueryConnection):
+
+ DefaultRegionName = 'us-east-1'
+ DefaultRegionEndpoint = 'sdb.amazonaws.com'
+ APIVersion = '2007-11-07'
+ SignatureVersion = '2'
+ ResponseError = SDBResponseError
+
+ 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='/', converter=None):
+ if not region:
+ region = SDBRegionInfo(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)
+ self.box_usage = 0.0
+ self.converter = converter
+ self.item_cls = Item
+
+ def set_item_cls(self, cls):
+ self.item_cls = cls
+
+ def build_name_value_list(self, params, attributes, replace=False):
+ keys = attributes.keys()
+ keys.sort()
+ i = 1
+ for key in keys:
+ value = attributes[key]
+ if isinstance(value, list):
+ for v in value:
+ params['Attribute.%d.Name'%i] = key
+ if self.converter:
+ v = self.converter.encode(v)
+ params['Attribute.%d.Value'%i] = v
+ if replace:
+ params['Attribute.%d.Replace'%i] = 'true'
+ i += 1
+ else:
+ params['Attribute.%d.Name'%i] = key
+ if self.converter:
+ value = self.converter.encode(value)
+ params['Attribute.%d.Value'%i] = value
+ if replace:
+ params['Attribute.%d.Replace'%i] = 'true'
+ i += 1
+
+ def build_batch_list(self, params, items, replace=False):
+ item_names = items.keys()
+ i = 0
+ for item_name in item_names:
+ j = 0
+ item = items[item_name]
+ attr_names = item.keys()
+ params['Item.%d.ItemName' % i] = item_name
+ for attr_name in attr_names:
+ value = item[attr_name]
+ if isinstance(value, list):
+ for v in value:
+ if self.converter:
+ v = self.converter.encode(v)
+ params['Item.%d.Attribute.%d.Name' % (i,j)] = attr_name
+ params['Item.%d.Attribute.%d.Value' % (i,j)] = v
+ if replace:
+ params['Item.%d.Attribute.%d.Replace' % (i,j)] = 'true'
+ j += 1
+ else:
+ params['Item.%d.Attribute.%d.Name' % (i,j)] = attr_name
+ if self.converter:
+ value = self.converter.encode(value)
+ params['Item.%d.Attribute.%d.Value' % (i,j)] = value
+ if replace:
+ params['Item.%d.Attribute.%d.Replace' % (i,j)] = 'true'
+ j += 1
+ i += 1
+
+ def build_name_list(self, params, attribute_names):
+ i = 1
+ attribute_names.sort()
+ for name in attribute_names:
+ params['Attribute.%d.Name'%i] = name
+ i += 1
+
+ def get_usage(self):
+ """
+ Returns the BoxUsage accumulated on this SDBConnection object.
+
+ :rtype: float
+ :return: The accumulated BoxUsage of all requests made on the connection.
+ """
+ return self.box_usage
+
+ def print_usage(self):
+ """
+ Print the BoxUsage and approximate costs of all requests made on this connection.
+ """
+ print 'Total Usage: %f compute seconds' % self.box_usage
+ cost = self.box_usage * 0.14
+ print 'Approximate Cost: $%f' % cost
+
+ def get_domain(self, domain_name, validate=True):
+ domain = Domain(self, domain_name)
+ if validate:
+ self.select(domain, """select * from `%s` limit 1""" % domain_name)
+ return domain
+
+ def lookup(self, domain_name, validate=True):
+ """
+ Lookup an existing SimpleDB domain
+
+ :type domain_name: string
+ :param domain_name: The name of the new domain
+
+ :rtype: :class:`boto.sdb.domain.Domain` object or None
+ :return: The Domain object or None if the domain does not exist.
+ """
+ try:
+ domain = self.get_domain(domain_name, validate)
+ except:
+ domain = None
+ return domain
+
+ def get_all_domains(self, max_domains=None, next_token=None):
+ params = {}
+ if max_domains:
+ params['MaxNumberOfDomains'] = max_domains
+ if next_token:
+ params['NextToken'] = next_token
+ return self.get_list('ListDomains', params, [('DomainName', Domain)])
+
+ def create_domain(self, domain_name):
+ """
+ Create a SimpleDB domain.
+
+ :type domain_name: string
+ :param domain_name: The name of the new domain
+
+ :rtype: :class:`boto.sdb.domain.Domain` object
+ :return: The newly created domain
+ """
+ params = {'DomainName':domain_name}
+ d = self.get_object('CreateDomain', params, Domain)
+ d.name = domain_name
+ return d
+
+ def get_domain_and_name(self, domain_or_name):
+ if (isinstance(domain_or_name, Domain)):
+ return (domain_or_name, domain_or_name.name)
+ else:
+ return (self.get_domain(domain_or_name), domain_or_name)
+
+ def delete_domain(self, domain_or_name):
+ """
+ Delete a SimpleDB domain.
+
+ :type domain_or_name: string or :class:`boto.sdb.domain.Domain` object.
+ :param domain_or_name: Either the name of a domain or a Domain object
+
+ :rtype: bool
+ :return: True if successful
+
+ B{Note:} This will delete the domain and all items within the domain.
+ """
+ domain, domain_name = self.get_domain_and_name(domain_or_name)
+ params = {'DomainName':domain_name}
+ return self.get_status('DeleteDomain', params)
+
+ def domain_metadata(self, domain_or_name):
+ """
+ Get the Metadata for a SimpleDB domain.
+
+ :type domain_or_name: string or :class:`boto.sdb.domain.Domain` object.
+ :param domain_or_name: Either the name of a domain or a Domain object
+
+ :rtype: :class:`boto.sdb.domain.DomainMetaData` object
+ :return: The newly created domain metadata object
+ """
+ domain, domain_name = self.get_domain_and_name(domain_or_name)
+ params = {'DomainName':domain_name}
+ d = self.get_object('DomainMetadata', params, DomainMetaData)
+ d.domain = domain
+ return d
+
+ def put_attributes(self, domain_or_name, item_name, attributes, replace=True):
+ """
+ Store attributes for a given item in a domain.
+
+ :type domain_or_name: string or :class:`boto.sdb.domain.Domain` object.
+ :param domain_or_name: Either the name of a domain or a Domain object
+
+ :type item_name: string
+ :param item_name: The name of the item whose attributes are being stored.
+
+ :type attribute_names: dict or dict-like object
+ :param attribute_names: The name/value pairs to store as attributes
+
+ :type replace: bool
+ :param replace: Whether the attribute values passed in will replace
+ existing values or will be added as addition values.
+ Defaults to True.
+
+ :rtype: bool
+ :return: True if successful
+ """
+ domain, domain_name = self.get_domain_and_name(domain_or_name)
+ params = {'DomainName' : domain_name,
+ 'ItemName' : item_name}
+ self.build_name_value_list(params, attributes, replace)
+ return self.get_status('PutAttributes', params)
+
+ def batch_put_attributes(self, domain_or_name, items, replace=True):
+ """
+ Store attributes for multiple items in a domain.
+
+ :type domain_or_name: string or :class:`boto.sdb.domain.Domain` object.
+ :param domain_or_name: Either the name of a domain or a Domain object
+
+ :type items: dict or dict-like object
+ :param items: A dictionary-like object. The keys of the dictionary are
+ the item names and the values are themselves dictionaries
+ of attribute names/values, exactly the same as the
+ attribute_names parameter of the scalar put_attributes
+ call.
+
+ :type replace: bool
+ :param replace: Whether the attribute values passed in will replace
+ existing values or will be added as addition values.
+ Defaults to True.
+
+ :rtype: bool
+ :return: True if successful
+ """
+ domain, domain_name = self.get_domain_and_name(domain_or_name)
+ params = {'DomainName' : domain_name}
+ self.build_batch_list(params, items, replace)
+ return self.get_status('BatchPutAttributes', params, verb='POST')
+
+ def get_attributes(self, domain_or_name, item_name, attribute_names=None, item=None):
+ """
+ Retrieve attributes for a given item in a domain.
+
+ :type domain_or_name: string or :class:`boto.sdb.domain.Domain` object.
+ :param domain_or_name: Either the name of a domain or a Domain object
+
+ :type item_name: string
+ :param item_name: The name of the item whose attributes are being retrieved.
+
+ :type attribute_names: string or list of strings
+ :param attribute_names: An attribute name or list of attribute names. This
+ parameter is optional. If not supplied, all attributes
+ will be retrieved for the item.
+
+ :rtype: :class:`boto.sdb.item.Item`
+ :return: An Item mapping type containing the requested attribute name/values
+ """
+ domain, domain_name = self.get_domain_and_name(domain_or_name)
+ params = {'DomainName' : domain_name,
+ 'ItemName' : item_name}
+ if attribute_names:
+ if not isinstance(attribute_names, list):
+ attribute_names = [attribute_names]
+ self.build_list_params(params, attribute_names, 'AttributeName')
+ response = self.make_request('GetAttributes', params)
+ body = response.read()
+ if response.status == 200:
+ if item == None:
+ item = self.item_cls(domain, item_name)
+ h = handler.XmlHandler(item, self)
+ xml.sax.parseString(body, h)
+ return item
+ else:
+ raise SDBResponseError(response.status, response.reason, body)
+
+ def delete_attributes(self, domain_or_name, item_name, attr_names=None):
+ """
+ Delete attributes from a given item in a domain.
+
+ :type domain_or_name: string or :class:`boto.sdb.domain.Domain` object.
+ :param domain_or_name: Either the name of a domain or a Domain object
+
+ :type item_name: string
+ :param item_name: The name of the item whose attributes are being deleted.
+
+ :type attributes: dict, list or :class:`boto.sdb.item.Item`
+ :param attributes: Either a list containing attribute names which will cause
+ all values associated with that attribute name to be deleted or
+ a dict or Item containing the attribute names and keys and list
+ of values to delete as the value. If no value is supplied,
+ all attribute name/values for the item will be deleted.
+
+ :rtype: bool
+ :return: True if successful
+ """
+ domain, domain_name = self.get_domain_and_name(domain_or_name)
+ params = {'DomainName':domain_name,
+ 'ItemName' : item_name}
+ if attr_names:
+ if isinstance(attr_names, list):
+ self.build_name_list(params, attr_names)
+ elif isinstance(attr_names, dict) or isinstance(attr_names, self.item_cls):
+ self.build_name_value_list(params, attr_names)
+ return self.get_status('DeleteAttributes', params)
+
+ def query(self, domain_or_name, query='', max_items=None, next_token=None):
+ """
+ Returns a list of item names within domain_name that match the query.
+
+ :type domain_or_name: string or :class:`boto.sdb.domain.Domain` object.
+ :param domain_or_name: Either the name of a domain or a Domain object
+
+ :type query: string
+ :param query: The SimpleDB query to be performed.
+
+ :type max_items: int
+ :param max_items: The maximum number of items to return. If not
+ supplied, the default is None which returns all
+ items matching the query.
+
+ :rtype: ResultSet
+ :return: An iterator containing the results.
+ """
+ warnings.warn('Query interface is deprecated', DeprecationWarning)
+ domain, domain_name = self.get_domain_and_name(domain_or_name)
+ params = {'DomainName':domain_name,
+ 'QueryExpression' : query}
+ if max_items:
+ params['MaxNumberOfItems'] = max_items
+ if next_token:
+ params['NextToken'] = next_token
+ return self.get_object('Query', params, ResultSet)
+
+ def query_with_attributes(self, domain_or_name, query='', attr_names=None,
+ max_items=None, next_token=None):
+ """
+ Returns a set of Attributes for item names within domain_name that match the query.
+
+ :type domain_or_name: string or :class:`boto.sdb.domain.Domain` object.
+ :param domain_or_name: Either the name of a domain or a Domain object
+
+ :type query: string
+ :param query: The SimpleDB query to be performed.
+
+ :type attr_names: list
+ :param attr_names: The name of the attributes to be returned.
+ If no attributes are specified, all attributes
+ will be returned.
+
+ :type max_items: int
+ :param max_items: The maximum number of items to return. If not
+ supplied, the default is None which returns all
+ items matching the query.
+
+ :rtype: ResultSet
+ :return: An iterator containing the results.
+ """
+ warnings.warn('Query interface is deprecated', DeprecationWarning)
+ domain, domain_name = self.get_domain_and_name(domain_or_name)
+ params = {'DomainName':domain_name,
+ 'QueryExpression' : query}
+ if max_items:
+ params['MaxNumberOfItems'] = max_items
+ if next_token:
+ params['NextToken'] = next_token
+ if attr_names:
+ self.build_list_params(params, attr_names, 'AttributeName')
+ return self.get_list('QueryWithAttributes', params, [('Item', self.item_cls)], parent=domain)
+
+ def select(self, domain_or_name, query='', next_token=None):
+ """
+ Returns a set of Attributes for item names within domain_name that match the query.
+ The query must be expressed in using the SELECT style syntax rather than the
+ original SimpleDB query language.
+ Even though the select request does not require a domain object, a domain
+ object must be passed into this method so the Item objects returned can
+ point to the appropriate domain.
+
+ :type domain_or_name: string or :class:`boto.sdb.domain.Domain` object.
+ :param domain_or_name: Either the name of a domain or a Domain object
+
+ :type query: string
+ :param query: The SimpleDB query to be performed.
+
+ :rtype: ResultSet
+ :return: An iterator containing the results.
+ """
+ domain, domain_name = self.get_domain_and_name(domain_or_name)
+ params = {'SelectExpression' : query}
+ if next_token:
+ params['NextToken'] = next_token
+ return self.get_list('Select', params, [('Item', self.item_cls)], parent=domain)
+
+ def threaded_query(self, domain_or_name, query='', max_items=None, next_token=None, num_threads=6):
+ """
+ Returns a list of fully populated items that match the query provided.
+
+ The name/value pairs for all of the matching item names are retrieved in a number of separate
+ threads (specified by num_threads) to achieve maximum throughput.
+ The ResultSet that is returned has an attribute called next_token that can be used
+ to retrieve additional results for the same query.
+ """
+ domain, domain_name = self.get_domain_and_name(domain_or_name)
+ if max_items and num_threads > max_items:
+ num_threads = max_items
+ rs = self.query(domain_or_name, query, max_items, next_token)
+ threads = []
+ n = len(rs) / num_threads
+ for i in range(0, num_threads):
+ if i+1 == num_threads:
+ thread = ItemThread('Thread-%d' % i, domain_name, rs[n*i:])
+ else:
+ thread = ItemThread('Thread-%d' % i, domain_name, rs[n*i:n*(i+1)])
+ threads.append(thread)
+ thread.start()
+ del rs[0:]
+ for thread in threads:
+ thread.join()
+ for item in thread.items:
+ rs.append(item)
+ return rs
+
diff --git a/api/boto/sdb/db/__init__.py b/api/boto/sdb/db/__init__.py
new file mode 100644
index 0000000..86044ed
--- /dev/null
+++ b/api/boto/sdb/db/__init__.py
@@ -0,0 +1,21 @@
+# 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.
+
diff --git a/api/boto/sdb/db/blob.py b/api/boto/sdb/db/blob.py
new file mode 100644
index 0000000..d92eb65
--- /dev/null
+++ b/api/boto/sdb/db/blob.py
@@ -0,0 +1,64 @@
+# 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.
+
+
+class Blob(object):
+ """Blob object"""
+ def __init__(self, value=None, file=None, id=None):
+ self._file = file
+ self.id = id
+ self.value = value
+
+ @property
+ def file(self):
+ from StringIO import StringIO
+ if self._file:
+ f = self._file
+ else:
+ f = StringIO(self.value)
+ return f
+
+ def __str__(self):
+ if hasattr(self.file, "get_contents_as_string"):
+ return str(self.file.get_contents_as_string())
+ else:
+ return str(self.file.getvalue())
+
+ def read(self):
+ return self.file.read()
+
+ def readline(self):
+ return self.file.readline()
+
+ def next(self):
+ return sefl.file.next()
+
+ def __iter__(self):
+ return iter(self.file)
+
+ @property
+ def size(self):
+ if self._file:
+ return self._file.size
+ elif self.value:
+ return len(self.value)
+ else:
+ return 0
diff --git a/api/boto/sdb/db/key.py b/api/boto/sdb/db/key.py
new file mode 100644
index 0000000..42a9d8d
--- /dev/null
+++ b/api/boto/sdb/db/key.py
@@ -0,0 +1,59 @@
+# 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.
+
+class Key(object):
+
+ @classmethod
+ def from_path(cls, *args, **kwds):
+ raise NotImplementedError, "Paths are not currently supported"
+
+ def __init__(self, encoded=None, obj=None):
+ self.name = None
+ if obj:
+ self.id = obj.id
+ self.kind = obj.kind()
+ else:
+ self.id = None
+ self.kind = None
+
+ def app(self):
+ raise NotImplementedError, "Applications are not currently supported"
+
+ def kind(self):
+ return self.kind
+
+ def id(self):
+ return self.id
+
+ def name(self):
+ raise NotImplementedError, "Key Names are not currently supported"
+
+ def id_or_name(self):
+ return self.id
+
+ def has_id_or_name(self):
+ return self.id != None
+
+ def parent(self):
+ raise NotImplementedError, "Key parents are not currently supported"
+
+ def __str__(self):
+ return self.id_or_name()
diff --git a/api/boto/sdb/db/manager/__init__.py b/api/boto/sdb/db/manager/__init__.py
new file mode 100644
index 0000000..1d75549
--- /dev/null
+++ b/api/boto/sdb/db/manager/__init__.py
@@ -0,0 +1,86 @@
+# 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 boto
+
+def get_manager(cls):
+ """
+ Returns the appropriate Manager class for a given Model class. It does this by
+ looking in the boto config for a section like this::
+
+ [DB]
+ db_type = SimpleDB
+ db_user =
+ db_passwd =
+ db_name = my_domain
+ [DB_TestBasic]
+ db_type = SimpleDB
+ db_user =
+ db_passwd =
+ db_name = basic_domain
+ db_port = 1111
+
+ The values in the DB section are "generic values" that will be used if nothing more
+ specific is found. You can also create a section for a specific Model class that
+ gives the db info for that class. In the example above, TestBasic is a Model subclass.
+ """
+ db_user = boto.config.get('DB', 'db_user', None)
+ db_passwd = boto.config.get('DB', 'db_passwd', None)
+ db_type = boto.config.get('DB', 'db_type', 'SimpleDB')
+ db_name = boto.config.get('DB', 'db_name', None)
+ db_table = boto.config.get('DB', 'db_table', None)
+ db_host = boto.config.get('DB', 'db_host', "sdb.amazonaws.com")
+ db_port = boto.config.getint('DB', 'db_port', 443)
+ enable_ssl = boto.config.getbool('DB', 'enable_ssl', True)
+ sql_dir = boto.config.get('DB', 'sql_dir', None)
+ debug = boto.config.getint('DB', 'debug', 0)
+ # first see if there is a fully qualified section name in the Boto config file
+ module_name = cls.__module__.replace('.', '_')
+ db_section = 'DB_' + module_name + '_' + cls.__name__
+ if not boto.config.has_section(db_section):
+ db_section = 'DB_' + cls.__name__
+ if boto.config.has_section(db_section):
+ db_user = boto.config.get(db_section, 'db_user', db_user)
+ db_passwd = boto.config.get(db_section, 'db_passwd', db_passwd)
+ db_type = boto.config.get(db_section, 'db_type', db_type)
+ db_name = boto.config.get(db_section, 'db_name', db_name)
+ db_table = boto.config.get(db_section, 'db_table', db_table)
+ db_host = boto.config.get(db_section, 'db_host', db_host)
+ db_port = boto.config.getint(db_section, 'db_port', db_port)
+ enable_ssl = boto.config.getint(db_section, 'enable_ssl', enable_ssl)
+ debug = boto.config.getint(db_section, 'debug', debug)
+ if db_type == 'SimpleDB':
+ from sdbmanager import SDBManager
+ return SDBManager(cls, db_name, db_user, db_passwd,
+ db_host, db_port, db_table, sql_dir, enable_ssl)
+ elif db_type == 'PostgreSQL':
+ from pgmanager import PGManager
+ if db_table:
+ return PGManager(cls, db_name, db_user, db_passwd,
+ db_host, db_port, db_table, sql_dir, enable_ssl)
+ else:
+ return None
+ elif db_type == 'XML':
+ from xmlmanager import XMLManager
+ return XMLManager(cls, db_name, db_user, db_passwd,
+ db_host, db_port, db_table, sql_dir, enable_ssl)
+ else:
+ raise ValueError, 'Unknown db_type: %s' % db_type
+
diff --git a/api/boto/sdb/db/manager/pgmanager.py b/api/boto/sdb/db/manager/pgmanager.py
new file mode 100644
index 0000000..4c7e3ad
--- /dev/null
+++ b/api/boto/sdb/db/manager/pgmanager.py
@@ -0,0 +1,387 @@
+# 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.
+from boto.sdb.db.key import Key
+from boto.sdb.db.model import Model
+import psycopg2
+import psycopg2.extensions
+import uuid, sys, os, string
+from boto.exception import *
+
+psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
+
+class PGConverter:
+
+ def __init__(self, manager):
+ self.manager = manager
+ self.type_map = {Key : (self.encode_reference, self.decode_reference),
+ Model : (self.encode_reference, self.decode_reference)}
+
+ def encode(self, type, value):
+ if type in self.type_map:
+ encode = self.type_map[type][0]
+ return encode(value)
+ return value
+
+ def decode(self, type, value):
+ if type in self.type_map:
+ decode = self.type_map[type][1]
+ return decode(value)
+ return value
+
+ def encode_prop(self, prop, value):
+ if isinstance(value, list):
+ if hasattr(prop, 'item_type'):
+ s = "{"
+ new_value = []
+ for v in value:
+ item_type = getattr(prop, 'item_type')
+ if Model in item_type.mro():
+ item_type = Model
+ new_value.append('%s' % self.encode(item_type, v))
+ s += ','.join(new_value)
+ s += "}"
+ return s
+ else:
+ return value
+ return self.encode(prop.data_type, value)
+
+ def decode_prop(self, prop, value):
+ if prop.data_type == list:
+ if value != None:
+ if not isinstance(value, list):
+ value = [value]
+ if hasattr(prop, 'item_type'):
+ item_type = getattr(prop, "item_type")
+ if Model in item_type.mro():
+ if item_type != self.manager.cls:
+ return item_type._manager.decode_value(prop, value)
+ else:
+ item_type = Model
+ return [self.decode(item_type, v) for v in value]
+ return value
+ elif hasattr(prop, 'reference_class'):
+ ref_class = getattr(prop, 'reference_class')
+ if ref_class != self.manager.cls:
+ return ref_class._manager.decode_value(prop, value)
+ else:
+ return self.decode(prop.data_type, value)
+ elif hasattr(prop, 'calculated_type'):
+ calc_type = getattr(prop, 'calculated_type')
+ return self.decode(calc_type, value)
+ else:
+ return self.decode(prop.data_type, value)
+
+ def encode_reference(self, value):
+ if isinstance(value, str) or isinstance(value, unicode):
+ return value
+ if value == None:
+ return ''
+ else:
+ return value.id
+
+ def decode_reference(self, value):
+ if not value:
+ return None
+ try:
+ return self.manager.get_object_from_id(value)
+ except:
+ raise ValueError, 'Unable to convert %s to Object' % value
+
+class PGManager(object):
+
+ def __init__(self, cls, db_name, db_user, db_passwd,
+ db_host, db_port, db_table, sql_dir, enable_ssl):
+ self.cls = cls
+ self.db_name = db_name
+ self.db_user = db_user
+ self.db_passwd = db_passwd
+ self.db_host = db_host
+ self.db_port = db_port
+ self.db_table = db_table
+ self.sql_dir = sql_dir
+ self.in_transaction = False
+ self.converter = PGConverter(self)
+ self._connect()
+
+ def _build_connect_string(self):
+ cs = 'dbname=%s user=%s password=%s host=%s port=%d'
+ return cs % (self.db_name, self.db_user, self.db_passwd,
+ self.db_host, self.db_port)
+
+ def _connect(self):
+ self.connection = psycopg2.connect(self._build_connect_string())
+ self.connection.set_client_encoding('UTF8')
+ self.cursor = self.connection.cursor()
+
+ def _object_lister(self, cursor):
+ try:
+ for row in cursor:
+ yield self._object_from_row(row, cursor.description)
+ except StopIteration:
+ cursor.close()
+ raise StopIteration
+
+ def _dict_from_row(self, row, description):
+ d = {}
+ for i in range(0, len(row)):
+ d[description[i][0]] = row[i]
+ return d
+
+ def _object_from_row(self, row, description=None):
+ if not description:
+ description = self.cursor.description
+ d = self._dict_from_row(row, description)
+ obj = self.cls(d['id'])
+ obj._manager = self
+ obj._auto_update = False
+ for prop in obj.properties(hidden=False):
+ if prop.data_type != Key:
+ v = self.decode_value(prop, d[prop.name])
+ v = prop.make_value_from_datastore(v)
+ if hasattr(prop, 'calculated_type'):
+ prop._set_direct(obj, v)
+ elif not prop.empty(v):
+ setattr(obj, prop.name, v)
+ else:
+ setattr(obj, prop.name, prop.default_value())
+ return obj
+
+ def _build_insert_qs(self, obj, calculated):
+ fields = []
+ values = []
+ templs = []
+ id_calculated = [p for p in calculated if p.name == 'id']
+ for prop in obj.properties(hidden=False):
+ if prop not in calculated:
+ value = prop.get_value_for_datastore(obj)
+ if value != prop.default_value() or prop.required:
+ value = self.encode_value(prop, value)
+ values.append(value)
+ fields.append('"%s"' % prop.name)
+ templs.append('%s')
+ qs = 'INSERT INTO "%s" (' % self.db_table
+ if len(id_calculated) == 0:
+ qs += '"id",'
+ qs += ','.join(fields)
+ qs += ") VALUES ("
+ if len(id_calculated) == 0:
+ qs += "'%s'," % obj.id
+ qs += ','.join(templs)
+ qs += ')'
+ if calculated:
+ qs += ' RETURNING '
+ calc_values = ['"%s"' % p.name for p in calculated]
+ qs += ','.join(calc_values)
+ qs += ';'
+ return qs, values
+
+ def _build_update_qs(self, obj, calculated):
+ fields = []
+ values = []
+ for prop in obj.properties(hidden=False):
+ if prop not in calculated:
+ value = prop.get_value_for_datastore(obj)
+ if value != prop.default_value() or prop.required:
+ value = self.encode_value(prop, value)
+ values.append(value)
+ field = '"%s"=' % prop.name
+ field += '%s'
+ fields.append(field)
+ qs = 'UPDATE "%s" SET ' % self.db_table
+ qs += ','.join(fields)
+ qs += """ WHERE "id" = '%s'""" % obj.id
+ if calculated:
+ qs += ' RETURNING '
+ calc_values = ['"%s"' % p.name for p in calculated]
+ qs += ','.join(calc_values)
+ qs += ';'
+ return qs, values
+
+ def _get_sql(self, mapping=None):
+ print '_get_sql'
+ sql = None
+ if self.sql_dir:
+ path = os.path.join(self.sql_dir, self.cls.__name__ + '.sql')
+ print path
+ if os.path.isfile(path):
+ fp = open(path)
+ sql = fp.read()
+ fp.close()
+ t = string.Template(sql)
+ sql = t.safe_substitute(mapping)
+ return sql
+
+ def start_transaction(self):
+ print 'start_transaction'
+ self.in_transaction = True
+
+ def end_transaction(self):
+ print 'end_transaction'
+ self.in_transaction = False
+ self.commit()
+
+ def commit(self):
+ if not self.in_transaction:
+ print '!!commit on %s' % self.db_table
+ try:
+ self.connection.commit()
+
+ except psycopg2.ProgrammingError, err:
+ self.connection.rollback()
+ raise err
+
+ def rollback(self):
+ print '!!rollback on %s' % self.db_table
+ self.connection.rollback()
+
+ def delete_table(self):
+ self.cursor.execute('DROP TABLE "%s";' % self.db_table)
+ self.commit()
+
+ def create_table(self, mapping=None):
+ self.cursor.execute(self._get_sql(mapping))
+ self.commit()
+
+ def encode_value(self, prop, value):
+ return self.converter.encode_prop(prop, value)
+
+ def decode_value(self, prop, value):
+ return self.converter.decode_prop(prop, value)
+
+ def execute_sql(self, query):
+ self.cursor.execute(query, None)
+ self.commit()
+
+ def query_sql(self, query, vars=None):
+ self.cursor.execute(query, vars)
+ return self.cursor.fetchall()
+
+ def lookup(self, cls, name, value):
+ values = []
+ qs = 'SELECT * FROM "%s" WHERE ' % self.db_table
+ found = False
+ for property in cls.properties(hidden=False):
+ if property.name == name:
+ found = True
+ value = self.encode_value(property, value)
+ values.append(value)
+ qs += "%s=" % name
+ qs += "%s"
+ if not found:
+ raise SDBPersistenceError('%s is not a valid field' % key)
+ qs += ';'
+ print qs
+ self.cursor.execute(qs, values)
+ if self.cursor.rowcount == 1:
+ row = self.cursor.fetchone()
+ return self._object_from_row(row, self.cursor.description)
+ elif self.cursor.rowcount == 0:
+ raise KeyError, 'Object not found'
+ else:
+ raise LookupError, 'Multiple Objects Found'
+
+ def query(self, cls, filters, limit=None, order_by=None):
+ parts = []
+ qs = 'SELECT * FROM "%s"' % self.db_table
+ if filters:
+ qs += ' WHERE '
+ properties = cls.properties(hidden=False)
+ for filter, value in filters:
+ name, op = filter.strip().split()
+ found = False
+ for property in properties:
+ if property.name == name:
+ found = True
+ value = self.encode_value(property, value)
+ parts.append(""""%s"%s'%s'""" % (name, op, value))
+ if not found:
+ raise SDBPersistenceError('%s is not a valid field' % key)
+ qs += ','.join(parts)
+ qs += ';'
+ print qs
+ cursor = self.connection.cursor()
+ cursor.execute(qs)
+ return self._object_lister(cursor)
+
+ def get_property(self, prop, obj, name):
+ qs = """SELECT "%s" FROM "%s" WHERE id='%s';""" % (name, self.db_table, obj.id)
+ print qs
+ self.cursor.execute(qs, None)
+ if self.cursor.rowcount == 1:
+ rs = self.cursor.fetchone()
+ for prop in obj.properties(hidden=False):
+ if prop.name == name:
+ v = self.decode_value(prop, rs[0])
+ return v
+ raise AttributeError, '%s not found' % name
+
+ def set_property(self, prop, obj, name, value):
+ pass
+ value = self.encode_value(prop, value)
+ qs = 'UPDATE "%s" SET ' % self.db_table
+ qs += "%s='%s'" % (name, self.encode_value(prop, value))
+ qs += " WHERE id='%s'" % obj.id
+ qs += ';'
+ print qs
+ self.cursor.execute(qs)
+ self.commit()
+
+ def get_object(self, cls, id):
+ qs = """SELECT * FROM "%s" WHERE id='%s';""" % (self.db_table, id)
+ self.cursor.execute(qs, None)
+ if self.cursor.rowcount == 1:
+ row = self.cursor.fetchone()
+ return self._object_from_row(row, self.cursor.description)
+ else:
+ raise SDBPersistenceError('%s object with id=%s does not exist' % (cls.__name__, id))
+
+ def get_object_from_id(self, id):
+ return self.get_object(self.cls, id)
+
+ def _find_calculated_props(self, obj):
+ return [p for p in obj.properties() if hasattr(p, 'calculated_type')]
+
+ def save_object(self, obj):
+ obj._auto_update = False
+ calculated = self._find_calculated_props(obj)
+ if not obj.id:
+ obj.id = str(uuid.uuid4())
+ qs, values = self._build_insert_qs(obj, calculated)
+ else:
+ qs, values = self._build_update_qs(obj, calculated)
+ print qs
+ self.cursor.execute(qs, values)
+ if calculated:
+ calc_values = self.cursor.fetchone()
+ print calculated
+ print calc_values
+ for i in range(0, len(calculated)):
+ prop = calculated[i]
+ prop._set_direct(obj, calc_values[i])
+ self.commit()
+
+ def delete_object(self, obj):
+ qs = """DELETE FROM "%s" WHERE id='%s';""" % (self.db_table, obj.id)
+ print qs
+ self.cursor.execute(qs)
+ self.commit()
+
+
diff --git a/api/boto/sdb/db/manager/sdbmanager.py b/api/boto/sdb/db/manager/sdbmanager.py
new file mode 100644
index 0000000..2bb2440
--- /dev/null
+++ b/api/boto/sdb/db/manager/sdbmanager.py
@@ -0,0 +1,518 @@
+# 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 boto
+import re
+from boto.utils import find_class
+import uuid
+from boto.sdb.db.key import Key
+from boto.sdb.db.model import Model
+from boto.sdb.db.blob import Blob
+from boto.sdb.db.property import ListProperty, MapProperty
+from datetime import datetime
+from boto.exception import SDBPersistenceError
+from tempfile import TemporaryFile
+
+ISO8601 = '%Y-%m-%dT%H:%M:%SZ'
+
+class SDBConverter:
+ """
+ Responsible for converting base Python types to format compatible with underlying
+ database. For SimpleDB, that means everything needs to be converted to a string
+ when stored in SimpleDB and from a string when retrieved.
+
+ To convert a value, pass it to the encode or decode method. The encode method
+ will take a Python native value and convert to DB format. The decode method will
+ take a DB format value and convert it to Python native format. To find the appropriate
+ method to call, the generic encode/decode methods will look for the type-specific
+ method by searching for a method called "encode_" or "decode_".
+ """
+ def __init__(self, manager):
+ self.manager = manager
+ self.type_map = { bool : (self.encode_bool, self.decode_bool),
+ int : (self.encode_int, self.decode_int),
+ long : (self.encode_long, self.decode_long),
+ float : (self.encode_float, self.decode_float),
+ Model : (self.encode_reference, self.decode_reference),
+ Key : (self.encode_reference, self.decode_reference),
+ datetime : (self.encode_datetime, self.decode_datetime),
+ Blob: (self.encode_blob, self.decode_blob),
+ }
+
+ def encode(self, item_type, value):
+ if item_type in self.type_map:
+ encode = self.type_map[item_type][0]
+ return encode(value)
+ return value
+
+ def decode(self, item_type, value):
+ if item_type in self.type_map:
+ decode = self.type_map[item_type][1]
+ return decode(value)
+ return value
+
+ def encode_list(self, prop, value):
+ if not isinstance(value, list):
+ value = [value]
+ new_value = []
+ for v in value:
+ item_type = getattr(prop, "item_type")
+ if Model in item_type.mro():
+ item_type = Model
+ new_value.append(self.encode(item_type, v))
+ return new_value
+
+ def encode_map(self, prop, value):
+ if not isinstance(value, dict):
+ raise ValueError, 'Expected a dict value, got %s' % type(value)
+ new_value = []
+ for key in value:
+ item_type = getattr(prop, "item_type")
+ if Model in item_type.mro():
+ item_type = Model
+ encoded_value = self.encode(item_type, value[key])
+ new_value.append('%s:%s' % (key, encoded_value))
+ return new_value
+
+ def encode_prop(self, prop, value):
+ if isinstance(prop, ListProperty):
+ return self.encode_list(prop, value)
+ elif isinstance(prop, MapProperty):
+ return self.encode_map(prop, value)
+ else:
+ return self.encode(prop.data_type, value)
+
+ def decode_list(self, prop, value):
+ if not isinstance(value, list):
+ value = [value]
+ if hasattr(prop, 'item_type'):
+ item_type = getattr(prop, "item_type")
+ if Model in item_type.mro():
+ return [item_type(id=v) for v in value]
+ return [self.decode(item_type, v) for v in value]
+ else:
+ return value
+
+ def decode_map(self, prop, value):
+ if not isinstance(value, list):
+ value = [value]
+ ret_value = {}
+ item_type = getattr(prop, "item_type")
+ for keyval in value:
+ key, val = keyval.split(':', 1)
+ if Model in item_type.mro():
+ val = item_type(id=val)
+ else:
+ val = self.decode(item_type, val)
+ ret_value[key] = val
+ return ret_value
+
+ def decode_prop(self, prop, value):
+ if isinstance(prop, ListProperty):
+ return self.decode_list(prop, value)
+ elif isinstance(prop, MapProperty):
+ return self.decode_map(prop, value)
+ else:
+ return self.decode(prop.data_type, value)
+
+ def encode_int(self, value):
+ value = int(value)
+ value += 2147483648
+ return '%010d' % value
+
+ def decode_int(self, value):
+ value = int(value)
+ value -= 2147483648
+ return int(value)
+
+ def encode_long(self, value):
+ value = long(value)
+ value += 9223372036854775808
+ return '%020d' % value
+
+ def decode_long(self, value):
+ value = long(value)
+ value -= 9223372036854775808
+ return value
+
+ def encode_bool(self, value):
+ if value == True:
+ return 'true'
+ else:
+ return 'false'
+
+ def decode_bool(self, value):
+ if value.lower() == 'true':
+ return True
+ else:
+ return False
+
+ def encode_float(self, value):
+ """
+ See http://tools.ietf.org/html/draft-wood-ldapext-float-00.
+ """
+ s = '%e' % value
+ l = s.split('e')
+ mantissa = l[0].ljust(18, '0')
+ exponent = l[1]
+ if value == 0.0:
+ case = '3'
+ exponent = '000'
+ elif mantissa[0] != '-' and exponent[0] == '+':
+ case = '5'
+ exponent = exponent[1:].rjust(3, '0')
+ elif mantissa[0] != '-' and exponent[0] == '-':
+ case = '4'
+ exponent = 999 + int(exponent)
+ exponent = '%03d' % exponent
+ elif mantissa[0] == '-' and exponent[0] == '-':
+ case = '2'
+ mantissa = '%f' % (10 + float(mantissa))
+ mantissa = mantissa.ljust(18, '0')
+ exponent = exponent[1:].rjust(3, '0')
+ else:
+ case = '1'
+ mantissa = '%f' % (10 + float(mantissa))
+ mantissa = mantissa.ljust(18, '0')
+ exponent = 999 - int(exponent)
+ exponent = '%03d' % exponent
+ return '%s %s %s' % (case, exponent, mantissa)
+
+ def decode_float(self, value):
+ case = value[0]
+ exponent = value[2:5]
+ mantissa = value[6:]
+ if case == '3':
+ return 0.0
+ elif case == '5':
+ pass
+ elif case == '4':
+ exponent = '%03d' % (int(exponent) - 999)
+ elif case == '2':
+ mantissa = '%f' % (float(mantissa) - 10)
+ exponent = '-' + exponent
+ else:
+ mantissa = '%f' % (float(mantissa) - 10)
+ exponent = '%03d' % abs((int(exponent) - 999))
+ return float(mantissa + 'e' + exponent)
+
+ def encode_datetime(self, value):
+ if isinstance(value, str) or isinstance(value, unicode):
+ return value
+ return value.strftime(ISO8601)
+
+ def decode_datetime(self, value):
+ try:
+ return datetime.strptime(value, ISO8601)
+ except:
+ return None
+
+ def encode_reference(self, value):
+ if isinstance(value, str) or isinstance(value, unicode):
+ return value
+ if value == None:
+ return ''
+ else:
+ return value.id
+
+ def decode_reference(self, value):
+ if not value:
+ return None
+ return value
+
+ def encode_blob(self, value):
+ if not value:
+ return None
+
+ if not value.id:
+ bucket = self.manager.get_blob_bucket()
+ key = bucket.new_key(str(uuid.uuid4()))
+ value.id = "s3://%s/%s" % (key.bucket.name, key.name)
+ else:
+ match = re.match("^s3:\/\/([^\/]*)\/(.*)$", value.id)
+ if match:
+ s3 = self.manager.get_s3_connection()
+ bucket = s3.get_bucket(match.group(1), validate=False)
+ key = bucket.get_key(match.group(2))
+ else:
+ raise SDBPersistenceError("Invalid Blob ID: %s" % value.id)
+
+ if value.value != None:
+ key.set_contents_from_string(value.value)
+ return value.id
+
+
+ def decode_blob(self, value):
+ if not value:
+ return None
+ match = re.match("^s3:\/\/([^\/]*)\/(.*)$", value)
+ if match:
+ s3 = self.manager.get_s3_connection()
+ bucket = s3.get_bucket(match.group(1), validate=False)
+ key = bucket.get_key(match.group(2))
+ else:
+ return None
+ if key:
+ return Blob(file=key, id="s3://%s/%s" % (key.bucket.name, key.name))
+ else:
+ return None
+
+class SDBManager(object):
+
+ def __init__(self, cls, db_name, db_user, db_passwd,
+ db_host, db_port, db_table, ddl_dir, enable_ssl):
+ self.cls = cls
+ self.db_name = db_name
+ self.db_user = db_user
+ self.db_passwd = db_passwd
+ self.db_host = db_host
+ self.db_port = db_port
+ self.db_table = db_table
+ self.ddl_dir = ddl_dir
+ self.enable_ssl = enable_ssl
+ self.s3 = None
+ self.bucket = None
+ self.converter = SDBConverter(self)
+ self._connect()
+
+ def _connect(self):
+ self.sdb = boto.connect_sdb(aws_access_key_id=self.db_user,
+ aws_secret_access_key=self.db_passwd,
+ is_secure=self.enable_ssl)
+ # This assumes that the domain has already been created
+ # It's much more efficient to do it this way rather than
+ # having this make a roundtrip each time to validate.
+ # The downside is that if the domain doesn't exist, it breaks
+ self.domain = self.sdb.lookup(self.db_name, validate=False)
+ if not self.domain:
+ self.domain = self.sdb.create_domain(self.db_name)
+
+ def _object_lister(self, cls, query_lister):
+ for item in query_lister:
+ obj = self.get_object(cls, item.name, item)
+ if obj:
+ yield obj
+
+ def encode_value(self, prop, value):
+ if value == None:
+ return None
+ if not prop:
+ return str(value)
+ return self.converter.encode_prop(prop, value)
+
+ def decode_value(self, prop, value):
+ return self.converter.decode_prop(prop, value)
+
+ def get_s3_connection(self):
+ if not self.s3:
+ self.s3 = boto.connect_s3(self.db_user, self.db_passwd)
+ return self.s3
+
+ def get_blob_bucket(self, bucket_name=None):
+ s3 = self.get_s3_connection()
+ bucket_name = "%s-%s" % (s3.aws_access_key_id, self.domain.name)
+ bucket_name = bucket_name.lower()
+ try:
+ self.bucket = s3.get_bucket(bucket_name)
+ except:
+ self.bucket = s3.create_bucket(bucket_name)
+ return self.bucket
+
+ def load_object(self, obj):
+ if not obj._loaded:
+ a = self.domain.get_attributes(obj.id)
+ if a.has_key('__type__'):
+ for prop in obj.properties(hidden=False):
+ if a.has_key(prop.name):
+ value = self.decode_value(prop, a[prop.name])
+ value = prop.make_value_from_datastore(value)
+ setattr(obj, prop.name, value)
+ obj._loaded = True
+
+ def get_object(self, cls, id, a=None):
+ obj = None
+ if not a:
+ a = self.domain.get_attributes(id)
+ if a.has_key('__type__'):
+ if not cls or a['__type__'] != cls.__name__:
+ cls = find_class(a['__module__'], a['__type__'])
+ if cls:
+ params = {}
+ for prop in cls.properties(hidden=False):
+ if a.has_key(prop.name):
+ value = self.decode_value(prop, a[prop.name])
+ value = prop.make_value_from_datastore(value)
+ params[prop.name] = value
+ obj = cls(id, **params)
+ obj._loaded = True
+ else:
+ s = '(%s) class %s.%s not found' % (id, a['__module__'], a['__type__'])
+ boto.log.info('sdbmanager: %s' % s)
+ return obj
+
+ def get_object_from_id(self, id):
+ return self.get_object(None, id)
+
+ def query(self, query):
+ query_str = "select * from `%s` %s" % (self.domain.name, self._build_filter_part(query.model_class, query.filters, query.sort_by))
+ if query.limit:
+ query_str += " limit %s" % query.limit
+ rs = self.domain.select(query_str, max_items=query.limit, next_token = query.next_token)
+ query.rs = rs
+ return self._object_lister(query.model_class, rs)
+
+ def count(self, cls, filters):
+ """
+ Get the number of results that would
+ be returned in this query
+ """
+ query = "select count(*) from `%s` %s" % (self.domain.name, self._build_filter_part(cls, filters))
+ count = int(self.domain.select(query).next()["Count"])
+ return count
+
+ def _build_filter_part(self, cls, filters, order_by=None):
+ """
+ Build the filter part
+ """
+ import types
+ query_parts = []
+ order_by_filtered = False
+ if order_by:
+ if order_by[0] == "-":
+ order_by_method = "desc";
+ order_by = order_by[1:]
+ else:
+ order_by_method = "asc";
+
+ for filter in filters:
+ (name, op) = filter[0].strip().split(" ", 1)
+ value = filter[1]
+ property = cls.find_property(name)
+ if name == order_by:
+ order_by_filtered = True
+ if types.TypeType(value) == types.ListType:
+ filter_parts = []
+ for val in value:
+ val = self.encode_value(property, val)
+ if isinstance(val, list):
+ for v in val:
+ filter_parts.append("`%s` %s '%s'" % (name, op, v.replace("'", "''")))
+ else:
+ filter_parts.append("`%s` %s '%s'" % (name, op, val.replace("'", "''")))
+ query_parts.append("(%s)" % (" or ".join(filter_parts)))
+ else:
+ if op == 'is' and value == None:
+ query_parts.append("`%s` is null" % name)
+ elif op == 'is not' and value == None:
+ query_parts.append("`%s` is not null" % name)
+ else:
+ val = self.encode_value(property, value)
+ if isinstance(val, list):
+ for v in val:
+ query_parts.append("`%s` %s '%s'" % (name, op, v.replace("'", "''")))
+ else:
+ query_parts.append("`%s` %s '%s'" % (name, op, val.replace("'", "''")))
+
+ type_query = "(`__type__` = '%s'" % cls.__name__
+ for subclass in cls.__sub_classes__:
+ type_query += " or `__type__` = '%s'" % subclass.__name__
+ type_query +=")"
+ query_parts.append(type_query)
+
+ order_by_query = ""
+ if order_by:
+ if not order_by_filtered:
+ query_parts.append("`%s` like '%%'" % order_by)
+ order_by_query = " order by `%s` %s" % (order_by, order_by_method)
+
+ if len(query_parts) > 0:
+ return "where %s %s" % (" and ".join(query_parts), order_by_query)
+ else:
+ return ""
+
+
+ def query_gql(self, query_string, *args, **kwds):
+ raise NotImplementedError, "GQL queries not supported in SimpleDB"
+
+ def save_object(self, obj):
+ if not obj.id:
+ obj.id = str(uuid.uuid4())
+
+ attrs = {'__type__' : obj.__class__.__name__,
+ '__module__' : obj.__class__.__module__,
+ '__lineage__' : obj.get_lineage()}
+ for property in obj.properties(hidden=False):
+ value = property.get_value_for_datastore(obj)
+ if value is not None:
+ value = self.encode_value(property, value)
+ attrs[property.name] = value
+ if property.unique:
+ try:
+ args = {property.name: value}
+ obj2 = obj.find(**args).next()
+ if obj2.id != obj.id:
+ raise SDBPersistenceError("Error: %s must be unique!" % property.name)
+ except(StopIteration):
+ pass
+ self.domain.put_attributes(obj.id, attrs, replace=True)
+
+ def delete_object(self, obj):
+ self.domain.delete_attributes(obj.id)
+
+ def set_property(self, prop, obj, name, value):
+ value = prop.get_value_for_datastore(obj)
+ value = self.encode_value(prop, value)
+ if prop.unique:
+ try:
+ args = {prop.name: value}
+ obj2 = obj.find(**args).next()
+ if obj2.id != obj.id:
+ raise SDBPersistenceError("Error: %s must be unique!" % prop.name)
+ except(StopIteration):
+ pass
+ self.domain.put_attributes(obj.id, {name : value}, replace=True)
+
+ def get_property(self, prop, obj, name):
+ a = self.domain.get_attributes(obj.id)
+
+ # try to get the attribute value from SDB
+ if name in a:
+ value = self.decode_value(prop, a[name])
+ value = prop.make_value_from_datastore(value)
+ setattr(obj, prop.name, value)
+ return value
+ raise AttributeError, '%s not found' % name
+
+ def set_key_value(self, obj, name, value):
+ self.domain.put_attributes(obj.id, {name : value}, replace=True)
+
+ def delete_key_value(self, obj, name):
+ self.domain.delete_attributes(obj.id, name)
+
+ def get_key_value(self, obj, name):
+ a = self.domain.get_attributes(obj.id, name)
+ if a.has_key(name):
+ return a[name]
+ else:
+ return None
+
+ def get_raw_item(self, obj):
+ return self.domain.get_item(obj.id)
+
diff --git a/api/boto/sdb/db/manager/xmlmanager.py b/api/boto/sdb/db/manager/xmlmanager.py
new file mode 100644
index 0000000..b12f5df
--- /dev/null
+++ b/api/boto/sdb/db/manager/xmlmanager.py
@@ -0,0 +1,519 @@
+# 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.
+import boto
+from boto.utils import find_class, Password
+import uuid
+from boto.sdb.db.key import Key
+from boto.sdb.db.model import Model
+from datetime import datetime
+from boto.exception import SDBPersistenceError
+from xml.dom.minidom import getDOMImplementation, parse, parseString, Node
+
+ISO8601 = '%Y-%m-%dT%H:%M:%SZ'
+
+class XMLConverter:
+ """
+ Responsible for converting base Python types to format compatible with underlying
+ database. For SimpleDB, that means everything needs to be converted to a string
+ when stored in SimpleDB and from a string when retrieved.
+
+ To convert a value, pass it to the encode or decode method. The encode method
+ will take a Python native value and convert to DB format. The decode method will
+ take a DB format value and convert it to Python native format. To find the appropriate
+ method to call, the generic encode/decode methods will look for the type-specific
+ method by searching for a method called "encode_" or "decode_".
+ """
+ def __init__(self, manager):
+ self.manager = manager
+ self.type_map = { bool : (self.encode_bool, self.decode_bool),
+ int : (self.encode_int, self.decode_int),
+ long : (self.encode_long, self.decode_long),
+ Model : (self.encode_reference, self.decode_reference),
+ Key : (self.encode_reference, self.decode_reference),
+ Password : (self.encode_password, self.decode_password),
+ datetime : (self.encode_datetime, self.decode_datetime)}
+
+ def get_text_value(self, parent_node):
+ value = ''
+ for node in parent_node.childNodes:
+ if node.nodeType == node.TEXT_NODE:
+ value += node.data
+ return value
+
+ def encode(self, item_type, value):
+ if item_type in self.type_map:
+ encode = self.type_map[item_type][0]
+ return encode(value)
+ return value
+
+ def decode(self, item_type, value):
+ if item_type in self.type_map:
+ decode = self.type_map[item_type][1]
+ return decode(value)
+ else:
+ value = self.get_text_value(value)
+ return value
+
+ def encode_prop(self, prop, value):
+ if isinstance(value, list):
+ if hasattr(prop, 'item_type'):
+ new_value = []
+ for v in value:
+ item_type = getattr(prop, "item_type")
+ if Model in item_type.mro():
+ item_type = Model
+ new_value.append(self.encode(item_type, v))
+ return new_value
+ else:
+ return value
+ else:
+ return self.encode(prop.data_type, value)
+
+ def decode_prop(self, prop, value):
+ if prop.data_type == list:
+ if hasattr(prop, 'item_type'):
+ item_type = getattr(prop, "item_type")
+ if Model in item_type.mro():
+ item_type = Model
+ values = []
+ for item_node in value.getElementsByTagName('item'):
+ value = self.decode(item_type, item_node)
+ values.append(value)
+ return values
+ else:
+ return self.get_text_value(value)
+ else:
+ return self.decode(prop.data_type, value)
+
+ def encode_int(self, value):
+ value = int(value)
+ return '%d' % value
+
+ def decode_int(self, value):
+ value = self.get_text_value(value)
+ if value:
+ value = int(value)
+ else:
+ value = None
+ return value
+
+ def encode_long(self, value):
+ value = long(value)
+ return '%d' % value
+
+ def decode_long(self, value):
+ value = self.get_text_value(value)
+ return long(value)
+
+ def encode_bool(self, value):
+ if value == True:
+ return 'true'
+ else:
+ return 'false'
+
+ def decode_bool(self, value):
+ value = self.get_text_value(value)
+ if value.lower() == 'true':
+ return True
+ else:
+ return False
+
+ def encode_datetime(self, value):
+ return value.strftime(ISO8601)
+
+ def decode_datetime(self, value):
+ value = self.get_text_value(value)
+ try:
+ return datetime.strptime(value, ISO8601)
+ except:
+ return None
+
+ def encode_reference(self, value):
+ if isinstance(value, str) or isinstance(value, unicode):
+ return value
+ if value == None:
+ return ''
+ else:
+ val_node = self.manager.doc.createElement("object")
+ val_node.setAttribute('id', value.id)
+ val_node.setAttribute('class', '%s.%s' % (value.__class__.__module__, value.__class__.__name__))
+ return val_node
+
+ def decode_reference(self, value):
+ if not value:
+ return None
+ try:
+ value = value.childNodes[0]
+ class_name = value.getAttribute("class")
+ id = value.getAttribute("id")
+ cls = find_class(class_name)
+ return cls.get_by_ids(id)
+ except:
+ return None
+
+ def encode_password(self, value):
+ if value and len(value) > 0:
+ return str(value)
+ else:
+ return None
+
+ def decode_password(self, value):
+ value = self.get_text_value(value)
+ return Password(value)
+
+
+class XMLManager(object):
+
+ def __init__(self, cls, db_name, db_user, db_passwd,
+ db_host, db_port, db_table, ddl_dir, enable_ssl):
+ self.cls = cls
+ if not db_name:
+ db_name = cls.__name__.lower()
+ self.db_name = db_name
+ self.db_user = db_user
+ self.db_passwd = db_passwd
+ self.db_host = db_host
+ self.db_port = db_port
+ self.db_table = db_table
+ self.ddl_dir = ddl_dir
+ self.s3 = None
+ self.converter = XMLConverter(self)
+ self.impl = getDOMImplementation()
+ self.doc = self.impl.createDocument(None, 'objects', None)
+
+ self.connection = None
+ self.enable_ssl = enable_ssl
+ self.auth_header = None
+ if self.db_user:
+ import base64
+ base64string = base64.encodestring('%s:%s' % (self.db_user, self.db_passwd))[:-1]
+ authheader = "Basic %s" % base64string
+ self.auth_header = authheader
+
+ def _connect(self):
+ if self.db_host:
+ if self.enable_ssl:
+ from httplib import HTTPSConnection as Connection
+ else:
+ from httplib import HTTPConnection as Connection
+
+ self.connection = Connection(self.db_host, self.db_port)
+
+ def _make_request(self, method, url, post_data=None, body=None):
+ """
+ Make a request on this connection
+ """
+ if not self.connection:
+ self._connect()
+ try:
+ self.connection.close()
+ except:
+ pass
+ self.connection.connect()
+ headers = {}
+ if self.auth_header:
+ headers["Authorization"] = self.auth_header
+ self.connection.request(method, url, body, headers)
+ resp = self.connection.getresponse()
+ return resp
+
+ def new_doc(self):
+ return self.impl.createDocument(None, 'objects', None)
+
+ def _object_lister(self, cls, doc):
+ for obj_node in doc.getElementsByTagName('object'):
+ if not cls:
+ class_name = obj_node.getAttribute('class')
+ cls = find_class(class_name)
+ id = obj_node.getAttribute('id')
+ obj = cls(id)
+ for prop_node in obj_node.getElementsByTagName('property'):
+ prop_name = prop_node.getAttribute('name')
+ prop = obj.find_property(prop_name)
+ if prop:
+ if hasattr(prop, 'item_type'):
+ value = self.get_list(prop_node, prop.item_type)
+ else:
+ value = self.decode_value(prop, prop_node)
+ value = prop.make_value_from_datastore(value)
+ setattr(obj, prop.name, value)
+ yield obj
+
+ def reset(self):
+ self._connect()
+
+ def get_doc(self):
+ return self.doc
+
+ def encode_value(self, prop, value):
+ return self.converter.encode_prop(prop, value)
+
+ def decode_value(self, prop, value):
+ return self.converter.decode_prop(prop, value)
+
+ def get_s3_connection(self):
+ if not self.s3:
+ self.s3 = boto.connect_s3(self.aws_access_key_id, self.aws_secret_access_key)
+ return self.s3
+
+ def get_list(self, prop_node, item_type):
+ values = []
+ try:
+ items_node = prop_node.getElementsByTagName('items')[0]
+ except:
+ return []
+ for item_node in items_node.getElementsByTagName('item'):
+ value = self.converter.decode(item_type, item_node)
+ values.append(value)
+ return values
+
+ def get_object_from_doc(self, cls, id, doc):
+ obj_node = doc.getElementsByTagName('object')[0]
+ if not cls:
+ class_name = obj_node.getAttribute('class')
+ cls = find_class(class_name)
+ if not id:
+ id = obj_node.getAttribute('id')
+ obj = cls(id)
+ for prop_node in obj_node.getElementsByTagName('property'):
+ prop_name = prop_node.getAttribute('name')
+ prop = obj.find_property(prop_name)
+ value = self.decode_value(prop, prop_node)
+ value = prop.make_value_from_datastore(value)
+ if value != None:
+ try:
+ setattr(obj, prop.name, value)
+ except:
+ pass
+ return obj
+
+ def get_props_from_doc(self, cls, id, doc):
+ """
+ Pull out the properties from this document
+ Returns the class, the properties in a hash, and the id if provided as a tuple
+ :return: (cls, props, id)
+ """
+ obj_node = doc.getElementsByTagName('object')[0]
+ if not cls:
+ class_name = obj_node.getAttribute('class')
+ cls = find_class(class_name)
+ if not id:
+ id = obj_node.getAttribute('id')
+ props = {}
+ for prop_node in obj_node.getElementsByTagName('property'):
+ prop_name = prop_node.getAttribute('name')
+ prop = cls.find_property(prop_name)
+ value = self.decode_value(prop, prop_node)
+ value = prop.make_value_from_datastore(value)
+ if value != None:
+ props[prop.name] = value
+ return (cls, props, id)
+
+
+ def get_object(self, cls, id):
+ if not self.connection:
+ self._connect()
+
+ if not self.connection:
+ raise NotImplementedError("Can't query without a database connection")
+ url = "/%s/%s" % (self.db_name, id)
+ resp = self._make_request('GET', url)
+ if resp.status == 200:
+ doc = parse(resp)
+ else:
+ raise Exception("Error: %s" % resp.status)
+ return self.get_object_from_doc(cls, id, doc)
+
+ def query(self, cls, filters, limit=None, order_by=None):
+ if not self.connection:
+ self._connect()
+
+ if not self.connection:
+ raise NotImplementedError("Can't query without a database connection")
+
+ from urllib import urlencode
+
+ query = str(self._build_query(cls, filters, limit, order_by))
+ if query:
+ url = "/%s?%s" % (self.db_name, urlencode({"query": query}))
+ else:
+ url = "/%s" % self.db_name
+ resp = self._make_request('GET', url)
+ if resp.status == 200:
+ doc = parse(resp)
+ else:
+ raise Exception("Error: %s" % resp.status)
+ return self._object_lister(cls, doc)
+
+ def _build_query(self, cls, filters, limit, order_by):
+ import types
+ if len(filters) > 4:
+ raise Exception('Too many filters, max is 4')
+ parts = []
+ properties = cls.properties(hidden=False)
+ for filter, value in filters:
+ name, op = filter.strip().split()
+ found = False
+ for property in properties:
+ if property.name == name:
+ found = True
+ if types.TypeType(value) == types.ListType:
+ filter_parts = []
+ for val in value:
+ val = self.encode_value(property, val)
+ filter_parts.append("'%s' %s '%s'" % (name, op, val))
+ parts.append("[%s]" % " OR ".join(filter_parts))
+ else:
+ value = self.encode_value(property, value)
+ parts.append("['%s' %s '%s']" % (name, op, value))
+ if not found:
+ raise Exception('%s is not a valid field' % name)
+ if order_by:
+ if order_by.startswith("-"):
+ key = order_by[1:]
+ type = "desc"
+ else:
+ key = order_by
+ type = "asc"
+ parts.append("['%s' starts-with ''] sort '%s' %s" % (key, key, type))
+ return ' intersection '.join(parts)
+
+ def query_gql(self, query_string, *args, **kwds):
+ raise NotImplementedError, "GQL queries not supported in XML"
+
+ def save_list(self, doc, items, prop_node):
+ items_node = doc.createElement('items')
+ prop_node.appendChild(items_node)
+ for item in items:
+ item_node = doc.createElement('item')
+ items_node.appendChild(item_node)
+ if isinstance(item, Node):
+ item_node.appendChild(item)
+ else:
+ text_node = doc.createTextNode(item)
+ item_node.appendChild(text_node)
+
+ def save_object(self, obj):
+ """
+ Marshal the object and do a PUT
+ """
+ doc = self.marshal_object(obj)
+ if obj.id:
+ url = "/%s/%s" % (self.db_name, obj.id)
+ else:
+ url = "/%s" % (self.db_name)
+ resp = self._make_request("PUT", url, body=doc.toxml())
+ new_obj = self.get_object_from_doc(obj.__class__, None, parse(resp))
+ obj.id = new_obj.id
+ for prop in obj.properties():
+ try:
+ propname = prop.name
+ except AttributeError:
+ propname = None
+ if propname:
+ value = getattr(new_obj, prop.name)
+ if value:
+ setattr(obj, prop.name, value)
+ return obj
+
+
+ def marshal_object(self, obj, doc=None):
+ if not doc:
+ doc = self.new_doc()
+ if not doc:
+ doc = self.doc
+ obj_node = doc.createElement('object')
+
+ if obj.id:
+ obj_node.setAttribute('id', obj.id)
+
+ obj_node.setAttribute('class', '%s.%s' % (obj.__class__.__module__,
+ obj.__class__.__name__))
+ root = doc.documentElement
+ root.appendChild(obj_node)
+ for property in obj.properties(hidden=False):
+ prop_node = doc.createElement('property')
+ prop_node.setAttribute('name', property.name)
+ prop_node.setAttribute('type', property.type_name)
+ value = property.get_value_for_datastore(obj)
+ if value is not None:
+ value = self.encode_value(property, value)
+ if isinstance(value, list):
+ self.save_list(doc, value, prop_node)
+ elif isinstance(value, Node):
+ prop_node.appendChild(value)
+ else:
+ text_node = doc.createTextNode(str(value))
+ prop_node.appendChild(text_node)
+ obj_node.appendChild(prop_node)
+
+ return doc
+
+ def unmarshal_object(self, fp, cls=None, id=None):
+ if isinstance(fp, str) or isinstance(fp, unicode):
+ doc = parseString(fp)
+ else:
+ doc = parse(fp)
+ return self.get_object_from_doc(cls, id, doc)
+
+ def unmarshal_props(self, fp, cls=None, id=None):
+ """
+ Same as unmarshalling an object, except it returns
+ from "get_props_from_doc"
+ """
+ if isinstance(fp, str) or isinstance(fp, unicode):
+ doc = parseString(fp)
+ else:
+ doc = parse(fp)
+ return self.get_props_from_doc(cls, id, doc)
+
+ def delete_object(self, obj):
+ url = "/%s/%s" % (self.db_name, obj.id)
+ return self._make_request("DELETE", url)
+
+ def set_key_value(self, obj, name, value):
+ self.domain.put_attributes(obj.id, {name : value}, replace=True)
+
+ def delete_key_value(self, obj, name):
+ self.domain.delete_attributes(obj.id, name)
+
+ def get_key_value(self, obj, name):
+ a = self.domain.get_attributes(obj.id, name)
+ if a.has_key(name):
+ return a[name]
+ else:
+ return None
+
+ def get_raw_item(self, obj):
+ return self.domain.get_item(obj.id)
+
+ def set_property(self, prop, obj, name, value):
+ pass
+
+ def get_property(self, prop, obj, name):
+ pass
+
+ def load_object(self, obj):
+ if not obj._loaded:
+ obj = obj.get_by_id(obj.id)
+ obj._loaded = True
+ return obj
+
diff --git a/api/boto/sdb/db/model.py b/api/boto/sdb/db/model.py
new file mode 100644
index 0000000..dc142e8
--- /dev/null
+++ b/api/boto/sdb/db/model.py
@@ -0,0 +1,232 @@
+# 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.
+
+from boto.sdb.db.manager import get_manager
+from boto.sdb.db.property import *
+from boto.sdb.db.key import Key
+from boto.sdb.db.query import Query
+import boto
+
+class ModelMeta(type):
+ "Metaclass for all Models"
+
+ def __init__(cls, name, bases, dict):
+ super(ModelMeta, cls).__init__(name, bases, dict)
+ # Make sure this is a subclass of Model - mainly copied from django ModelBase (thanks!)
+ cls.__sub_classes__ = []
+ try:
+ if filter(lambda b: issubclass(b, Model), bases):
+ for base in bases:
+ base.__sub_classes__.append(cls)
+ cls._manager = get_manager(cls)
+ # look for all of the Properties and set their names
+ for key in dict.keys():
+ if isinstance(dict[key], Property):
+ property = dict[key]
+ property.__property_config__(cls, key)
+ prop_names = []
+ props = cls.properties()
+ for prop in props:
+ if not prop.__class__.__name__.startswith('_'):
+ prop_names.append(prop.name)
+ setattr(cls, '_prop_names', prop_names)
+ except NameError:
+ # 'Model' isn't defined yet, meaning we're looking at our own
+ # Model class, defined below.
+ pass
+
+class Model(object):
+ __metaclass__ = ModelMeta
+
+ @classmethod
+ def get_lineage(cls):
+ l = [c.__name__ for c in cls.mro()]
+ l.reverse()
+ return '.'.join(l)
+
+ @classmethod
+ def kind(cls):
+ return cls.__name__
+
+ @classmethod
+ def _get_by_id(cls, id, manager=None):
+ if not manager:
+ manager = cls._manager
+ return manager.get_object(cls, id)
+
+ @classmethod
+ def get_by_id(cls, ids=None, parent=None):
+ if isinstance(ids, list):
+ objs = [cls._get_by_id(id) for id in ids]
+ return objs
+ else:
+ return cls._get_by_id(ids)
+
+ get_by_ids = get_by_id
+
+ @classmethod
+ def get_by_key_name(cls, key_names, parent=None):
+ raise NotImplementedError, "Key Names are not currently supported"
+
+ @classmethod
+ def find(cls, limit=None, next_token=None, **params):
+ q = Query(cls, limit=limit, next_token=next_token)
+ for key, value in params.items():
+ q.filter('%s =' % key, value)
+ return q
+
+ @classmethod
+ def lookup(cls, name, value):
+ return cls._manager.lookup(cls, name, value)
+
+ @classmethod
+ def all(cls, limit=None, next_token=None):
+ return cls.find(limit=limit, next_token=next_token)
+
+ @classmethod
+ def get_or_insert(key_name, **kw):
+ raise NotImplementedError, "get_or_insert not currently supported"
+
+ @classmethod
+ def properties(cls, hidden=True):
+ properties = []
+ while cls:
+ for key in cls.__dict__.keys():
+ prop = cls.__dict__[key]
+ if isinstance(prop, Property):
+ if hidden or not prop.__class__.__name__.startswith('_'):
+ properties.append(prop)
+ if len(cls.__bases__) > 0:
+ cls = cls.__bases__[0]
+ else:
+ cls = None
+ return properties
+
+ @classmethod
+ def find_property(cls, prop_name):
+ property = None
+ while cls:
+ for key in cls.__dict__.keys():
+ prop = cls.__dict__[key]
+ if isinstance(prop, Property):
+ if not prop.__class__.__name__.startswith('_') and prop_name == prop.name:
+ property = prop
+ if len(cls.__bases__) > 0:
+ cls = cls.__bases__[0]
+ else:
+ cls = None
+ return property
+
+ @classmethod
+ def get_xmlmanager(cls):
+ if not hasattr(cls, '_xmlmanager'):
+ from boto.sdb.db.manager.xmlmanager import XMLManager
+ cls._xmlmanager = XMLManager(cls, None, None, None,
+ None, None, None, None, False)
+ return cls._xmlmanager
+
+ @classmethod
+ def from_xml(cls, fp):
+ xmlmanager = cls.get_xmlmanager()
+ return xmlmanager.unmarshal_object(fp)
+
+ def __init__(self, id=None, **kw):
+ self._loaded = False
+ # first initialize all properties to their default values
+ for prop in self.properties(hidden=False):
+ setattr(self, prop.name, prop.default_value())
+ if kw.has_key('manager'):
+ self._manager = kw['manager']
+ self.id = id
+ for key in kw:
+ if key != 'manager':
+ # We don't want any errors populating up when loading an object,
+ # so if it fails we just revert to it's default value
+ try:
+ setattr(self, key, kw[key])
+ except Exception, e:
+ boto.log.exception(e)
+
+ def __repr__(self):
+ return '%s<%s>' % (self.__class__.__name__, self.id)
+
+ def __str__(self):
+ return str(self.id)
+
+ def __eq__(self, other):
+ return other and isinstance(other, Model) and self.id == other.id
+
+ def _get_raw_item(self):
+ return self._manager.get_raw_item(self)
+
+ def load(self):
+ if self.id and not self._loaded:
+ self._manager.load_object(self)
+
+ def put(self):
+ self._manager.save_object(self)
+
+ save = put
+
+ def delete(self):
+ self._manager.delete_object(self)
+
+ def key(self):
+ return Key(obj=self)
+
+ def set_manager(self, manager):
+ self._manager = manager
+
+ def to_dict(self):
+ props = {}
+ for prop in self.properties(hidden=False):
+ props[prop.name] = getattr(self, prop.name)
+ obj = {'properties' : props,
+ 'id' : self.id}
+ return {self.__class__.__name__ : obj}
+
+ def to_xml(self, doc=None):
+ xmlmanager = self.get_xmlmanager()
+ doc = xmlmanager.marshal_object(self, doc)
+ return doc
+
+class Expando(Model):
+
+ def __setattr__(self, name, value):
+ if name in self._prop_names:
+ object.__setattr__(self, name, value)
+ elif name.startswith('_'):
+ object.__setattr__(self, name, value)
+ elif name == 'id':
+ object.__setattr__(self, name, value)
+ else:
+ self._manager.set_key_value(self, name, value)
+ object.__setattr__(self, name, value)
+
+ def __getattr__(self, name):
+ if not name.startswith('_'):
+ value = self._manager.get_key_value(self, name)
+ if value:
+ object.__setattr__(self, name, value)
+ return value
+ raise AttributeError
+
+
diff --git a/api/boto/sdb/db/property.py b/api/boto/sdb/db/property.py
new file mode 100644
index 0000000..61d424a
--- /dev/null
+++ b/api/boto/sdb/db/property.py
@@ -0,0 +1,556 @@
+# 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 datetime
+from key import Key
+from boto.utils import Password
+from boto.sdb.db.query import Query
+from tempfile import TemporaryFile
+
+import re
+import boto
+import boto.s3.key
+from boto.sdb.db.blob import Blob
+
+class Property(object):
+
+ data_type = str
+ type_name = ''
+ name = ''
+ verbose_name = ''
+
+ def __init__(self, verbose_name=None, name=None, default=None, required=False,
+ validator=None, choices=None, unique=False):
+ self.verbose_name = verbose_name
+ self.name = name
+ self.default = default
+ self.required = required
+ self.validator = validator
+ self.choices = choices
+ self.slot_name = '_'
+ self.unique = unique
+
+ def __get__(self, obj, objtype):
+ if obj:
+ obj.load()
+ return getattr(obj, self.slot_name)
+ else:
+ return None
+
+ def __set__(self, obj, value):
+ self.validate(value)
+
+ # Fire off any on_set functions
+ try:
+ if obj._loaded and hasattr(obj, "on_set_%s" % self.name):
+ fnc = getattr(obj, "on_set_%s" % self.name)
+ value = fnc(value)
+ except Exception, e:
+ boto.log.exception("Exception running on_set_%s" % self.name)
+
+ setattr(obj, self.slot_name, value)
+
+ def __property_config__(self, model_class, property_name):
+ self.model_class = model_class
+ self.name = property_name
+ self.slot_name = '_' + self.name
+
+ def default_validator(self, value):
+ if value == self.default_value():
+ return
+ if not isinstance(value, self.data_type):
+ raise TypeError, 'Validation Error, expecting %s, got %s' % (self.data_type, type(value))
+
+ def default_value(self):
+ return self.default
+
+ def validate(self, value):
+ if self.required and value==None:
+ raise ValueError, '%s is a required property' % self.name
+ if self.choices and value and not value in self.choices:
+ raise ValueError, '%s not a valid choice for %s.%s' % (value, self.model_class.__name__, self.name)
+ if self.validator:
+ self.validator(value)
+ else:
+ self.default_validator(value)
+ return value
+
+ def empty(self, value):
+ return not value
+
+ def get_value_for_datastore(self, model_instance):
+ return getattr(model_instance, self.name)
+
+ def make_value_from_datastore(self, value):
+ return value
+
+ def get_choices(self):
+ if callable(self.choices):
+ return self.choices()
+ return self.choices
+
+def validate_string(value):
+ if isinstance(value, str) or isinstance(value, unicode):
+ if len(value) > 1024:
+ raise ValueError, 'Length of value greater than maxlength'
+ else:
+ raise TypeError, 'Expecting String, got %s' % type(value)
+
+class StringProperty(Property):
+
+ type_name = 'String'
+
+ def __init__(self, verbose_name=None, name=None, default='', required=False,
+ validator=validate_string, choices=None, unique=False):
+ Property.__init__(self, verbose_name, name, default, required, validator, choices, unique)
+
+class TextProperty(Property):
+
+ type_name = 'Text'
+
+ def __init__(self, verbose_name=None, name=None, default='', required=False,
+ validator=None, choices=None, unique=False, max_length=None):
+ Property.__init__(self, verbose_name, name, default, required, validator, choices, unique)
+ self.max_length = max_length
+
+ def validate(self, value):
+ if not isinstance(value, str) and not isinstance(value, unicode):
+ raise TypeError, 'Expecting Text, got %s' % type(value)
+ if self.max_length and len(value) > self.max_length:
+ raise ValueError, 'Length of value greater than maxlength %s' % self.max_length
+
+class PasswordProperty(StringProperty):
+ """
+ Hashed property who's original value can not be
+ retrieved, but still can be compaired.
+ """
+ data_type = Password
+ type_name = 'Password'
+
+ def __init__(self, verbose_name=None, name=None, default='', required=False,
+ validator=None, choices=None, unique=False):
+ StringProperty.__init__(self, verbose_name, name, default, required, validator, choices, unique)
+
+ def make_value_from_datastore(self, value):
+ p = Password(value)
+ return p
+
+ def get_value_for_datastore(self, model_instance):
+ value = StringProperty.get_value_for_datastore(self, model_instance)
+ if value and len(value):
+ return str(value)
+ else:
+ return None
+
+ def __set__(self, obj, value):
+ if not isinstance(value, Password):
+ p = Password()
+ p.set(value)
+ value = p
+ Property.__set__(self, obj, value)
+
+ def __get__(self, obj, objtype):
+ return Password(StringProperty.__get__(self, obj, objtype))
+
+ def validate(self, value):
+ value = Property.validate(self, value)
+ if isinstance(value, Password):
+ if len(value) > 1024:
+ raise ValueError, 'Length of value greater than maxlength'
+ else:
+ raise TypeError, 'Expecting Password, got %s' % type(value)
+
+class BlobProperty(Property):
+ data_type = Blob
+ type_name = "blob"
+
+ def __set__(self, obj, value):
+ if value != self.default_value():
+ if not isinstance(value, Blob):
+ oldb = self.__get__(obj, type(obj))
+ id = None
+ if oldb:
+ id = oldb.id
+ b = Blob(value=value, id=id)
+ value = b
+ Property.__set__(self, obj, value)
+
+class S3KeyProperty(Property):
+
+ data_type = boto.s3.key.Key
+ type_name = 'S3Key'
+ validate_regex = "^s3:\/\/([^\/]*)\/(.*)$"
+
+ def __init__(self, verbose_name=None, name=None, default=None,
+ required=False, validator=None, choices=None, unique=False):
+ Property.__init__(self, verbose_name, name, default, required,
+ validator, choices, unique)
+
+ def validate(self, value):
+ if value == self.default_value() or value == str(self.default_value()):
+ return self.default_value()
+ if isinstance(value, self.data_type):
+ return
+ match = re.match(self.validate_regex, value)
+ if match:
+ return
+ raise TypeError, 'Validation Error, expecting %s, got %s' % (self.data_type, type(value))
+
+ def __get__(self, obj, objtype):
+ value = Property.__get__(self, obj, objtype)
+ if value:
+ if isinstance(value, self.data_type):
+ return value
+ match = re.match(self.validate_regex, value)
+ if match:
+ s3 = obj._manager.get_s3_connection()
+ bucket = s3.get_bucket(match.group(1), validate=False)
+ k = bucket.get_key(match.group(2))
+ if not k:
+ k = bucket.new_key(match.group(2))
+ k.set_contents_from_string("")
+ return k
+ else:
+ return value
+
+ def get_value_for_datastore(self, model_instance):
+ value = Property.get_value_for_datastore(self, model_instance)
+ if value:
+ return "s3://%s/%s" % (value.bucket.name, value.name)
+ else:
+ return None
+
+class IntegerProperty(Property):
+
+ data_type = int
+ type_name = 'Integer'
+
+ def __init__(self, verbose_name=None, name=None, default=0, required=False,
+ validator=None, choices=None, unique=False, max=2147483647, min=-2147483648):
+ Property.__init__(self, verbose_name, name, default, required, validator, choices, unique)
+ self.max = max
+ self.min = min
+
+ def validate(self, value):
+ value = int(value)
+ value = Property.validate(self, value)
+ if value > self.max:
+ raise ValueError, 'Maximum value is %d' % self.max
+ if value < self.min:
+ raise ValueError, 'Minimum value is %d' % self.min
+ return value
+
+ def empty(self, value):
+ return value is None
+
+class LongProperty(Property):
+
+ data_type = long
+ type_name = 'Long'
+
+ def __init__(self, verbose_name=None, name=None, default=0, required=False,
+ validator=None, choices=None, unique=False):
+ Property.__init__(self, verbose_name, name, default, required, validator, choices, unique)
+
+ def validate(self, value):
+ value = long(value)
+ value = Property.validate(self, value)
+ min = -9223372036854775808
+ max = 9223372036854775807
+ if value > max:
+ raise ValueError, 'Maximum value is %d' % max
+ if value < min:
+ raise ValueError, 'Minimum value is %d' % min
+ return value
+
+ def empty(self, value):
+ return value is None
+
+class BooleanProperty(Property):
+
+ data_type = bool
+ type_name = 'Boolean'
+
+ def __init__(self, verbose_name=None, name=None, default=False, required=False,
+ validator=None, choices=None, unique=False):
+ Property.__init__(self, verbose_name, name, default, required, validator, choices, unique)
+
+ def empty(self, value):
+ return value is None
+
+class FloatProperty(Property):
+
+ data_type = float
+ type_name = 'Float'
+
+ def __init__(self, verbose_name=None, name=None, default=0.0, required=False,
+ validator=None, choices=None, unique=False):
+ Property.__init__(self, verbose_name, name, default, required, validator, choices, unique)
+
+ def validate(self, value):
+ value = float(value)
+ value = Property.validate(self, value)
+ return value
+
+ def empty(self, value):
+ return value is None
+
+class DateTimeProperty(Property):
+
+ data_type = datetime.datetime
+ type_name = 'DateTime'
+
+ def __init__(self, verbose_name=None, auto_now=False, auto_now_add=False, name=None,
+ default=None, required=False, validator=None, choices=None, unique=False):
+ Property.__init__(self, verbose_name, name, default, required, validator, choices, unique)
+ self.auto_now = auto_now
+ self.auto_now_add = auto_now_add
+
+ def default_value(self):
+ if self.auto_now or self.auto_now_add:
+ return self.now()
+ return Property.default_value(self)
+
+ def validate(self, value):
+ if value == None:
+ return
+ if not isinstance(value, self.data_type):
+ raise TypeError, 'Validation Error, expecting %s, got %s' % (self.data_type, type(value))
+
+ def get_value_for_datastore(self, model_instance):
+ if self.auto_now:
+ setattr(model_instance, self.name, self.now())
+ return Property.get_value_for_datastore(self, model_instance)
+
+ def now(self):
+ return datetime.datetime.utcnow()
+
+class ReferenceProperty(Property):
+
+ data_type = Key
+ type_name = 'Reference'
+
+ def __init__(self, reference_class=None, collection_name=None,
+ verbose_name=None, name=None, default=None, required=False, validator=None, choices=None, unique=False):
+ Property.__init__(self, verbose_name, name, default, required, validator, choices, unique)
+ self.reference_class = reference_class
+ self.collection_name = collection_name
+
+ def __get__(self, obj, objtype):
+ if obj:
+ value = getattr(obj, self.slot_name)
+ if value == self.default_value():
+ return value
+ # If the value is still the UUID for the referenced object, we need to create
+ # the object now that is the attribute has actually been accessed. This lazy
+ # instantiation saves unnecessary roundtrips to SimpleDB
+ if isinstance(value, str) or isinstance(value, unicode):
+ # This is some minor handling to allow us to use the base "Model" class
+ # as our reference class. If we do so, we're going to assume we're using
+ # our own class's manager to fetch objects
+ if hasattr(self.reference_class, "_manager"):
+ manager = self.reference_class._manager
+ else:
+ manager = obj._manager
+ value = manager.get_object(self.reference_class, value)
+ setattr(obj, self.name, value)
+ return value
+
+ def __property_config__(self, model_class, property_name):
+ Property.__property_config__(self, model_class, property_name)
+ if self.collection_name is None:
+ self.collection_name = '%s_%s_set' % (model_class.__name__.lower(), self.name)
+ if hasattr(self.reference_class, self.collection_name):
+ raise ValueError, 'duplicate property: %s' % self.collection_name
+ setattr(self.reference_class, self.collection_name,
+ _ReverseReferenceProperty(model_class, property_name, self.collection_name))
+
+ def check_uuid(self, value):
+ # This does a bit of hand waving to "type check" the string
+ t = value.split('-')
+ if len(t) != 5:
+ raise ValueError
+
+ def check_instance(self, value):
+ try:
+ obj_lineage = value.get_lineage()
+ cls_lineage = self.reference_class.get_lineage()
+ if obj_lineage.startswith(cls_lineage):
+ return
+ raise TypeError, '%s not instance of %s' % (obj_lineage, cls_lineage)
+ except:
+ raise ValueError, '%s is not a Model' % value
+
+ def validate(self, value):
+ if self.required and value==None:
+ raise ValueError, '%s is a required property' % self.name
+ if value == self.default_value():
+ return
+ if not isinstance(value, str) and not isinstance(value, unicode):
+ self.check_instance(value)
+
+class _ReverseReferenceProperty(Property):
+ data_type = Query
+ type_name = 'query'
+
+ def __init__(self, model, prop, name):
+ self.__model = model
+ self.__property = prop
+ self.name = name
+ self.item_type = model
+
+ def __get__(self, model_instance, model_class):
+ """Fetches collection of model instances of this collection property."""
+ if model_instance is not None:
+ query = Query(self.__model)
+ return query.filter(self.__property + ' =', model_instance)
+ else:
+ return self
+
+ def __set__(self, model_instance, value):
+ """Not possible to set a new collection."""
+ raise ValueError, 'Virtual property is read-only'
+
+
+class CalculatedProperty(Property):
+
+ def __init__(self, verbose_name=None, name=None, default=None,
+ required=False, validator=None, choices=None,
+ calculated_type=int, unique=False, use_method=False):
+ Property.__init__(self, verbose_name, name, default, required,
+ validator, choices, unique)
+ self.calculated_type = calculated_type
+ self.use_method = use_method
+
+ def __get__(self, obj, objtype):
+ value = self.default_value()
+ if obj:
+ try:
+ value = getattr(obj, self.slot_name)
+ if self.use_method:
+ value = value()
+ except AttributeError:
+ pass
+ return value
+
+ def __set__(self, obj, value):
+ """Not possible to set a new AutoID."""
+ pass
+
+ def _set_direct(self, obj, value):
+ if not self.use_method:
+ setattr(obj, self.slot_name, value)
+
+ def get_value_for_datastore(self, model_instance):
+ if self.calculated_type in [str, int, bool]:
+ value = self.__get__(model_instance, model_instance.__class__)
+ return value
+ else:
+ return None
+
+class ListProperty(Property):
+
+ data_type = list
+ type_name = 'List'
+
+ def __init__(self, item_type, verbose_name=None, name=None, default=None, **kwds):
+ if default is None:
+ default = []
+ self.item_type = item_type
+ Property.__init__(self, verbose_name, name, default=default, required=True, **kwds)
+
+ def validate(self, value):
+ if value is not None:
+ if not isinstance(value, list):
+ value = [value]
+
+ if self.item_type in (int, long):
+ item_type = (int, long)
+ elif self.item_type in (str, unicode):
+ item_type = (str, unicode)
+ else:
+ item_type = self.item_type
+
+ for item in value:
+ if not isinstance(item, item_type):
+ if item_type == (int, long):
+ raise ValueError, 'Items in the %s list must all be integers.' % self.name
+ else:
+ raise ValueError('Items in the %s list must all be %s instances' %
+ (self.name, self.item_type.__name__))
+ return value
+
+ def empty(self, value):
+ return value is None
+
+ def default_value(self):
+ return list(super(ListProperty, self).default_value())
+
+ def __set__(self, obj, value):
+ """Override the set method to allow them to set the property to an instance of the item_type instead of requiring a list to be passed in"""
+ if self.item_type in (int, long):
+ item_type = (int, long)
+ elif self.item_type in (str, unicode):
+ item_type = (str, unicode)
+ else:
+ item_type = self.item_type
+ if isinstance(value, item_type):
+ value = [value]
+ return super(ListProperty, self).__set__(obj,value)
+
+
+class MapProperty(Property):
+
+ data_type = dict
+ type_name = 'Map'
+
+ def __init__(self, item_type=str, verbose_name=None, name=None, default=None, **kwds):
+ if default is None:
+ default = {}
+ self.item_type = item_type
+ Property.__init__(self, verbose_name, name, default=default, required=True, **kwds)
+
+ def validate(self, value):
+ if value is not None:
+ if not isinstance(value, dict):
+ raise ValueError, 'Value must of type dict'
+
+ if self.item_type in (int, long):
+ item_type = (int, long)
+ elif self.item_type in (str, unicode):
+ item_type = (str, unicode)
+ else:
+ item_type = self.item_type
+
+ for key in value:
+ if not isinstance(value[key], item_type):
+ if item_type == (int, long):
+ raise ValueError, 'Values in the %s Map must all be integers.' % self.name
+ else:
+ raise ValueError('Values in the %s Map must all be %s instances' %
+ (self.name, self.item_type.__name__))
+ return value
+
+ def empty(self, value):
+ return value is None
+
+ def default_value(self):
+ return {}
diff --git a/api/boto/sdb/db/query.py b/api/boto/sdb/db/query.py
new file mode 100644
index 0000000..034d9d3
--- /dev/null
+++ b/api/boto/sdb/db/query.py
@@ -0,0 +1,76 @@
+# 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.
+
+class Query(object):
+ __local_iter__ = None
+ def __init__(self, model_class, limit=None, next_token=None, manager=None):
+ self.model_class = model_class
+ self.limit = limit
+ if manager:
+ self.manager = manager
+ else:
+ self.manager = self.model_class._manager
+ self.filters = []
+ self.sort_by = None
+ self.rs = None
+ self.next_token = next_token
+
+ def __iter__(self):
+ return iter(self.manager.query(self))
+
+ def next(self):
+ if self.__local_iter__ == None:
+ self.__local_iter__ = self.__iter__()
+ return self.__local_iter__.next()
+
+ def filter(self, property_operator, value):
+ self.filters.append((property_operator, value))
+ return self
+
+ def fetch(self, limit, offset=0):
+ raise NotImplementedError, "fetch mode is not currently supported"
+
+ def count(self):
+ return self.manager.count(self.model_class, self.filters)
+
+ def order(self, key):
+ self.sort_by = key
+ return self
+
+ def to_xml(self, doc=None):
+ if not doc:
+ xmlmanager = self.model_class.get_xmlmanager()
+ doc = xmlmanager.new_doc()
+ for obj in self:
+ obj.to_xml(doc)
+ return doc
+
+ def get_next_token(self):
+ if self.rs:
+ return self.rs.next_token
+ if self._next_token:
+ return self._next_token
+ return None
+
+ def set_next_token(self, token):
+ self._next_token = token
+
+ next_token = property(get_next_token, set_next_token)
diff --git a/api/boto/sdb/db/test_db.py b/api/boto/sdb/db/test_db.py
new file mode 100644
index 0000000..b790b9e
--- /dev/null
+++ b/api/boto/sdb/db/test_db.py
@@ -0,0 +1,224 @@
+from boto.sdb.db.model import Model
+from boto.sdb.db.property import *
+from boto.sdb.db.manager import get_manager
+from datetime import datetime
+import time
+from boto.exception import SDBPersistenceError
+
+_objects = {}
+
+#
+# This will eventually be moved to the boto.tests module and become a real unit test
+# but for now it will live here. It shows examples of each of the Property types in
+# use and tests the basic operations.
+#
+class TestBasic(Model):
+
+ name = StringProperty()
+ size = IntegerProperty()
+ foo = BooleanProperty()
+ date = DateTimeProperty()
+
+class TestFloat(Model):
+
+ name = StringProperty()
+ value = FloatProperty()
+
+class TestRequired(Model):
+
+ req = StringProperty(required=True, default='foo')
+
+class TestReference(Model):
+
+ ref = ReferenceProperty(reference_class=TestBasic, collection_name='refs')
+
+class TestSubClass(TestBasic):
+
+ answer = IntegerProperty()
+
+class TestPassword(Model):
+ password = PasswordProperty()
+
+class TestList(Model):
+
+ name = StringProperty()
+ nums = ListProperty(int)
+
+class TestMap(Model):
+
+ name = StringProperty()
+ map = MapProperty()
+
+class TestListReference(Model):
+
+ name = StringProperty()
+ basics = ListProperty(TestBasic)
+
+class TestAutoNow(Model):
+
+ create_date = DateTimeProperty(auto_now_add=True)
+ modified_date = DateTimeProperty(auto_now=True)
+
+class TestUnique(Model):
+ name = StringProperty(unique=True)
+
+def test_basic():
+ global _objects
+ t = TestBasic()
+ t.name = 'simple'
+ t.size = -42
+ t.foo = True
+ t.date = datetime.now()
+ print 'saving object'
+ t.put()
+ _objects['test_basic_t'] = t
+ time.sleep(5)
+ print 'now try retrieving it'
+ tt = TestBasic.get_by_id(t.id)
+ _objects['test_basic_tt'] = tt
+ assert tt.id == t.id
+ l = TestBasic.get_by_id([t.id])
+ assert len(l) == 1
+ assert l[0].id == t.id
+ assert t.size == tt.size
+ assert t.foo == tt.foo
+ assert t.name == tt.name
+ #assert t.date == tt.date
+ return t
+
+def test_float():
+ global _objects
+ t = TestFloat()
+ t.name = 'float object'
+ t.value = 98.6
+ print 'saving object'
+ t.save()
+ _objects['test_float_t'] = t
+ time.sleep(5)
+ print 'now try retrieving it'
+ tt = TestFloat.get_by_id(t.id)
+ _objects['test_float_tt'] = tt
+ assert tt.id == t.id
+ assert tt.name == t.name
+ assert tt.value == t.value
+ return t
+
+def test_required():
+ global _objects
+ t = TestRequired()
+ _objects['test_required_t'] = t
+ t.put()
+ return t
+
+def test_reference(t=None):
+ global _objects
+ if not t:
+ t = test_basic()
+ tt = TestReference()
+ tt.ref = t
+ tt.put()
+ time.sleep(10)
+ tt = TestReference.get_by_id(tt.id)
+ _objects['test_reference_tt'] = tt
+ assert tt.ref.id == t.id
+ for o in t.refs:
+ print o
+
+def test_subclass():
+ global _objects
+ t = TestSubClass()
+ _objects['test_subclass_t'] = t
+ t.name = 'a subclass'
+ t.size = -489
+ t.save()
+
+def test_password():
+ global _objects
+ t = TestPassword()
+ _objects['test_password_t'] = t
+ t.password = "foo"
+ t.save()
+ time.sleep(5)
+ # Make sure it stored ok
+ tt = TestPassword.get_by_id(t.id)
+ _objects['test_password_tt'] = tt
+ #Testing password equality
+ assert tt.password == "foo"
+ #Testing password not stored as string
+ assert str(tt.password) != "foo"
+
+def test_list():
+ global _objects
+ t = TestList()
+ _objects['test_list_t'] = t
+ t.name = 'a list of ints'
+ t.nums = [1,2,3,4,5]
+ t.put()
+ tt = TestList.get_by_id(t.id)
+ _objects['test_list_tt'] = tt
+ assert tt.name == t.name
+ for n in tt.nums:
+ assert isinstance(n, int)
+
+def test_list_reference():
+ global _objects
+ t = TestBasic()
+ t.put()
+ _objects['test_list_ref_t'] = t
+ tt = TestListReference()
+ tt.name = "foo"
+ tt.basics = [t]
+ tt.put()
+ time.sleep(5)
+ _objects['test_list_ref_tt'] = tt
+ ttt = TestListReference.get_by_id(tt.id)
+ assert ttt.basics[0].id == t.id
+
+def test_unique():
+ global _objects
+ t = TestUnique()
+ name = 'foo' + str(int(time.time()))
+ t.name = name
+ t.put()
+ _objects['test_unique_t'] = t
+ time.sleep(10)
+ tt = TestUnique()
+ _objects['test_unique_tt'] = tt
+ tt.name = name
+ try:
+ tt.put()
+ assert False
+ except(SDBPersistenceError):
+ pass
+
+def test_datetime():
+ global _objects
+ t = TestAutoNow()
+ t.put()
+ _objects['test_datetime_t'] = t
+ time.sleep(5)
+ tt = TestAutoNow.get_by_id(t.id)
+ assert tt.create_date.timetuple() == t.create_date.timetuple()
+
+def test():
+ print 'test_basic'
+ t1 = test_basic()
+ print 'test_required'
+ test_required()
+ print 'test_reference'
+ test_reference(t1)
+ print 'test_subclass'
+ test_subclass()
+ print 'test_password'
+ test_password()
+ print 'test_list'
+ test_list()
+ print 'test_list_reference'
+ test_list_reference()
+ print "test_datetime"
+ test_datetime()
+ print 'test_unique'
+ test_unique()
+
+if __name__ == "__main__":
+ test()
diff --git a/api/boto/sdb/domain.py b/api/boto/sdb/domain.py
new file mode 100644
index 0000000..3c0def6
--- /dev/null
+++ b/api/boto/sdb/domain.py
@@ -0,0 +1,330 @@
+# 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 SDB Domain
+"""
+from boto.sdb.queryresultset import QueryResultSet, SelectResultSet
+
+class Domain:
+
+ def __init__(self, connection=None, name=None):
+ self.connection = connection
+ self.name = name
+ self._metadata = None
+
+ def __repr__(self):
+ return 'Domain:%s' % self.name
+
+ def __iter__(self):
+ return iter(self.select("SELECT * FROM `%s`" % self.name))
+
+ def startElement(self, name, attrs, connection):
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'DomainName':
+ self.name = value
+ else:
+ setattr(self, name, value)
+
+ def get_metadata(self):
+ if not self._metadata:
+ self._metadata = self.connection.domain_metadata(self)
+ return self._metadata
+
+ def put_attributes(self, item_name, attributes, replace=True):
+ """
+ Store attributes for a given item.
+
+ :type item_name: string
+ :param item_name: The name of the item whose attributes are being stored.
+
+ :type attribute_names: dict or dict-like object
+ :param attribute_names: The name/value pairs to store as attributes
+
+ :type replace: bool
+ :param replace: Whether the attribute values passed in will replace
+ existing values or will be added as addition values.
+ Defaults to True.
+
+ :rtype: bool
+ :return: True if successful
+ """
+ return self.connection.put_attributes(self, item_name, attributes, replace)
+
+ def batch_put_attributes(self, items, replace=True):
+ """
+ Store attributes for multiple items.
+
+ :type items: dict or dict-like object
+ :param items: A dictionary-like object. The keys of the dictionary are
+ the item names and the values are themselves dictionaries
+ of attribute names/values, exactly the same as the
+ attribute_names parameter of the scalar put_attributes
+ call.
+
+ :type replace: bool
+ :param replace: Whether the attribute values passed in will replace
+ existing values or will be added as addition values.
+ Defaults to True.
+
+ :rtype: bool
+ :return: True if successful
+ """
+ return self.connection.batch_put_attributes(self, items, replace)
+
+ def get_attributes(self, item_name, attribute_name=None, item=None):
+ """
+ Retrieve attributes for a given item.
+
+ :type item_name: string
+ :param item_name: The name of the item whose attributes are being retrieved.
+
+ :type attribute_names: string or list of strings
+ :param attribute_names: An attribute name or list of attribute names. This
+ parameter is optional. If not supplied, all attributes
+ will be retrieved for the item.
+
+ :rtype: :class:`boto.sdb.item.Item`
+ :return: An Item mapping type containing the requested attribute name/values
+ """
+ return self.connection.get_attributes(self, item_name, attribute_name, item)
+
+ def delete_attributes(self, item_name, attributes=None):
+ """
+ Delete attributes from a given item.
+
+ :type item_name: string
+ :param item_name: The name of the item whose attributes are being deleted.
+
+ :type attributes: dict, list or :class:`boto.sdb.item.Item`
+ :param attributes: Either a list containing attribute names which will cause
+ all values associated with that attribute name to be deleted or
+ a dict or Item containing the attribute names and keys and list
+ of values to delete as the value. If no value is supplied,
+ all attribute name/values for the item will be deleted.
+
+ :rtype: bool
+ :return: True if successful
+ """
+ return self.connection.delete_attributes(self, item_name, attributes)
+
+ def query(self, query='', max_items=None, attr_names=None):
+ """
+ Returns a list of items within domain that match the query.
+
+ :type query: string
+ :param query: The SimpleDB query to be performed.
+
+ :type max_items: int
+ :param max_items: The maximum number of items to return. If not
+ supplied, the default is None which returns all
+ items matching the query.
+
+ :type attr_names: list
+ :param attr_names: Either None, meaning return all attributes
+ or a list of attribute names which means to return
+ only those attributes.
+
+ :rtype: iter
+ :return: An iterator containing the results. This is actually a generator
+ function that will iterate across all search results, not just the
+ first page.
+ """
+ return iter(QueryResultSet(self, query, max_items, attr_names))
+
+ def select(self, query='', next_token=None, max_items=None):
+ """
+ Returns a set of Attributes for item names within domain_name that match the query.
+ The query must be expressed in using the SELECT style syntax rather than the
+ original SimpleDB query language.
+
+ :type query: string
+ :param query: The SimpleDB query to be performed.
+
+ :type max_items: int
+ :param max_items: The maximum number of items to return.
+
+ :rtype: iter
+ :return: An iterator containing the results. This is actually a generator
+ function that will iterate across all search results, not just the
+ first page.
+ """
+ return SelectResultSet(self, query, max_items=max_items,
+ next_token=next_token)
+
+ def get_item(self, item_name):
+ item = self.get_attributes(item_name)
+ if item:
+ item.domain = self
+ return item
+ else:
+ return None
+
+ def new_item(self, item_name):
+ return self.connection.item_cls(self, item_name)
+
+ def delete_item(self, item):
+ self.delete_attributes(item.name)
+
+ def to_xml(self, f=None):
+ """Get this domain as an XML DOM Document
+ :param f: Optional File to dump directly to
+ :type f: File or Stream
+
+ :return: File object where the XML has been dumped to
+ :rtype: file
+ """
+ if not f:
+ from tempfile import TemporaryFile
+ f = TemporaryFile()
+ print >>f, ''
+ print >>f, '' % self.name
+ for item in self:
+ print >>f, '\t' % item.name
+ for k in item:
+ print >>f, '\t\t' % k
+ values = item[k]
+ if not isinstance(values, list):
+ values = [values]
+ for value in values:
+ print >>f, '\t\t\t>f, ']]>'
+ print >>f, '\t\t'
+ print >>f, '\t'
+ print >>f, ''
+ f.flush()
+ f.seek(0)
+ return f
+
+
+ def from_xml(self, doc):
+ """Load this domain based on an XML document"""
+ import xml.sax
+ handler = DomainDumpParser(self)
+ xml.sax.parse(doc, handler)
+ return handler
+
+
+class DomainMetaData:
+
+ def __init__(self, domain=None):
+ self.domain = domain
+ self.item_count = None
+ self.item_names_size = None
+ self.attr_name_count = None
+ self.attr_names_size = None
+ self.attr_value_count = None
+ self.attr_values_size = None
+
+ def startElement(self, name, attrs, connection):
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'ItemCount':
+ self.item_count = int(value)
+ elif name == 'ItemNamesSizeBytes':
+ self.item_names_size = int(value)
+ elif name == 'AttributeNameCount':
+ self.attr_name_count = int(value)
+ elif name == 'AttributeNamesSizeBytes':
+ self.attr_names_size = int(value)
+ elif name == 'AttributeValueCount':
+ self.attr_value_count = int(value)
+ elif name == 'AttributeValuesSizeBytes':
+ self.attr_values_size = int(value)
+ elif name == 'Timestamp':
+ self.timestamp = value
+ else:
+ setattr(self, name, value)
+
+import sys
+from xml.sax.handler import ContentHandler
+class DomainDumpParser(ContentHandler):
+ """
+ SAX parser for a domain that has been dumped
+ """
+
+ def __init__(self, domain):
+ self.uploader = UploaderThread(domain.name)
+ self.item_id = None
+ self.attrs = {}
+ self.attribute = None
+ self.value = ""
+ self.domain = domain
+
+ def startElement(self, name, attrs):
+ if name == "Item":
+ self.item_id = attrs['id']
+ self.attrs = {}
+ elif name == "attribute":
+ self.attribute = attrs['id']
+ elif name == "value":
+ self.value = ""
+
+ def characters(self, ch):
+ self.value += ch
+
+ def endElement(self, name):
+ if name == "value":
+ if self.value and self.attribute:
+ value = self.value.strip()
+ attr_name = self.attribute.strip()
+ if self.attrs.has_key(attr_name):
+ self.attrs[attr_name].append(value)
+ else:
+ self.attrs[attr_name] = [value]
+ elif name == "Item":
+ self.uploader.items[self.item_id] = self.attrs
+ # Every 40 items we spawn off the uploader
+ if len(self.uploader.items) >= 40:
+ self.uploader.start()
+ self.uploader = UploaderThread(self.domain.name)
+ elif name == "Domain":
+ # If we're done, spawn off our last Uploader Thread
+ self.uploader.start()
+
+from threading import Thread
+class UploaderThread(Thread):
+ """Uploader Thread"""
+
+ def __init__(self, domain_name):
+ import boto
+ self.sdb = boto.connect_sdb()
+ self.db = self.sdb.get_domain(domain_name)
+ self.items = {}
+ Thread.__init__(self)
+
+ def run(self):
+ try:
+ self.db.batch_put_attributes(self.items)
+ except:
+ print "Exception using batch put, trying regular put instead"
+ for item_name in self.items:
+ self.db.put_attributes(item_name, self.items[item_name])
+ print ".",
+ sys.stdout.flush()
diff --git a/api/boto/sdb/item.py b/api/boto/sdb/item.py
new file mode 100644
index 0000000..b81e715
--- /dev/null
+++ b/api/boto/sdb/item.py
@@ -0,0 +1,97 @@
+# 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 SDB Item
+"""
+
+import base64
+
+class Item(dict):
+
+ def __init__(self, domain, name='', active=False):
+ dict.__init__(self)
+ self.domain = domain
+ self.name = name
+ self.active = active
+ self.request_id = None
+ self.encoding = None
+ self.in_attribute = False
+ self.converter = self.domain.connection.converter
+
+ def startElement(self, name, attrs, connection):
+ if name == 'Attribute':
+ self.in_attribute = True
+ self.encoding = attrs.get('encoding', None)
+ return None
+
+ def decode_value(self, value):
+ if self.encoding == 'base64':
+ self.encoding = None
+ return base64.decodestring(value)
+ else:
+ return value
+
+ def endElement(self, name, value, connection):
+ if name == 'ItemName':
+ self.name = self.decode_value(value)
+ elif name == 'Name':
+ if self.in_attribute:
+ self.last_key = self.decode_value(value)
+ else:
+ self.name = self.decode_value(value)
+ elif name == 'Value':
+ if self.has_key(self.last_key):
+ if not isinstance(self[self.last_key], list):
+ self[self.last_key] = [self[self.last_key]]
+ value = self.decode_value(value)
+ if self.converter:
+ value = self.converter.decode(value)
+ self[self.last_key].append(value)
+ else:
+ value = self.decode_value(value)
+ if self.converter:
+ value = self.converter.decode(value)
+ self[self.last_key] = value
+ elif name == 'BoxUsage':
+ try:
+ connection.box_usage += float(value)
+ except:
+ pass
+ elif name == 'RequestId':
+ self.request_id = value
+ elif name == 'Attribute':
+ self.in_attribute = False
+ else:
+ setattr(self, name, value)
+
+ def load(self):
+ self.domain.get_attributes(self.name, item=self)
+
+ def save(self, replace=True):
+ self.domain.put_attributes(self.name, self, replace)
+
+ def delete(self):
+ self.domain.delete_item(self)
+
+
+
+
diff --git a/api/boto/sdb/persist/__init__.py b/api/boto/sdb/persist/__init__.py
new file mode 100644
index 0000000..2f2b0c1
--- /dev/null
+++ b/api/boto/sdb/persist/__init__.py
@@ -0,0 +1,83 @@
+# 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 boto
+from boto.utils import find_class
+
+class Manager(object):
+
+ DefaultDomainName = boto.config.get('Persist', 'default_domain', None)
+
+ def __init__(self, domain_name=None, aws_access_key_id=None, aws_secret_access_key=None, debug=0):
+ self.domain_name = domain_name
+ self.aws_access_key_id = aws_access_key_id
+ self.aws_secret_access_key = aws_secret_access_key
+ self.domain = None
+ self.sdb = None
+ self.s3 = None
+ if not self.domain_name:
+ self.domain_name = self.DefaultDomainName
+ if self.domain_name:
+ boto.log.info('No SimpleDB domain set, using default_domain: %s' % self.domain_name)
+ else:
+ boto.log.warning('No SimpleDB domain set, persistance is disabled')
+ if self.domain_name:
+ self.sdb = boto.connect_sdb(aws_access_key_id=self.aws_access_key_id,
+ aws_secret_access_key=self.aws_secret_access_key,
+ debug=debug)
+ self.domain = self.sdb.lookup(self.domain_name)
+ if not self.domain:
+ self.domain = self.sdb.create_domain(self.domain_name)
+
+ def get_s3_connection(self):
+ if not self.s3:
+ self.s3 = boto.connect_s3(self.aws_access_key_id, self.aws_secret_access_key)
+ return self.s3
+
+def get_manager(domain_name=None, aws_access_key_id=None, aws_secret_access_key=None, debug=0):
+ return Manager(domain_name, aws_access_key_id, aws_secret_access_key, debug=debug)
+
+def set_domain(domain_name):
+ Manager.DefaultDomainName = domain_name
+
+def get_domain():
+ return Manager.DefaultDomainName
+
+def revive_object_from_id(id, manager):
+ if not manager.domain:
+ return None
+ attrs = manager.domain.get_attributes(id, ['__module__', '__type__', '__lineage__'])
+ try:
+ cls = find_class(attrs['__module__'], attrs['__type__'])
+ return cls(id, manager=manager)
+ except ImportError:
+ return None
+
+def object_lister(cls, query_lister, manager):
+ for item in query_lister:
+ if cls:
+ yield cls(item.name)
+ else:
+ o = revive_object_from_id(item.name, manager)
+ if o:
+ yield o
+
+
diff --git a/api/boto/sdb/persist/checker.py b/api/boto/sdb/persist/checker.py
new file mode 100644
index 0000000..147ea47
--- /dev/null
+++ b/api/boto/sdb/persist/checker.py
@@ -0,0 +1,303 @@
+# 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.
+
+from datetime import datetime
+import boto
+from boto.s3.key import Key
+from boto.s3.bucket import Bucket
+from boto.sdb.persist import revive_object_from_id
+from boto.exception import SDBPersistenceError
+from boto.utils import Password
+
+ISO8601 = '%Y-%m-%dT%H:%M:%SZ'
+
+class ValueChecker:
+
+ def check(self, value):
+ """
+ Checks a value to see if it is of the right type.
+
+ Should raise a TypeError exception if an in appropriate value is passed in.
+ """
+ raise TypeError
+
+ def from_string(self, str_value, obj):
+ """
+ Takes a string as input and returns the type-specific value represented by that string.
+
+ Should raise a ValueError if the value cannot be converted to the appropriate type.
+ """
+ raise ValueError
+
+ def to_string(self, value):
+ """
+ Convert a value to it's string representation.
+
+ Should raise a ValueError if the value cannot be converted to a string representation.
+ """
+ raise ValueError
+
+class StringChecker(ValueChecker):
+
+ def __init__(self, **params):
+ if params.has_key('maxlength'):
+ self.maxlength = params['maxlength']
+ else:
+ self.maxlength = 1024
+ if params.has_key('default'):
+ self.check(params['default'])
+ self.default = params['default']
+ else:
+ self.default = ''
+
+ def check(self, value):
+ if isinstance(value, str) or isinstance(value, unicode):
+ if len(value) > self.maxlength:
+ raise ValueError, 'Length of value greater than maxlength'
+ else:
+ raise TypeError, 'Expecting String, got %s' % type(value)
+
+ def from_string(self, str_value, obj):
+ return str_value
+
+ def to_string(self, value):
+ self.check(value)
+ return value
+
+class PasswordChecker(StringChecker):
+ def check(self, value):
+ if isinstance(value, str) or isinstance(value, unicode) or isinstance(value, Password):
+ if len(value) > self.maxlength:
+ raise ValueError, 'Length of value greater than maxlength'
+ else:
+ raise TypeError, 'Expecting String, got %s' % type(value)
+
+class IntegerChecker(ValueChecker):
+
+ __sizes__ = { 'small' : (65535, 32767, -32768, 5),
+ 'medium' : (4294967295, 2147483647, -2147483648, 10),
+ 'large' : (18446744073709551615, 9223372036854775807, -9223372036854775808, 20)}
+
+ def __init__(self, **params):
+ self.size = params.get('size', 'medium')
+ if self.size not in self.__sizes__.keys():
+ raise ValueError, 'size must be one of %s' % self.__sizes__.keys()
+ self.signed = params.get('signed', True)
+ self.default = params.get('default', 0)
+ self.format_string = '%%0%dd' % self.__sizes__[self.size][-1]
+
+ def check(self, value):
+ if not isinstance(value, int) and not isinstance(value, long):
+ raise TypeError, 'Expecting int or long, got %s' % type(value)
+ if self.signed:
+ min = self.__sizes__[self.size][2]
+ max = self.__sizes__[self.size][1]
+ else:
+ min = 0
+ max = self.__sizes__[self.size][0]
+ if value > max:
+ raise ValueError, 'Maximum value is %d' % max
+ if value < min:
+ raise ValueError, 'Minimum value is %d' % min
+
+ def from_string(self, str_value, obj):
+ val = int(str_value)
+ if self.signed:
+ val = val + self.__sizes__[self.size][2]
+ return val
+
+ def to_string(self, value):
+ self.check(value)
+ if self.signed:
+ value += -self.__sizes__[self.size][2]
+ return self.format_string % value
+
+class BooleanChecker(ValueChecker):
+
+ def __init__(self, **params):
+ if params.has_key('default'):
+ self.default = params['default']
+ else:
+ self.default = False
+
+ def check(self, value):
+ if not isinstance(value, bool):
+ raise TypeError, 'Expecting bool, got %s' % type(value)
+
+ def from_string(self, str_value, obj):
+ if str_value.lower() == 'true':
+ return True
+ else:
+ return False
+
+ def to_string(self, value):
+ self.check(value)
+ if value == True:
+ return 'true'
+ else:
+ return 'false'
+
+class DateTimeChecker(ValueChecker):
+
+ def __init__(self, **params):
+ if params.has_key('maxlength'):
+ self.maxlength = params['maxlength']
+ else:
+ self.maxlength = 1024
+ if params.has_key('default'):
+ self.default = params['default']
+ else:
+ self.default = datetime.now()
+
+ def check(self, value):
+ if not isinstance(value, datetime):
+ raise TypeError, 'Expecting datetime, got %s' % type(value)
+
+ def from_string(self, str_value, obj):
+ try:
+ return datetime.strptime(str_value, ISO8601)
+ except:
+ raise ValueError, 'Unable to convert %s to DateTime' % str_value
+
+ def to_string(self, value):
+ self.check(value)
+ return value.strftime(ISO8601)
+
+class ObjectChecker(ValueChecker):
+
+ def __init__(self, **params):
+ self.default = None
+ self.ref_class = params.get('ref_class', None)
+ if self.ref_class == None:
+ raise SDBPersistenceError('ref_class parameter is required')
+
+ def check(self, value):
+ if value == None:
+ return
+ if isinstance(value, str) or isinstance(value, unicode):
+ # ugly little hack - sometimes I want to just stick a UUID string
+ # in here rather than instantiate an object.
+ # This does a bit of hand waving to "type check" the string
+ t = value.split('-')
+ if len(t) != 5:
+ raise ValueError
+ else:
+ try:
+ obj_lineage = value.get_lineage()
+ cls_lineage = self.ref_class.get_lineage()
+ if obj_lineage.startswith(cls_lineage):
+ return
+ raise TypeError, '%s not instance of %s' % (obj_lineage, cls_lineage)
+ except:
+ raise ValueError, '%s is not an SDBObject' % value
+
+ def from_string(self, str_value, obj):
+ if not str_value:
+ return None
+ try:
+ return revive_object_from_id(str_value, obj._manager)
+ except:
+ raise ValueError, 'Unable to convert %s to Object' % str_value
+
+ def to_string(self, value):
+ self.check(value)
+ if isinstance(value, str) or isinstance(value, unicode):
+ return value
+ if value == None:
+ return ''
+ else:
+ return value.id
+
+class S3KeyChecker(ValueChecker):
+
+ def __init__(self, **params):
+ self.default = None
+
+ def check(self, value):
+ if value == None:
+ return
+ if isinstance(value, str) or isinstance(value, unicode):
+ try:
+ bucket_name, key_name = value.split('/', 1)
+ except:
+ raise ValueError
+ elif not isinstance(value, Key):
+ raise TypeError, 'Expecting Key, got %s' % type(value)
+
+ def from_string(self, str_value, obj):
+ if not str_value:
+ return None
+ if str_value == 'None':
+ return None
+ try:
+ bucket_name, key_name = str_value.split('/', 1)
+ if obj:
+ s3 = obj._manager.get_s3_connection()
+ bucket = s3.get_bucket(bucket_name)
+ key = bucket.get_key(key_name)
+ if not key:
+ key = bucket.new_key(key_name)
+ return key
+ except:
+ raise ValueError, 'Unable to convert %s to S3Key' % str_value
+
+ def to_string(self, value):
+ self.check(value)
+ if isinstance(value, str) or isinstance(value, unicode):
+ return value
+ if value == None:
+ return ''
+ else:
+ return '%s/%s' % (value.bucket.name, value.name)
+
+class S3BucketChecker(ValueChecker):
+
+ def __init__(self, **params):
+ self.default = None
+
+ def check(self, value):
+ if value == None:
+ return
+ if isinstance(value, str) or isinstance(value, unicode):
+ return
+ elif not isinstance(value, Bucket):
+ raise TypeError, 'Expecting Bucket, got %s' % type(value)
+
+ def from_string(self, str_value, obj):
+ if not str_value:
+ return None
+ if str_value == 'None':
+ return None
+ try:
+ if obj:
+ s3 = obj._manager.get_s3_connection()
+ bucket = s3.get_bucket(str_value)
+ return bucket
+ except:
+ raise ValueError, 'Unable to convert %s to S3Bucket' % str_value
+
+ def to_string(self, value):
+ self.check(value)
+ if value == None:
+ return ''
+ else:
+ return '%s' % value.name
+
diff --git a/api/boto/sdb/persist/object.py b/api/boto/sdb/persist/object.py
new file mode 100644
index 0000000..3646d43
--- /dev/null
+++ b/api/boto/sdb/persist/object.py
@@ -0,0 +1,207 @@
+# 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.
+
+from boto.exception import SDBPersistenceError
+from boto.sdb.persist import get_manager, object_lister
+from boto.sdb.persist.property import *
+import uuid
+
+class SDBBase(type):
+ "Metaclass for all SDBObjects"
+ def __init__(cls, name, bases, dict):
+ super(SDBBase, cls).__init__(name, bases, dict)
+ # Make sure this is a subclass of SDBObject - mainly copied from django ModelBase (thanks!)
+ try:
+ if filter(lambda b: issubclass(b, SDBObject), bases):
+ # look for all of the Properties and set their names
+ for key in dict.keys():
+ if isinstance(dict[key], Property):
+ property = dict[key]
+ property.set_name(key)
+ prop_names = []
+ props = cls.properties()
+ for prop in props:
+ prop_names.append(prop.name)
+ setattr(cls, '_prop_names', prop_names)
+ except NameError:
+ # 'SDBObject' isn't defined yet, meaning we're looking at our own
+ # SDBObject class, defined below.
+ pass
+
+class SDBObject(object):
+ __metaclass__ = SDBBase
+
+ _manager = get_manager()
+
+ @classmethod
+ def get_lineage(cls):
+ l = [c.__name__ for c in cls.mro()]
+ l.reverse()
+ return '.'.join(l)
+
+ @classmethod
+ def get(cls, id=None, **params):
+ if params.has_key('manager'):
+ manager = params['manager']
+ else:
+ manager = cls._manager
+ if manager.domain and id:
+ a = cls._manager.domain.get_attributes(id, '__type__')
+ if a.has_key('__type__'):
+ return cls(id, manager)
+ else:
+ raise SDBPersistenceError('%s object with id=%s does not exist' % (cls.__name__, id))
+ else:
+ rs = cls.find(**params)
+ try:
+ obj = rs.next()
+ except StopIteration:
+ raise SDBPersistenceError('%s object matching query does not exist' % cls.__name__)
+ try:
+ rs.next()
+ except StopIteration:
+ return obj
+ raise SDBPersistenceError('Query matched more than 1 item')
+
+ @classmethod
+ def find(cls, **params):
+ if params.has_key('manager'):
+ manager = params['manager']
+ del params['manager']
+ else:
+ manager = cls._manager
+ keys = params.keys()
+ if len(keys) > 4:
+ raise SDBPersistenceError('Too many fields, max is 4')
+ parts = ["['__type__'='%s'] union ['__lineage__'starts-with'%s']" % (cls.__name__, cls.get_lineage())]
+ properties = cls.properties()
+ for key in keys:
+ found = False
+ for property in properties:
+ if property.name == key:
+ found = True
+ if isinstance(property, ScalarProperty):
+ checker = property.checker
+ parts.append("['%s' = '%s']" % (key, checker.to_string(params[key])))
+ else:
+ raise SDBPersistenceError('%s is not a searchable field' % key)
+ if not found:
+ raise SDBPersistenceError('%s is not a valid field' % key)
+ query = ' intersection '.join(parts)
+ if manager.domain:
+ rs = manager.domain.query(query)
+ else:
+ rs = []
+ return object_lister(None, rs, manager)
+
+ @classmethod
+ def list(cls, max_items=None, manager=None):
+ if not manager:
+ manager = cls._manager
+ if manager.domain:
+ rs = manager.domain.query("['__type__' = '%s']" % cls.__name__, max_items=max_items)
+ else:
+ rs = []
+ return object_lister(cls, rs, manager)
+
+ @classmethod
+ def properties(cls):
+ properties = []
+ while cls:
+ for key in cls.__dict__.keys():
+ if isinstance(cls.__dict__[key], Property):
+ properties.append(cls.__dict__[key])
+ if len(cls.__bases__) > 0:
+ cls = cls.__bases__[0]
+ else:
+ cls = None
+ return properties
+
+ # for backwards compatibility
+ find_properties = properties
+
+ def __init__(self, id=None, manager=None):
+ if manager:
+ self._manager = manager
+ self.id = id
+ if self.id:
+ self._auto_update = True
+ if self._manager.domain:
+ attrs = self._manager.domain.get_attributes(self.id, '__type__')
+ if len(attrs.keys()) == 0:
+ raise SDBPersistenceError('Object %s: not found' % self.id)
+ else:
+ self.id = str(uuid.uuid4())
+ self._auto_update = False
+
+ def __setattr__(self, name, value):
+ if name in self._prop_names:
+ object.__setattr__(self, name, value)
+ elif name.startswith('_'):
+ object.__setattr__(self, name, value)
+ elif name == 'id':
+ object.__setattr__(self, name, value)
+ else:
+ self._persist_attribute(name, value)
+ object.__setattr__(self, name, value)
+
+ def __getattr__(self, name):
+ if not name.startswith('_'):
+ a = self._manager.domain.get_attributes(self.id, name)
+ if a.has_key(name):
+ object.__setattr__(self, name, a[name])
+ return a[name]
+ raise AttributeError
+
+ def __repr__(self):
+ return '%s<%s>' % (self.__class__.__name__, self.id)
+
+ def _persist_attribute(self, name, value):
+ if self.id:
+ self._manager.domain.put_attributes(self.id, {name : value}, replace=True)
+
+ def _get_sdb_item(self):
+ return self._manager.domain.get_item(self.id)
+
+ def save(self):
+ attrs = {'__type__' : self.__class__.__name__,
+ '__module__' : self.__class__.__module__,
+ '__lineage__' : self.get_lineage()}
+ for property in self.properties():
+ attrs[property.name] = property.to_string(self)
+ if self._manager.domain:
+ self._manager.domain.put_attributes(self.id, attrs, replace=True)
+ self._auto_update = True
+
+ def delete(self):
+ if self._manager.domain:
+ self._manager.domain.delete_attributes(self.id)
+
+ def get_related_objects(self, ref_name, ref_cls=None):
+ if self._manager.domain:
+ query = "['%s' = '%s']" % (ref_name, self.id)
+ if ref_cls:
+ query += " intersection ['__type__'='%s']" % ref_cls.__name__
+ rs = self._manager.domain.query(query)
+ else:
+ rs = []
+ return object_lister(ref_cls, rs, self._manager)
+
diff --git a/api/boto/sdb/persist/property.py b/api/boto/sdb/persist/property.py
new file mode 100644
index 0000000..6eea765
--- /dev/null
+++ b/api/boto/sdb/persist/property.py
@@ -0,0 +1,370 @@
+# 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.
+
+from boto.exception import SDBPersistenceError
+from boto.sdb.persist.checker import *
+from boto.utils import Password
+
+class Property(object):
+
+ def __init__(self, checker_class, **params):
+ self.name = ''
+ self.checker = checker_class(**params)
+ self.slot_name = '__'
+
+ def set_name(self, name):
+ self.name = name
+ self.slot_name = '__' + self.name
+
+class ScalarProperty(Property):
+
+ def save(self, obj):
+ domain = obj._manager.domain
+ domain.put_attributes(obj.id, {self.name : self.to_string(obj)}, replace=True)
+
+ def to_string(self, obj):
+ return self.checker.to_string(getattr(obj, self.name))
+
+ def load(self, obj):
+ domain = obj._manager.domain
+ a = domain.get_attributes(obj.id, self.name)
+ # try to get the attribute value from SDB
+ if self.name in a:
+ value = self.checker.from_string(a[self.name], obj)
+ setattr(obj, self.slot_name, value)
+ # if it's not there, set the value to the default value
+ else:
+ self.__set__(obj, self.checker.default)
+
+ def __get__(self, obj, objtype):
+ if obj:
+ try:
+ value = getattr(obj, self.slot_name)
+ except AttributeError:
+ if obj._auto_update:
+ self.load(obj)
+ value = getattr(obj, self.slot_name)
+ else:
+ value = self.checker.default
+ setattr(obj, self.slot_name, self.checker.default)
+ return value
+
+ def __set__(self, obj, value):
+ self.checker.check(value)
+ try:
+ old_value = getattr(obj, self.slot_name)
+ except:
+ old_value = self.checker.default
+ setattr(obj, self.slot_name, value)
+ if obj._auto_update:
+ try:
+ self.save(obj)
+ except:
+ setattr(obj, self.slot_name, old_value)
+ raise
+
+class StringProperty(ScalarProperty):
+
+ def __init__(self, **params):
+ ScalarProperty.__init__(self, StringChecker, **params)
+
+class PasswordProperty(ScalarProperty):
+ """
+ Hashed password
+ """
+
+ def __init__(self, **params):
+ ScalarProperty.__init__(self, PasswordChecker, **params)
+
+ def __set__(self, obj, value):
+ p = Password()
+ p.set(value)
+ ScalarProperty.__set__(self, obj, p)
+
+ def __get__(self, obj, objtype):
+ return Password(ScalarProperty.__get__(self, obj, objtype))
+
+class SmallPositiveIntegerProperty(ScalarProperty):
+
+ def __init__(self, **params):
+ params['size'] = 'small'
+ params['signed'] = False
+ ScalarProperty.__init__(self, IntegerChecker, **params)
+
+class SmallIntegerProperty(ScalarProperty):
+
+ def __init__(self, **params):
+ params['size'] = 'small'
+ params['signed'] = True
+ ScalarProperty.__init__(self, IntegerChecker, **params)
+
+class PositiveIntegerProperty(ScalarProperty):
+
+ def __init__(self, **params):
+ params['size'] = 'medium'
+ params['signed'] = False
+ ScalarProperty.__init__(self, IntegerChecker, **params)
+
+class IntegerProperty(ScalarProperty):
+
+ def __init__(self, **params):
+ params['size'] = 'medium'
+ params['signed'] = True
+ ScalarProperty.__init__(self, IntegerChecker, **params)
+
+class LargePositiveIntegerProperty(ScalarProperty):
+
+ def __init__(self, **params):
+ params['size'] = 'large'
+ params['signed'] = False
+ ScalarProperty.__init__(self, IntegerChecker, **params)
+
+class LargeIntegerProperty(ScalarProperty):
+
+ def __init__(self, **params):
+ params['size'] = 'large'
+ params['signed'] = True
+ ScalarProperty.__init__(self, IntegerChecker, **params)
+
+class BooleanProperty(ScalarProperty):
+
+ def __init__(self, **params):
+ ScalarProperty.__init__(self, BooleanChecker, **params)
+
+class DateTimeProperty(ScalarProperty):
+
+ def __init__(self, **params):
+ ScalarProperty.__init__(self, DateTimeChecker, **params)
+
+class ObjectProperty(ScalarProperty):
+
+ def __init__(self, **params):
+ ScalarProperty.__init__(self, ObjectChecker, **params)
+
+class S3KeyProperty(ScalarProperty):
+
+ def __init__(self, **params):
+ ScalarProperty.__init__(self, S3KeyChecker, **params)
+
+ def __set__(self, obj, value):
+ self.checker.check(value)
+ try:
+ old_value = getattr(obj, self.slot_name)
+ except:
+ old_value = self.checker.default
+ if isinstance(value, str):
+ value = self.checker.from_string(value, obj)
+ setattr(obj, self.slot_name, value)
+ if obj._auto_update:
+ try:
+ self.save(obj)
+ except:
+ setattr(obj, self.slot_name, old_value)
+ raise
+
+class S3BucketProperty(ScalarProperty):
+
+ def __init__(self, **params):
+ ScalarProperty.__init__(self, S3BucketChecker, **params)
+
+ def __set__(self, obj, value):
+ self.checker.check(value)
+ try:
+ old_value = getattr(obj, self.slot_name)
+ except:
+ old_value = self.checker.default
+ if isinstance(value, str):
+ value = self.checker.from_string(value, obj)
+ setattr(obj, self.slot_name, value)
+ if obj._auto_update:
+ try:
+ self.save(obj)
+ except:
+ setattr(obj, self.slot_name, old_value)
+ raise
+
+class MultiValueProperty(Property):
+
+ def __init__(self, checker_class, **params):
+ Property.__init__(self, checker_class, **params)
+
+ def __get__(self, obj, objtype):
+ if obj:
+ try:
+ value = getattr(obj, self.slot_name)
+ except AttributeError:
+ if obj._auto_update:
+ self.load(obj)
+ value = getattr(obj, self.slot_name)
+ else:
+ value = MultiValue(self, obj, [])
+ setattr(obj, self.slot_name, value)
+ return value
+
+ def load(self, obj):
+ if obj != None:
+ _list = []
+ domain = obj._manager.domain
+ a = domain.get_attributes(obj.id, self.name)
+ if self.name in a:
+ lst = a[self.name]
+ if not isinstance(lst, list):
+ lst = [lst]
+ for value in lst:
+ value = self.checker.from_string(value, obj)
+ _list.append(value)
+ setattr(obj, self.slot_name, MultiValue(self, obj, _list))
+
+ def __set__(self, obj, value):
+ if not isinstance(value, list):
+ raise SDBPersistenceError('Value must be a list')
+ setattr(obj, self.slot_name, MultiValue(self, obj, value))
+ str_list = self.to_string(obj)
+ domain = obj._manager.domain
+ if obj._auto_update:
+ if len(str_list) == 1:
+ domain.put_attributes(obj.id, {self.name : str_list[0]}, replace=True)
+ else:
+ try:
+ self.__delete__(obj)
+ except:
+ pass
+ domain.put_attributes(obj.id, {self.name : str_list}, replace=True)
+ setattr(obj, self.slot_name, MultiValue(self, obj, value))
+
+ def __delete__(self, obj):
+ if obj._auto_update:
+ domain = obj._manager.domain
+ domain.delete_attributes(obj.id, [self.name])
+ setattr(obj, self.slot_name, MultiValue(self, obj, []))
+
+ def to_string(self, obj):
+ str_list = []
+ for value in self.__get__(obj, type(obj)):
+ str_list.append(self.checker.to_string(value))
+ return str_list
+
+class StringListProperty(MultiValueProperty):
+
+ def __init__(self, **params):
+ MultiValueProperty.__init__(self, StringChecker, **params)
+
+class SmallIntegerListProperty(MultiValueProperty):
+
+ def __init__(self, **params):
+ params['size'] = 'small'
+ params['signed'] = True
+ MultiValueProperty.__init__(self, IntegerChecker, **params)
+
+class SmallPositiveIntegerListProperty(MultiValueProperty):
+
+ def __init__(self, **params):
+ params['size'] = 'small'
+ params['signed'] = False
+ MultiValueProperty.__init__(self, IntegerChecker, **params)
+
+class IntegerListProperty(MultiValueProperty):
+
+ def __init__(self, **params):
+ params['size'] = 'medium'
+ params['signed'] = True
+ MultiValueProperty.__init__(self, IntegerChecker, **params)
+
+class PositiveIntegerListProperty(MultiValueProperty):
+
+ def __init__(self, **params):
+ params['size'] = 'medium'
+ params['signed'] = False
+ MultiValueProperty.__init__(self, IntegerChecker, **params)
+
+class LargeIntegerListProperty(MultiValueProperty):
+
+ def __init__(self, **params):
+ params['size'] = 'large'
+ params['signed'] = True
+ MultiValueProperty.__init__(self, IntegerChecker, **params)
+
+class LargePositiveIntegerListProperty(MultiValueProperty):
+
+ def __init__(self, **params):
+ params['size'] = 'large'
+ params['signed'] = False
+ MultiValueProperty.__init__(self, IntegerChecker, **params)
+
+class BooleanListProperty(MultiValueProperty):
+
+ def __init__(self, **params):
+ MultiValueProperty.__init__(self, BooleanChecker, **params)
+
+class ObjectListProperty(MultiValueProperty):
+
+ def __init__(self, **params):
+ MultiValueProperty.__init__(self, ObjectChecker, **params)
+
+class HasManyProperty(Property):
+
+ def set_name(self, name):
+ self.name = name
+ self.slot_name = '__' + self.name
+
+ def __get__(self, obj, objtype):
+ return self
+
+
+class MultiValue:
+ """
+ Special Multi Value for boto persistence layer to allow us to do
+ obj.list.append(foo)
+ """
+ def __init__(self, property, obj, _list):
+ self.checker = property.checker
+ self.name = property.name
+ self.object = obj
+ self._list = _list
+
+ def __repr__(self):
+ return repr(self._list)
+
+ def __getitem__(self, key):
+ return self._list.__getitem__(key)
+
+ def __delitem__(self, key):
+ item = self[key]
+ self._list.__delitem__(key)
+ domain = self.object._manager.domain
+ domain.delete_attributes(self.object.id, {self.name: [self.checker.to_string(item)]})
+
+ def __len__(self):
+ return len(self._list)
+
+ def append(self, value):
+ self.checker.check(value)
+ self._list.append(value)
+ domain = self.object._manager.domain
+ domain.put_attributes(self.object.id, {self.name: self.checker.to_string(value)}, replace=False)
+
+ def index(self, value):
+ for x in self._list:
+ if x.id == value.id:
+ return self._list.index(x)
+
+ def remove(self, value):
+ del(self[self.index(value)])
diff --git a/api/boto/sdb/persist/test_persist.py b/api/boto/sdb/persist/test_persist.py
new file mode 100644
index 0000000..3207e58
--- /dev/null
+++ b/api/boto/sdb/persist/test_persist.py
@@ -0,0 +1,138 @@
+from boto.sdb.persist.object import SDBObject
+from boto.sdb.persist.property import *
+from boto.sdb.persist import Manager
+from datetime import datetime
+import time
+
+#
+# This will eventually be moved to the boto.tests module and become a real unit test
+# but for now it will live here. It shows examples of each of the Property types in
+# use and tests the basic operations.
+#
+class TestScalar(SDBObject):
+
+ name = StringProperty()
+ description = StringProperty()
+ size = PositiveIntegerProperty()
+ offset = IntegerProperty()
+ foo = BooleanProperty()
+ date = DateTimeProperty()
+ file = S3KeyProperty()
+
+class TestRef(SDBObject):
+
+ name = StringProperty()
+ ref = ObjectProperty(ref_class=TestScalar)
+
+class TestSubClass1(TestRef):
+
+ answer = PositiveIntegerProperty()
+
+class TestSubClass2(TestScalar):
+
+ flag = BooleanProperty()
+
+class TestList(SDBObject):
+
+ names = StringListProperty()
+ numbers = PositiveIntegerListProperty()
+ bools = BooleanListProperty()
+ objects = ObjectListProperty(ref_class=TestScalar)
+
+def test1():
+ s = TestScalar()
+ s.name = 'foo'
+ s.description = 'This is foo'
+ s.size = 42
+ s.offset = -100
+ s.foo = True
+ s.date = datetime.now()
+ s.save()
+ return s
+
+def test2(ref_name):
+ s = TestRef()
+ s.name = 'testref'
+ rs = TestScalar.find(name=ref_name)
+ s.ref = rs.next()
+ s.save()
+ return s
+
+def test3():
+ s = TestScalar()
+ s.name = 'bar'
+ s.description = 'This is bar'
+ s.size = 24
+ s.foo = False
+ s.date = datetime.now()
+ s.save()
+ return s
+
+def test4(ref1, ref2):
+ s = TestList()
+ s.names.append(ref1.name)
+ s.names.append(ref2.name)
+ s.numbers.append(ref1.size)
+ s.numbers.append(ref2.size)
+ s.bools.append(ref1.foo)
+ s.bools.append(ref2.foo)
+ s.objects.append(ref1)
+ s.objects.append(ref2)
+ s.save()
+ return s
+
+def test5(ref):
+ s = TestSubClass1()
+ s.answer = 42
+ s.ref = ref
+ s.save()
+ # test out free form attribute
+ s.fiddlefaddle = 'this is fiddlefaddle'
+ s._fiddlefaddle = 'this is not fiddlefaddle'
+ return s
+
+def test6():
+ s = TestSubClass2()
+ s.name = 'fie'
+ s.description = 'This is fie'
+ s.size = 4200
+ s.offset = -820
+ s.foo = False
+ s.date = datetime.now()
+ s.flag = True
+ s.save()
+ return s
+
+def test(domain_name):
+ print 'Initialize the Persistance system'
+ Manager.DefaultDomainName = domain_name
+ print 'Call test1'
+ s1 = test1()
+ # now create a new instance and read the saved data from SDB
+ print 'Now sleep to wait for things to converge'
+ time.sleep(5)
+ print 'Now lookup the object and compare the fields'
+ s2 = TestScalar(s1.id)
+ assert s1.name == s2.name
+ assert s1.description == s2.description
+ assert s1.size == s2.size
+ assert s1.offset == s2.offset
+ assert s1.foo == s2.foo
+ #assert s1.date == s2.date
+ print 'Call test2'
+ s2 = test2(s1.name)
+ print 'Call test3'
+ s3 = test3()
+ print 'Call test4'
+ s4 = test4(s1, s3)
+ print 'Call test5'
+ s6 = test6()
+ s5 = test5(s6)
+ domain = s5._manager.domain
+ item1 = domain.get_item(s1.id)
+ item2 = domain.get_item(s2.id)
+ item3 = domain.get_item(s3.id)
+ item4 = domain.get_item(s4.id)
+ item5 = domain.get_item(s5.id)
+ item6 = domain.get_item(s6.id)
+ return [(s1, item1), (s2, item2), (s3, item3), (s4, item4), (s5, item5), (s6, item6)]
diff --git a/api/boto/sdb/queryresultset.py b/api/boto/sdb/queryresultset.py
new file mode 100644
index 0000000..a9430f4
--- /dev/null
+++ b/api/boto/sdb/queryresultset.py
@@ -0,0 +1,92 @@
+# 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.sdb.item import Item
+
+def query_lister(domain, query='', max_items=None, attr_names=None):
+ more_results = True
+ num_results = 0
+ next_token = None
+ while more_results:
+ rs = domain.connection.query_with_attributes(domain, query, attr_names,
+ next_token=next_token)
+ for item in rs:
+ if max_items:
+ if num_results == max_items:
+ raise StopIteration
+ yield item
+ num_results += 1
+ next_token = rs.next_token
+ more_results = next_token != None
+
+class QueryResultSet:
+
+ def __init__(self, domain=None, query='', max_items=None, attr_names=None):
+ self.max_items = max_items
+ self.domain = domain
+ self.query = query
+ self.attr_names = attr_names
+
+ def __iter__(self):
+ return query_lister(self.domain, self.query, self.max_items, self.attr_names)
+
+def select_lister(domain, query='', max_items=None):
+ more_results = True
+ num_results = 0
+ next_token = None
+ while more_results:
+ rs = domain.connection.select(domain, query, next_token=next_token)
+ for item in rs:
+ if max_items:
+ if num_results == max_items:
+ raise StopIteration
+ yield item
+ num_results += 1
+ next_token = rs.next_token
+ more_results = next_token != None
+
+class SelectResultSet(object):
+
+ def __init__(self, domain=None, query='', max_items=None,
+ next_token=None):
+ self.domain = domain
+ self.query = query
+ self.max_items = max_items
+ self.next_token = next_token
+
+ def __iter__(self):
+ more_results = True
+ num_results = 0
+ while more_results:
+ rs = self.domain.connection.select(self.domain, self.query,
+ next_token=self.next_token)
+ for item in rs:
+ if self.max_items and num_results >= self.max_items:
+ raise StopIteration
+ yield item
+ num_results += 1
+ self.next_token = rs.next_token
+ if self.max_items and num_results >= self.max_items:
+ raise StopIteration
+ more_results = self.next_token != None
+
+ def next(self):
+ return self.__iter__().next()
diff --git a/api/boto/sdb/regioninfo.py b/api/boto/sdb/regioninfo.py
new file mode 100644
index 0000000..bff9dea
--- /dev/null
+++ b/api/boto/sdb/regioninfo.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 boto.ec2.regioninfo import RegionInfo
+
+class SDBRegionInfo(RegionInfo):
+
+ def connect(self, **kw_params):
+ """
+ Connect to this Region's endpoint. Returns an SDBConnection
+ object pointing to the endpoint associated with this region.
+ You may pass any of the arguments accepted by the SDBConnection
+ object's constructor as keyword arguments and they will be
+ passed along to the SDBConnection object.
+
+ :rtype: :class:`boto.sdb.connection.SDBConnection`
+ :return: The connection to this regions endpoint
+ """
+ from boto.sdb.connection import SDBConnection
+ return SDBConnection(region=self, **kw_params)
+
diff --git a/api/boto/services/__init__.py b/api/boto/services/__init__.py
new file mode 100644
index 0000000..449bd16
--- /dev/null
+++ b/api/boto/services/__init__.py
@@ -0,0 +1,23 @@
+# 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/api/boto/services/bs.py b/api/boto/services/bs.py
new file mode 100755
index 0000000..aafe867
--- /dev/null
+++ b/api/boto/services/bs.py
@@ -0,0 +1,180 @@
+#!/usr/bin/env python
+# 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.
+from optparse import OptionParser
+from boto.services.servicedef import ServiceDef
+from boto.services.message import ServiceMessage
+from boto.services.submit import Submitter
+from boto.services.result import ResultProcessor
+import boto
+import sys, os, StringIO
+
+class BS(object):
+
+ Usage = "usage: %prog [options] config_file command"
+
+ Commands = {'reset' : 'Clear input queue and output bucket',
+ 'submit' : 'Submit local files to the service',
+ 'start' : 'Start the service',
+ 'status' : 'Report on the status of the service buckets and queues',
+ 'retrieve' : 'Retrieve output generated by a batch',
+ 'batches' : 'List all batches stored in current output_domain'}
+
+ def __init__(self):
+ self.service_name = None
+ self.parser = OptionParser(usage=self.Usage)
+ self.parser.add_option("--help-commands", action="store_true", dest="help_commands",
+ help="provides help on the available commands")
+ self.parser.add_option("-a", "--access-key", action="store", type="string",
+ help="your AWS Access Key")
+ self.parser.add_option("-s", "--secret-key", action="store", type="string",
+ help="your AWS Secret Access Key")
+ self.parser.add_option("-p", "--path", action="store", type="string", dest="path",
+ help="the path to local directory for submit and retrieve")
+ self.parser.add_option("-k", "--keypair", action="store", type="string", dest="keypair",
+ help="the SSH keypair used with launched instance(s)")
+ self.parser.add_option("-l", "--leave", action="store_true", dest="leave",
+ help="leave the files (don't retrieve) files during retrieve command")
+ self.parser.set_defaults(leave=False)
+ self.parser.add_option("-n", "--num-instances", action="store", type="string", dest="num_instances",
+ help="the number of launched instance(s)")
+ self.parser.set_defaults(num_instances=1)
+ self.parser.add_option("-i", "--ignore-dirs", action="append", type="string", dest="ignore",
+ help="directories that should be ignored by submit command")
+ self.parser.add_option("-b", "--batch-id", action="store", type="string", dest="batch",
+ help="batch identifier required by the retrieve command")
+
+ def print_command_help(self):
+ print '\nCommands:'
+ for key in self.Commands.keys():
+ print ' %s\t\t%s' % (key, self.Commands[key])
+
+ def do_reset(self):
+ iq = self.sd.get_obj('input_queue')
+ if iq:
+ print 'clearing out input queue'
+ i = 0
+ m = iq.read()
+ while m:
+ i += 1
+ iq.delete_message(m)
+ m = iq.read()
+ print 'deleted %d messages' % i
+ ob = self.sd.get_obj('output_bucket')
+ ib = self.sd.get_obj('input_bucket')
+ if ob:
+ if ib and ob.name == ib.name:
+ return
+ print 'delete generated files in output bucket'
+ i = 0
+ for k in ob:
+ i += 1
+ k.delete()
+ print 'deleted %d keys' % i
+
+ def do_submit(self):
+ if not self.options.path:
+ self.parser.error('No path provided')
+ if not os.path.exists(self.options.path):
+ self.parser.error('Invalid path (%s)' % self.options.path)
+ s = Submitter(self.sd)
+ t = s.submit_path(self.options.path, None, self.options.ignore, None,
+ None, True, self.options.path)
+ print 'A total of %d files were submitted' % t[1]
+ print 'Batch Identifier: %s' % t[0]
+
+ def do_start(self):
+ ami_id = self.sd.get('ami_id')
+ instance_type = self.sd.get('instance_type', 'm1.small')
+ security_group = self.sd.get('security_group', 'default')
+ if not ami_id:
+ self.parser.error('ami_id option is required when starting the service')
+ ec2 = boto.connect_ec2()
+ if not self.sd.has_section('Credentials'):
+ self.sd.add_section('Credentials')
+ self.sd.set('Credentials', 'aws_access_key_id', ec2.aws_access_key_id)
+ self.sd.set('Credentials', 'aws_secret_access_key', ec2.aws_secret_access_key)
+ s = StringIO.StringIO()
+ self.sd.write(s)
+ rs = ec2.get_all_images([ami_id])
+ img = rs[0]
+ r = img.run(user_data=s.getvalue(), key_name=self.options.keypair,
+ max_count=self.options.num_instances,
+ instance_type=instance_type,
+ security_groups=[security_group])
+ print 'Starting AMI: %s' % ami_id
+ print 'Reservation %s contains the following instances:' % r.id
+ for i in r.instances:
+ print '\t%s' % i.id
+
+ def do_status(self):
+ iq = self.sd.get_obj('input_queue')
+ if iq:
+ print 'The input_queue (%s) contains approximately %s messages' % (iq.id, iq.count())
+ ob = self.sd.get_obj('output_bucket')
+ ib = self.sd.get_obj('input_bucket')
+ if ob:
+ if ib and ob.name == ib.name:
+ return
+ total = 0
+ for k in ob:
+ total += 1
+ print 'The output_bucket (%s) contains %d keys' % (ob.name, total)
+
+ def do_retrieve(self):
+ if not self.options.path:
+ self.parser.error('No path provided')
+ if not os.path.exists(self.options.path):
+ self.parser.error('Invalid path (%s)' % self.options.path)
+ if not self.options.batch:
+ self.parser.error('batch identifier is required for retrieve command')
+ s = ResultProcessor(self.options.batch, self.sd)
+ s.get_results(self.options.path, get_file=(not self.options.leave))
+
+ def do_batches(self):
+ d = self.sd.get_obj('output_domain')
+ if d:
+ print 'Available Batches:'
+ rs = d.query("['type'='Batch']")
+ for item in rs:
+ print ' %s' % item.name
+ else:
+ self.parser.error('No output_domain specified for service')
+
+ def main(self):
+ self.options, self.args = self.parser.parse_args()
+ if self.options.help_commands:
+ self.print_command_help()
+ sys.exit(0)
+ if len(self.args) != 2:
+ self.parser.error("config_file and command are required")
+ self.config_file = self.args[0]
+ self.sd = ServiceDef(self.config_file)
+ self.command = self.args[1]
+ if hasattr(self, 'do_%s' % self.command):
+ method = getattr(self, 'do_%s' % self.command)
+ method()
+ else:
+ self.parser.error('command (%s) not recognized' % self.command)
+
+if __name__ == "__main__":
+ bs = BS()
+ bs.main()
diff --git a/api/boto/services/message.py b/api/boto/services/message.py
new file mode 100644
index 0000000..6bb2e58
--- /dev/null
+++ b/api/boto/services/message.py
@@ -0,0 +1,59 @@
+# 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 boto
+from boto.sqs.message import MHMessage
+from boto.utils import get_ts
+from socket import gethostname
+import os, mimetypes, time
+
+class ServiceMessage(MHMessage):
+
+ def for_key(self, key, params=None, bucket_name=None):
+ if params:
+ self.update(params)
+ if key.path:
+ t = os.path.split(key.path)
+ self['OriginalLocation'] = t[0]
+ self['OriginalFileName'] = t[1]
+ mime_type = mimetypes.guess_type(t[1])[0]
+ if mime_type == None:
+ mime_type = 'application/octet-stream'
+ self['Content-Type'] = mime_type
+ s = os.stat(key.path)
+ t = time.gmtime(s[7])
+ self['FileAccessedDate'] = get_ts(t)
+ t = time.gmtime(s[8])
+ self['FileModifiedDate'] = get_ts(t)
+ t = time.gmtime(s[9])
+ self['FileCreateDate'] = get_ts(t)
+ else:
+ self['OriginalFileName'] = key.name
+ self['OriginalLocation'] = key.bucket.name
+ self['ContentType'] = key.content_type
+ self['Host'] = gethostname()
+ if bucket_name:
+ self['Bucket'] = bucket_name
+ else:
+ self['Bucket'] = key.bucket.name
+ self['InputKey'] = key.name
+ self['Size'] = key.size
+
diff --git a/api/boto/services/result.py b/api/boto/services/result.py
new file mode 100644
index 0000000..240085b
--- /dev/null
+++ b/api/boto/services/result.py
@@ -0,0 +1,140 @@
+#!/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, os, time, mimetypes
+from datetime import datetime, timedelta
+from boto.services.servicedef import ServiceDef
+from boto.utils import parse_ts
+import boto
+
+class ResultProcessor:
+
+ LogFileName = 'log.csv'
+
+ def __init__(self, batch_name, sd, mimetype_files=None):
+ self.sd = sd
+ self.batch = batch_name
+ self.log_fp = None
+ self.num_files = 0
+ self.total_time = 0
+ self.min_time = timedelta.max
+ self.max_time = timedelta.min
+ self.earliest_time = datetime.max
+ self.latest_time = datetime.min
+ self.queue = self.sd.get_obj('output_queue')
+ self.domain = self.sd.get_obj('output_domain')
+
+ def calculate_stats(self, msg):
+ start_time = parse_ts(msg['Service-Read'])
+ end_time = parse_ts(msg['Service-Write'])
+ elapsed_time = end_time - start_time
+ if elapsed_time > self.max_time:
+ self.max_time = elapsed_time
+ if elapsed_time < self.min_time:
+ self.min_time = elapsed_time
+ self.total_time += elapsed_time.seconds
+ if start_time < self.earliest_time:
+ self.earliest_time = start_time
+ if end_time > self.latest_time:
+ self.latest_time = end_time
+
+ def log_message(self, msg, path):
+ keys = msg.keys()
+ keys.sort()
+ if not self.log_fp:
+ self.log_fp = open(os.path.join(path, self.LogFileName), 'w')
+ line = ','.join(keys)
+ self.log_fp.write(line+'\n')
+ values = []
+ for key in keys:
+ value = msg[key]
+ if value.find(',') > 0:
+ value = '"%s"' % value
+ values.append(value)
+ line = ','.join(values)
+ self.log_fp.write(line+'\n')
+
+ def process_record(self, record, path, get_file=True):
+ self.log_message(record, path)
+ self.calculate_stats(record)
+ outputs = record['OutputKey'].split(',')
+ if record.has_key('OutputBucket'):
+ bucket = boto.lookup('s3', record['OutputBucket'])
+ else:
+ bucket = boto.lookup('s3', record['Bucket'])
+ for output in outputs:
+ if get_file:
+ key_name, type = output.split(';')
+ if type:
+ mimetype = type.split('=')[1]
+ key = bucket.lookup(key_name)
+ file_name = os.path.join(path, key_name)
+ print 'retrieving file: %s to %s' % (key_name, file_name)
+ key.get_contents_to_filename(file_name)
+ self.num_files += 1
+
+ def get_results_from_queue(self, path, get_file=True, delete_msg=True):
+ m = self.queue.read()
+ while m:
+ if m.has_key('Batch') and m['Batch'] == self.batch:
+ self.process_record(m, path, get_file)
+ if delete_msg:
+ self.queue.delete_message(m)
+ m = self.queue.read()
+
+ def get_results_from_domain(self, path, get_file=True):
+ rs = self.domain.query("['Batch'='%s']" % self.batch)
+ for item in rs:
+ self.process_record(item, path, get_file)
+
+ def get_results_from_bucket(self, path):
+ bucket = self.sd.get_obj('output_bucket')
+ if bucket:
+ print 'No output queue or domain, just retrieving files from output_bucket'
+ for key in bucket:
+ file_name = os.path.join(path, key_name)
+ print 'retrieving file: %s to %s' % (key_name, file_name)
+ key.get_contents_to_filename(file_name)
+ self.num_files + 1
+
+ def get_results(self, path, get_file=True, delete_msg=True):
+ if not os.path.isdir(path):
+ os.mkdir(path)
+ if self.queue:
+ self.get_results_from_queue(path, get_file)
+ elif self.domain:
+ self.get_results_from_domain(path, get_file)
+ else:
+ self.get_results_from_bucket(path)
+ if self.log_fp:
+ self.log_fp.close()
+ print '%d results successfully retrieved.' % self.num_files
+ if self.num_files > 0:
+ self.avg_time = float(self.total_time)/self.num_files
+ print 'Minimum Processing Time: %d' % self.min_time.seconds
+ print 'Maximum Processing Time: %d' % self.max_time.seconds
+ print 'Average Processing Time: %f' % self.avg_time
+ self.elapsed_time = self.latest_time-self.earliest_time
+ print 'Elapsed Time: %d' % self.elapsed_time.seconds
+ tput = 1.0 / ((self.elapsed_time.seconds/60.0) / self.num_files)
+ print 'Throughput: %f transactions / minute' % tput
+
diff --git a/api/boto/services/service.py b/api/boto/services/service.py
new file mode 100644
index 0000000..942c47f
--- /dev/null
+++ b/api/boto/services/service.py
@@ -0,0 +1,163 @@
+# 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 boto
+from boto.services.message import ServiceMessage
+from boto.services.servicedef import ServiceDef
+from boto.pyami.scriptbase import ScriptBase
+from boto.exception import S3ResponseError
+from boto.utils import get_ts
+import StringIO
+import time
+import os
+import sys, traceback
+import mimetypes
+
+class Service(ScriptBase):
+
+ # Time required to process a transaction
+ ProcessingTime = 60
+
+ def __init__(self, config_file=None, mimetype_files=None):
+ ScriptBase.__init__(self, config_file)
+ self.name = self.__class__.__name__
+ self.working_dir = boto.config.get('Pyami', 'working_dir')
+ self.sd = ServiceDef(config_file)
+ self.retry_count = self.sd.getint('retry_count', 5)
+ self.loop_delay = self.sd.getint('loop_delay', 30)
+ self.processing_time = self.sd.getint('processing_time', 60)
+ self.input_queue = self.sd.get_obj('input_queue')
+ self.output_queue = self.sd.get_obj('output_queue')
+ self.output_domain = self.sd.get_obj('output_domain')
+ if mimetype_files:
+ mimetypes.init(mimetype_files)
+
+ def split_key(key):
+ if key.find(';') < 0:
+ t = (key, '')
+ else:
+ key, type = key.split(';')
+ label, mtype = type.split('=')
+ t = (key, mtype)
+ return t
+
+ def read_message(self):
+ boto.log.info('read_message')
+ message = self.input_queue.read(self.processing_time)
+ if message:
+ boto.log.info(message.get_body())
+ key = 'Service-Read'
+ message[key] = get_ts()
+ return message
+
+ # retrieve the source file from S3
+ def get_file(self, message):
+ bucket_name = message['Bucket']
+ key_name = message['InputKey']
+ file_name = os.path.join(self.working_dir, message.get('OriginalFileName', 'in_file'))
+ boto.log.info('get_file: %s/%s to %s' % (bucket_name, key_name, file_name))
+ bucket = boto.lookup('s3', bucket_name)
+ key = bucket.new_key(key_name)
+ key.get_contents_to_filename(os.path.join(self.working_dir, file_name))
+ return file_name
+
+ # process source file, return list of output files
+ def process_file(self, in_file_name, msg):
+ return []
+
+ # store result file in S3
+ def put_file(self, bucket_name, file_path, key_name=None):
+ boto.log.info('putting file %s as %s.%s' % (file_path, bucket_name, key_name))
+ bucket = boto.lookup('s3', bucket_name)
+ key = bucket.new_key(key_name)
+ key.set_contents_from_filename(file_path)
+ return key
+
+ def save_results(self, results, input_message, output_message):
+ output_keys = []
+ for file, type in results:
+ if input_message.has_key('OutputBucket'):
+ output_bucket = input_message['OutputBucket']
+ else:
+ output_bucket = input_message['Bucket']
+ key_name = os.path.split(file)[1]
+ key = self.put_file(output_bucket, file, key_name)
+ output_keys.append('%s;type=%s' % (key.name, type))
+ output_message['OutputKey'] = ','.join(output_keys)
+
+ # write message to each output queue
+ def write_message(self, message):
+ message['Service-Write'] = get_ts()
+ message['Server'] = self.name
+ if os.environ.has_key('HOSTNAME'):
+ message['Host'] = os.environ['HOSTNAME']
+ else:
+ message['Host'] = 'unknown'
+ message['Instance-ID'] = self.instance_id
+ if self.output_queue:
+ boto.log.info('Writing message to SQS queue: %s' % self.output_queue.id)
+ self.output_queue.write(message)
+ if self.output_domain:
+ boto.log.info('Writing message to SDB domain: %s' % self.output_domain.name)
+ item_name = '/'.join([message['Service-Write'], message['Bucket'], message['InputKey']])
+ self.output_domain.put_attributes(item_name, message)
+
+ # delete message from input queue
+ def delete_message(self, message):
+ boto.log.info('deleting message from %s' % self.input_queue.id)
+ self.input_queue.delete_message(message)
+
+ # to clean up any files, etc. after each iteration
+ def cleanup(self):
+ pass
+
+ def shutdown(self):
+ on_completion = self.sd.get('on_completion', 'shutdown')
+ if on_completion == 'shutdown':
+ if self.instance_id:
+ time.sleep(60)
+ c = boto.connect_ec2()
+ c.terminate_instances([self.instance_id])
+
+ def main(self, notify=False):
+ self.notify('Service: %s Starting' % self.name)
+ empty_reads = 0
+ while self.retry_count < 0 or empty_reads < self.retry_count:
+ try:
+ input_message = self.read_message()
+ if input_message:
+ empty_reads = 0
+ output_message = ServiceMessage(None, input_message.get_body())
+ input_file = self.get_file(input_message)
+ results = self.process_file(input_file, output_message)
+ self.save_results(results, input_message, output_message)
+ self.write_message(output_message)
+ self.delete_message(input_message)
+ self.cleanup()
+ else:
+ empty_reads += 1
+ time.sleep(self.loop_delay)
+ except Exception, e:
+ boto.log.exception('Service Failed')
+ empty_reads += 1
+ self.notify('Service: %s Shutting Down' % self.name)
+ self.shutdown()
+
diff --git a/api/boto/services/servicedef.py b/api/boto/services/servicedef.py
new file mode 100644
index 0000000..1cb01aa
--- /dev/null
+++ b/api/boto/services/servicedef.py
@@ -0,0 +1,91 @@
+# 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.pyami.config import Config
+from boto.services.message import ServiceMessage
+import boto
+
+class ServiceDef(Config):
+
+ def __init__(self, config_file, aws_access_key_id=None, aws_secret_access_key=None):
+ Config.__init__(self, config_file)
+ self.aws_access_key_id = aws_access_key_id
+ self.aws_secret_access_key = aws_secret_access_key
+ script = Config.get(self, 'Pyami', 'scripts')
+ if script:
+ self.name = script.split('.')[-1]
+ else:
+ self.name = None
+
+
+ def get(self, name, default=None):
+ return Config.get(self, self.name, name, default)
+
+ def has_option(self, option):
+ return Config.has_option(self, self.name, option)
+
+ def getint(self, option, default=0):
+ try:
+ val = Config.get(self, self.name, option)
+ val = int(val)
+ except:
+ val = int(default)
+ return val
+
+ def getbool(self, option, default=False):
+ try:
+ val = Config.get(self, self.name, option)
+ if val.lower() == 'true':
+ val = True
+ else:
+ val = False
+ except:
+ val = default
+ return val
+
+ def get_obj(self, name):
+ """
+ Returns the AWS object associated with a given option.
+
+ The heuristics used are a bit lame. If the option name contains
+ the word 'bucket' it is assumed to be an S3 bucket, if the name
+ contains the word 'queue' it is assumed to be an SQS queue and
+ if it contains the word 'domain' it is assumed to be a SimpleDB
+ domain. If the option name specified does not exist in the
+ config file or if the AWS object cannot be retrieved this
+ returns None.
+ """
+ val = self.get(name)
+ if not val:
+ return None
+ if name.find('queue') >= 0:
+ obj = boto.lookup('sqs', val)
+ if obj:
+ obj.set_message_class(ServiceMessage)
+ elif name.find('bucket') >= 0:
+ obj = boto.lookup('s3', val)
+ elif name.find('domain') >= 0:
+ obj = boto.lookup('sdb', val)
+ else:
+ obj = None
+ return obj
+
+
diff --git a/api/boto/services/sonofmmm.cfg b/api/boto/services/sonofmmm.cfg
new file mode 100644
index 0000000..d70d379
--- /dev/null
+++ b/api/boto/services/sonofmmm.cfg
@@ -0,0 +1,43 @@
+#
+# Your AWS Credentials
+# You only need to supply these in this file if you are not using
+# the boto tools to start your service
+#
+#[Credentials]
+#aws_access_key_id =
+#aws_secret_access_key =
+
+#
+# Fill out this section if you want emails from the service
+# when it starts and stops
+#
+#[Notification]
+#smtp_host =
+#smtp_user =
+#smtp_pass =
+#smtp_from =
+#smtp_to =
+
+[Pyami]
+scripts = boto.services.sonofmmm.SonOfMMM
+
+[SonOfMMM]
+# id of the AMI to be launched
+ami_id = ami-dc799cb5
+# number of times service will read an empty queue before exiting
+# a negative value will cause the service to run forever
+retry_count = 5
+# seconds to wait after empty queue read before reading again
+loop_delay = 10
+# average time it takes to process a transaction
+# controls invisibility timeout of messages
+processing_time = 60
+ffmpeg_args = -y -i %%s -f mov -r 29.97 -b 1200kb -mbd 2 -flags +4mv+trell -aic 2 -cmp 2 -subcmp 2 -ar 48000 -ab 19200 -s 320x240 -vcodec mpeg4 -acodec libfaac %%s
+output_mimetype = video/quicktime
+output_ext = .mov
+input_bucket =
+output_bucket =
+output_domain =
+output_queue =
+input_queue =
+
diff --git a/api/boto/services/sonofmmm.py b/api/boto/services/sonofmmm.py
new file mode 100644
index 0000000..5b94f90
--- /dev/null
+++ b/api/boto/services/sonofmmm.py
@@ -0,0 +1,80 @@
+# 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 boto
+from boto.services.service import Service
+from boto.services.message import ServiceMessage
+import os, time, mimetypes
+
+class SonOfMMM(Service):
+
+ def __init__(self, config_file=None):
+ Service.__init__(self, config_file)
+ self.log_file = '%s.log' % self.instance_id
+ self.log_path = os.path.join(self.working_dir, self.log_file)
+ boto.set_file_logger(self.name, self.log_path)
+ if self.sd.has_option('ffmpeg_args'):
+ self.command = '/usr/local/bin/ffmpeg ' + self.sd.get('ffmpeg_args')
+ else:
+ self.command = '/usr/local/bin/ffmpeg -y -i %s %s'
+ self.output_mimetype = self.sd.get('output_mimetype')
+ if self.sd.has_option('output_ext'):
+ self.output_ext = self.sd.get('output_ext')
+ else:
+ self.output_ext = mimetypes.guess_extension(self.output_mimetype)
+ self.output_bucket = self.sd.get_obj('output_bucket')
+ self.input_bucket = self.sd.get_obj('input_bucket')
+ # check to see if there are any messages queue
+ # if not, create messages for all files in input_bucket
+ m = self.input_queue.read(1)
+ if not m:
+ self.queue_files()
+
+ def queue_files(self):
+ boto.log.info('Queueing files from %s' % self.input_bucket.name)
+ for key in self.input_bucket:
+ boto.log.info('Queueing %s' % key.name)
+ m = ServiceMessage()
+ if self.output_bucket:
+ d = {'OutputBucket' : self.output_bucket.name}
+ else:
+ d = None
+ m.for_key(key, d)
+ self.input_queue.write(m)
+
+ def process_file(self, in_file_name, msg):
+ base, ext = os.path.splitext(in_file_name)
+ out_file_name = os.path.join(self.working_dir,
+ base+self.output_ext)
+ command = self.command % (in_file_name, out_file_name)
+ boto.log.info('running:\n%s' % command)
+ status = self.run(command)
+ if status == 0:
+ return [(out_file_name, self.output_mimetype)]
+ else:
+ return []
+
+ def shutdown(self):
+ if os.path.isfile(self.log_path):
+ if self.output_bucket:
+ key = self.output_bucket.new_key(self.log_file)
+ key.set_contents_from_filename(self.log_path)
+ Service.shutdown(self)
diff --git a/api/boto/services/submit.py b/api/boto/services/submit.py
new file mode 100644
index 0000000..dfa71f2
--- /dev/null
+++ b/api/boto/services/submit.py
@@ -0,0 +1,87 @@
+# 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 boto
+import time, os
+
+class Submitter:
+
+ def __init__(self, sd):
+ self.sd = sd
+ self.input_bucket = self.sd.get_obj('input_bucket')
+ self.output_bucket = self.sd.get_obj('output_bucket')
+ self.output_domain = self.sd.get_obj('output_domain')
+ self.queue = self.sd.get_obj('input_queue')
+
+ def get_key_name(self, fullpath, prefix):
+ key_name = fullpath[len(prefix):]
+ l = key_name.split(os.sep)
+ return '/'.join(l)
+
+ def write_message(self, key, metadata):
+ if self.queue:
+ m = self.queue.new_message()
+ m.for_key(key, metadata)
+ if self.output_bucket:
+ m['OutputBucket'] = self.output_bucket.name
+ self.queue.write(m)
+
+ def submit_file(self, path, metadata=None, cb=None, num_cb=0, prefix='/'):
+ if not metadata:
+ metadata = {}
+ key_name = self.get_key_name(path, prefix)
+ k = self.input_bucket.new_key(key_name)
+ k.update_metadata(metadata)
+ k.set_contents_from_filename(path, replace=False, cb=cb, num_cb=num_cb)
+ self.write_message(k, metadata)
+
+ def submit_path(self, path, tags=None, ignore_dirs=None, cb=None, num_cb=0, status=False, prefix='/'):
+ path = os.path.expanduser(path)
+ path = os.path.expandvars(path)
+ path = os.path.abspath(path)
+ total = 0
+ metadata = {}
+ if tags:
+ metadata['Tags'] = tags
+ l = []
+ for t in time.gmtime():
+ l.append(str(t))
+ metadata['Batch'] = '_'.join(l)
+ if self.output_domain:
+ self.output_domain.put_attributes(metadata['Batch'], {'type' : 'Batch'})
+ if os.path.isdir(path):
+ for root, dirs, files in os.walk(path):
+ if ignore_dirs:
+ for ignore in ignore_dirs:
+ if ignore in dirs:
+ dirs.remove(ignore)
+ for file in files:
+ fullpath = os.path.join(root, file)
+ if status:
+ print 'Submitting %s' % fullpath
+ self.submit_file(fullpath, metadata, cb, num_cb, prefix)
+ total += 1
+ elif os.path.isfile(path):
+ self.submit_file(path, metadata, cb, num_cb)
+ total += 1
+ else:
+ print 'problem with %s' % path
+ return (metadata['Batch'], total)
diff --git a/api/boto/sqs/20070501/__init__.py b/api/boto/sqs/20070501/__init__.py
new file mode 100644
index 0000000..561f9cf
--- /dev/null
+++ b/api/boto/sqs/20070501/__init__.py
@@ -0,0 +1,27 @@
+# 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 connection import SQSConnection as Connection
+from queue import Queue
+from message import Message, MHMessage
+
+__all__ = ['Connection', 'Queue', 'Message', 'MHMessage']
diff --git a/api/boto/sqs/20070501/attributes.py b/api/boto/sqs/20070501/attributes.py
new file mode 100644
index 0000000..b13370d
--- /dev/null
+++ b/api/boto/sqs/20070501/attributes.py
@@ -0,0 +1,45 @@
+# 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 SQS Attribute Name/Value set
+"""
+
+class Attributes(dict):
+
+ def __init__(self):
+ self.current_key = None
+ self.current_value = None
+
+ def startElement(self, name, attrs, connection):
+ pass
+
+ def endElement(self, name, value, connection):
+ if name == 'AttributedValue':
+ self[self.current_key] = self.current_value
+ elif name == 'Attribute':
+ self.current_key = value
+ elif name == 'Value':
+ self.current_value = value
+ else:
+ setattr(self, name, value)
+
+
diff --git a/api/boto/sqs/20070501/connection.py b/api/boto/sqs/20070501/connection.py
new file mode 100644
index 0000000..7890aa2
--- /dev/null
+++ b/api/boto/sqs/20070501/connection.py
@@ -0,0 +1,389 @@
+# 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.connection import AWSAuthConnection, AWSQueryConnection
+import xml.sax
+from boto.sqs.queue import Queue
+from boto.sqs.message import Message
+from boto.sqs.attributes import Attributes
+from boto import handler
+from boto.resultset import ResultSet
+from boto.exception import SQSError
+
+PERM_ReceiveMessage = 'ReceiveMessage'
+PERM_SendMessage = 'SendMessage'
+PERM_FullControl = 'FullControl'
+
+AllPermissions = [PERM_ReceiveMessage, PERM_SendMessage, PERM_FullControl]
+
+class SQSQueryConnection(AWSQueryConnection):
+
+ """
+ This class uses the Query API (boo!) to SQS to access some of the
+ new features which have not yet been added to the REST api (yeah!).
+ """
+
+ DefaultHost = 'queue.amazonaws.com'
+ APIVersion = '2007-05-01'
+ SignatureVersion = '1'
+ DefaultContentType = 'text/plain'
+
+ def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
+ is_secure=False, port=None, proxy=None, proxy_port=None,
+ host=DefaultHost, debug=0, https_connection_factory=None):
+ AWSQueryConnection.__init__(self, aws_access_key_id,
+ aws_secret_access_key,
+ is_secure, port, proxy, proxy_port,
+ host, debug, https_connection_factory)
+
+ def get_queue_attributes(self, queue_url, attribute='All'):
+ params = {'Attribute' : attribute}
+ response = self.make_request('GetQueueAttributes', params, queue_url)
+ body = response.read()
+ if response.status == 200:
+ attrs = Attributes()
+ h = handler.XmlHandler(attrs, self)
+ xml.sax.parseString(body, h)
+ return attrs
+ else:
+ raise SQSError(response.status, response.reason, body)
+
+ def set_queue_attribute(self, queue_url, attribute, value):
+ params = {'Attribute' : attribute, 'Value' : value}
+ response = self.make_request('SetQueueAttributes', params, queue_url)
+ body = response.read()
+ if response.status == 200:
+ rs = ResultSet()
+ h = handler.XmlHandler(rs, self)
+ xml.sax.parseString(body, h)
+ return rs.status
+ else:
+ raise SQSError(response.status, response.reason, body)
+
+ def change_message_visibility(self, queue_url, message_id, vtimeout):
+ params = {'MessageId' : message_id,
+ 'VisibilityTimeout' : vtimeout}
+ response = self.make_request('ChangeMessageVisibility', params,
+ queue_url)
+ body = response.read()
+ if response.status == 200:
+ rs = ResultSet()
+ h = handler.XmlHandler(rs, self)
+ xml.sax.parseString(body, h)
+ return rs.status
+ else:
+ raise SQSError(response.status, response.reason, body)
+
+ def add_grant(self, queue_url, permission, email_address=None, user_id=None):
+ params = {'Permission' : permission}
+ if user_id:
+ params['Grantee.ID'] = user_id
+ if email_address:
+ params['Grantee.EmailAddress'] = email_address
+ response = self.make_request('AddGrant', params, queue_url)
+ body = response.read()
+ if response.status == 200:
+ rs = ResultSet()
+ h = handler.XmlHandler(rs, self)
+ xml.sax.parseString(body, h)
+ return rs.status
+ else:
+ raise SQSError(response.status, response.reason, body)
+
+ def remove_grant(self, queue_url, permission, email_address=None, user_id=None):
+ params = {'Permission' : permission}
+ if user_id:
+ params['Grantee.ID'] = user_id
+ if email_address:
+ params['Grantee.EmailAddress'] = email_address
+ response = self.make_request('RemoveGrant', params, queue_url)
+ body = response.read()
+ if response.status == 200:
+ rs = ResultSet()
+ h = handler.XmlHandler(rs, self)
+ xml.sax.parseString(body, h)
+ return rs.status
+ else:
+ raise SQSError(response.status, response.reason, body)
+
+ def list_grants(self, queue_url, permission=None, email_address=None, user_id=None):
+ params = {}
+ if user_id:
+ params['Grantee.ID'] = user_id
+ if email_address:
+ params['Grantee.EmailAddress'] = email_address
+ if permission:
+ params['Permission'] = permission
+ response = self.make_request('ListGrants', params, queue_url)
+ body = response.read()
+ if response.status == 200:
+ return body
+ else:
+ raise SQSError(response.status, response.reason, body)
+
+ def receive_message(self, queue_url, number_messages=1,
+ visibility_timeout=None, message_class=Message):
+ """
+ This provides the same functionality as the read and get_messages methods
+ of the queue object. The only reason this is included here is that there is
+ currently a bug in SQS that makes it impossible to read a message from a queue
+ owned by someone else (even if you have been granted appropriate permissions)
+ via the REST interface. As it turns out, I need to be able to do this so until
+ the REST interface gets fixed this is the workaround.
+ """
+ params = {'NumberOfMessages' : number_messages}
+ if visibility_timeout:
+ params['VisibilityTimeout'] = visibility_timeout
+ response = self.make_request('ReceiveMessage', params, queue_url)
+ body = response.read()
+ if response.status == 200:
+ rs = ResultSet([('Message', message_class)])
+ h = handler.XmlHandler(rs, queue_url)
+ xml.sax.parseString(body, h)
+ if len(rs) == 1:
+ return rs[0]
+ else:
+ return rs
+ else:
+ raise SQSError(response.status, response.reason, body)
+
+ def delete_message(self, queue_url, message_id):
+ """
+ Because we have to use the Query interface to read messages from queues that
+ we don't own, we also have to provide a way to delete those messages via Query.
+ """
+ params = {'MessageId' : message_id}
+ response = self.make_request('DeleteMessage', params, queue_url)
+ body = response.read()
+ if response.status == 200:
+ rs = ResultSet()
+ h = handler.XmlHandler(rs, self)
+ xml.sax.parseString(body, h)
+ return rs.status
+ else:
+ raise SQSError(response.status, response.reason, body)
+
+class SQSConnection(AWSAuthConnection):
+
+ DefaultHost = 'queue.amazonaws.com'
+ APIVersion = '2007-05-01'
+ DefaultContentType = 'text/plain'
+
+ def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
+ is_secure=False, port=None, proxy=None, proxy_port=None,
+ host=DefaultHost, debug=0, https_connection_factory=None):
+ AWSAuthConnection.__init__(self, host,
+ aws_access_key_id, aws_secret_access_key,
+ is_secure, port, proxy, proxy_port, debug,
+ https_connection_factory)
+ self.query_conn = None
+
+ def make_request(self, method, path, headers=None, data=''):
+ # add auth header
+ if headers == None:
+ headers = {}
+
+ if not headers.has_key('AWS-Version'):
+ headers['AWS-Version'] = self.APIVersion
+
+ if not headers.has_key('Content-Type'):
+ headers['Content-Type'] = self.DefaultContentType
+
+ return AWSAuthConnection.make_request(self, method, path,
+ headers, data)
+
+ def get_query_connection(self):
+ if not self.query_conn:
+ self.query_conn = SQSQueryConnection(self.aws_access_key_id,
+ self.aws_secret_access_key,
+ self.is_secure, self.port,
+ self.proxy, self.proxy_port,
+ self.server, self.debug,
+ self.https_connection_factory)
+ return self.query_conn
+
+ def get_all_queues(self, prefix=''):
+ if prefix:
+ path = '/?QueueNamePrefix=%s' % prefix
+ else:
+ path = '/'
+ response = self.make_request('GET', path)
+ body = response.read()
+ if response.status >= 300:
+ raise SQSError(response.status, response.reason, body)
+ rs = ResultSet([('QueueUrl', Queue)])
+ h = handler.XmlHandler(rs, self)
+ xml.sax.parseString(body, h)
+ return rs
+
+ def get_queue(self, queue_name):
+ i = 0
+ rs = self.get_all_queues(queue_name)
+ for q in rs:
+ i += 1
+ if i != 1:
+ return None
+ else:
+ return q
+
+ def get_queue_attributes(self, queue_url, attribute='All'):
+ """
+ Performs a GetQueueAttributes request and returns an Attributes
+ instance (subclass of a Dictionary) holding the requested
+ attribute name/value pairs.
+ Inputs:
+ queue_url - the URL of the desired SQS queue
+ attribute - All|ApproximateNumberOfMessages|VisibilityTimeout
+ Default value is "All"
+ Returns:
+ An Attribute object which is a mapping type holding the
+ requested name/value pairs
+ """
+ qc = self.get_query_connection()
+ return qc.get_queue_attributes(queue_url, attribute)
+
+ def set_queue_attribute(self, queue_url, attribute, value):
+ """
+ Performs a SetQueueAttributes request.
+ Inputs:
+ queue_url - The URL of the desired SQS queue
+ attribute - The name of the attribute you want to set. The
+ only valid value at this time is: VisibilityTimeout
+ value - The new value for the attribute.
+ For VisibilityTimeout the value must be an
+ integer number of seconds from 0 to 86400.
+ Returns:
+ Boolean True if successful, otherwise False.
+ """
+ qc = self.get_query_connection()
+ return qc.set_queue_attribute(queue_url, attribute, value)
+
+ def change_message_visibility(self, queue_url, message_id, vtimeout):
+ """
+ Change the VisibilityTimeout for an individual message.
+ Inputs:
+ queue_url - The URL of the desired SQS queue
+ message_id - The ID of the message whose timeout will be changed
+ vtimeout - The new VisibilityTimeout value, in seconds
+ Returns:
+ Boolean True if successful, otherwise False
+ Note: This functionality is also available as a method of the
+ Message object.
+ """
+ qc = self.get_query_connection()
+ return qc.change_message_visibility(queue_url, message_id, vtimeout)
+
+ def add_grant(self, queue_url, permission, email_address=None, user_id=None):
+ """
+ Add a grant to a queue.
+ Inputs:
+ queue_url - The URL of the desired SQS queue
+ permission - The permission being granted. One of "ReceiveMessage", "SendMessage" or "FullControl"
+ email_address - the email address of the grantee. If email_address is supplied, user_id should be None
+ user_id - The ID of the grantee. If user_id is supplied, email_address should be None
+ Returns:
+ Boolean True if successful, otherwise False
+ """
+ qc = self.get_query_connection()
+ return qc.add_grant(queue_url, permission, email_address, user_id)
+
+ def remove_grant(self, queue_url, permission, email_address=None, user_id=None):
+ """
+ Remove a grant from a queue.
+ Inputs:
+ queue_url - The URL of the desired SQS queue
+ permission - The permission being removed. One of "ReceiveMessage", "SendMessage" or "FullControl"
+ email_address - the email address of the grantee. If email_address is supplied, user_id should be None
+ user_id - The ID of the grantee. If user_id is supplied, email_address should be None
+ Returns:
+ Boolean True if successful, otherwise False
+ """
+ qc = self.get_query_connection()
+ return qc.remove_grant(queue_url, permission, email_address, user_id)
+
+ def list_grants(self, queue_url, permission=None, email_address=None, user_id=None):
+ """
+ List the grants to a queue.
+ Inputs:
+ queue_url - The URL of the desired SQS queue
+ permission - The permission granted. One of "ReceiveMessage", "SendMessage" or "FullControl".
+ If supplied, only grants that allow this permission will be returned.
+ email_address - the email address of the grantee. If supplied, only grants related to this email
+ address will be returned
+ user_id - The ID of the grantee. If supplied, only grants related to his user_id will be returned.
+ Returns:
+ A string containing the XML Response elements describing the grants.
+ """
+ qc = self.get_query_connection()
+ return qc.list_grants(queue_url, permission, email_address, user_id)
+
+ def create_queue(self, queue_name, visibility_timeout=None):
+ """
+ Create a new queue.
+ Inputs:
+ queue_name - The name of the new queue
+ visibility_timeout - (Optional) The default visibility
+ timeout for the new queue.
+ Returns:
+ A new Queue object representing the newly created queue.
+ """
+ path = '/?QueueName=%s' % queue_name
+ if visibility_timeout:
+ path = path + '&DefaultVisibilityTimeout=%d' % visibility_timeout
+ response = self.make_request('POST', path)
+ body = response.read()
+ if response.status >= 300:
+ raise SQSError(response.status, response.reason, body)
+ q = Queue(self)
+ h = handler.XmlHandler(q, self)
+ xml.sax.parseString(body, h)
+ return q
+
+ def delete_queue(self, queue, force_deletion=False):
+ """
+ Delete an SQS Queue.
+ Inputs:
+ queue - a Queue object representing the SQS queue to be deleted.
+ force_deletion - (Optional) Normally, SQS will not delete a
+ queue that contains messages. However, if
+ the force_deletion argument is True, the
+ queue will be deleted regardless of whether
+ there are messages in the queue or not.
+ USE WITH CAUTION. This will delete all
+ messages in the queue as well.
+ Returns:
+ An empty ResultSet object. Not sure why, actually. It
+ should probably return a Boolean indicating success or
+ failure.
+ """
+ method = 'DELETE'
+ path = queue.id
+ if force_deletion:
+ path = path + '?ForceDeletion=true'
+ response = self.make_request(method, path)
+ body = response.read()
+ if response.status >= 300:
+ raise SQSError(response.status, response.reason, body)
+ rs = ResultSet()
+ h = handler.XmlHandler(rs, self)
+ xml.sax.parseString(body, h)
+ return rs
+
diff --git a/api/boto/sqs/20070501/message.py b/api/boto/sqs/20070501/message.py
new file mode 100644
index 0000000..0c45b31
--- /dev/null
+++ b/api/boto/sqs/20070501/message.py
@@ -0,0 +1,180 @@
+# 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 SQS Message
+"""
+
+import base64
+import StringIO
+
+class RawMessage:
+ """
+ Base class for SQS messages. RawMessage does not encode the message
+ in any way. Whatever you store in the body of the message is what
+ will be written to SQS and whatever is returned from SQS is stored
+ directly into the body of the message.
+ """
+
+ def __init__(self, queue=None, body=''):
+ self.queue = queue
+ self._body = ''
+ self.set_body(body)
+ self.id = None
+
+ def __len__(self):
+ return len(self._body)
+
+ def startElement(self, name, attrs, connection):
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'MessageBody':
+ self.set_body(value)
+ elif name == 'MessageId':
+ self.id = value
+ else:
+ setattr(self, name, value)
+
+ def set_body(self, body):
+ """
+ Set the body of the message. You should always call this method
+ rather than setting the attribute directly.
+ """
+ self._body = body
+
+ def get_body(self):
+ """
+ Retrieve the body of the message.
+ """
+ return self._body
+
+ def get_body_encoded(self):
+ """
+ This method is really a semi-private method used by the Queue.write
+ method when writing the contents of the message to SQS. The
+ RawMessage class does not encode the message in any way so this
+ just calls get_body(). You probably shouldn't need to call this
+ method in the normal course of events.
+ """
+ return self.get_body()
+
+ def change_visibility(self, vtimeout):
+ """
+ Convenience function to allow you to directly change the
+ invisibility timeout for an individual message that has been
+ read from an SQS queue. This won't affect the default visibility
+ timeout of the queue.
+ """
+ return self.queue.connection.change_message_visibility(self.queue.id,
+ self.id,
+ vtimeout)
+class Message(RawMessage):
+ """
+ The default Message class used for SQS queues. This class automatically
+ encodes/decodes the message body using Base64 encoding to avoid any
+ illegal characters in the message body. See:
+
+ http://developer.amazonwebservices.com/connect/thread.jspa?messageID=49680%EC%88%90
+
+ for details on why this is a good idea. The encode/decode is meant to
+ be transparent to the end-user.
+ """
+
+ def endElement(self, name, value, connection):
+ if name == 'MessageBody':
+ # Decode the message body returned from SQS using base64
+ self.set_body(base64.b64decode(value))
+ elif name == 'MessageId':
+ self.id = value
+ else:
+ setattr(self, name, value)
+
+ def get_body_encoded(self):
+ """
+ Because the Message class encodes the message body in base64
+ this private method used by queue.write needs to perform the
+ encoding.
+ """
+ return base64.b64encode(self.get_body())
+
+class MHMessage(Message):
+ """
+ The MHMessage class provides a message that provides RFC821-like
+ headers like this:
+
+ HeaderName: HeaderValue
+
+ The encoding/decoding of this is handled automatically and after
+ the message body has been read, the message instance can be treated
+ like a mapping object, i.e. m['HeaderName'] would return 'HeaderValue'.
+ """
+
+ def __init__(self, queue=None, body='', xml_attrs=None):
+ self._dict = {}
+ Message.__init__(self, queue, body)
+
+ def set_body(self, body):
+ fp = StringIO.StringIO(body)
+ line = fp.readline()
+ while line:
+ delim = line.find(':')
+ key = line[0:delim]
+ value = line[delim+1:].strip()
+ self._dict[key.strip()] = value.strip()
+ line = fp.readline()
+
+ def get_body(self):
+ s = ''
+ for key,value in self._dict.items():
+ s = s + '%s: %s\n' % (key, value)
+ return s
+
+ def __len__(self):
+ return len(self.get_body())
+
+ def __getitem__(self, key):
+ if self._dict.has_key(key):
+ return self._dict[key]
+ else:
+ raise KeyError(key)
+
+ def __setitem__(self, key, value):
+ self._dict[key] = value
+
+ def keys(self):
+ return self._dict.keys()
+
+ def values(self):
+ return self._dict.values()
+
+ def items(self):
+ return self._dict.items()
+
+ def has_key(self, key):
+ return self._dict.has_key(key)
+
+ def update(self, d):
+ return self._dict.update(d)
+
+ def get(self, key, default=None):
+ return self._dict.get(key, default)
+
diff --git a/api/boto/sqs/20070501/queue.py b/api/boto/sqs/20070501/queue.py
new file mode 100644
index 0000000..64289ef
--- /dev/null
+++ b/api/boto/sqs/20070501/queue.py
@@ -0,0 +1,343 @@
+# 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 SQS Queue
+"""
+
+import xml.sax
+import urlparse
+from boto.exception import SQSError
+from boto.handler import XmlHandler
+from boto.sqs.message import Message
+from boto.resultset import ResultSet
+
+class Queue:
+
+ def __init__(self, connection=None, url=None, message_class=Message):
+ self.connection = connection
+ self.url = url
+ self.message_class = message_class
+ self.visibility_timeout = None
+
+ def startElement(self, name, attrs, connection):
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'QueueUrl':
+ self.url = value
+ if value:
+ self.id = urlparse.urlparse(value)[2]
+ elif name == 'VisibilityTimeout':
+ self.visibility_timeout = int(value)
+ else:
+ setattr(self, name, value)
+
+ def set_message_class(self, message_class):
+ """
+ Set the message class that should be used when instantiating messages read
+ from the queue. By default, the class boto.sqs.message.Message is used but
+ this can be overriden with any class that behaves like a message.
+ Inputs:
+ message_class - The new message class
+ Returns:
+ Nothing
+ """
+ self.message_class = message_class
+
+ def get_attributes(self, attributes='All'):
+ """
+ Retrieves attributes about this queue object and returns
+ them in an Attribute instance (subclass of a Dictionary).
+ Inputs:
+ attributes - A string containing
+ All|ApproximateNumberOfMessages|VisibilityTimeout
+ Default value is "All"
+ Returns:
+ An Attribute object which is a mapping type holding the
+ requested name/value pairs
+ """
+ return self.connection.get_queue_attributes(self.id, attributes)
+
+ def set_attribute(self, attribute, value):
+ """
+ Set a new value for an attribute of the Queue.
+ Inputs:
+ attribute - The name of the attribute you want to set. The
+ only valid value at this time is: VisibilityTimeout
+ value - The new value for the attribute.
+ For VisibilityTimeout the value must be an
+ integer number of seconds from 0 to 86400.
+ Returns:
+ Boolean True if successful, otherwise False.
+ """
+ return self.connection.set_queue_attribute(self.id, attribute, value)
+
+ def add_grant(self, permission, email_address=None, user_id=None):
+ """
+ Add a grant to this queue.
+ Inputs:
+ permission - The permission being granted. One of "ReceiveMessage", "SendMessage" or "FullControl"
+ email_address - the email address of the grantee. If email_address is supplied, user_id should be None
+ user_id - The ID of the grantee. If user_id is supplied, email_address should be None
+ Returns:
+ Boolean True if successful, otherwise False
+ """
+ return self.connection.add_grant(self.id, permission, email_address, user_id)
+
+ def remove_grant(self, permission, email_address=None, user_id=None):
+ """
+ Remove a grant from this queue.
+ Inputs:
+ permission - The permission being removed. One of "ReceiveMessage", "SendMessage" or "FullControl"
+ email_address - the email address of the grantee. If email_address is supplied, user_id should be None
+ user_id - The ID of the grantee. If user_id is supplied, email_address should be None
+ Returns:
+ Boolean True if successful, otherwise False
+ """
+ return self.connection.remove_grant(self.id, permission, email_address, user_id)
+
+ def list_grants(self, permission=None, email_address=None, user_id=None):
+ """
+ List the grants to this queue.
+ Inputs:
+ permission - The permission granted. One of "ReceiveMessage", "SendMessage" or "FullControl".
+ If supplied, only grants that allow this permission will be returned.
+ email_address - the email address of the grantee. If supplied, only grants related to this email
+ address will be returned
+ user_id - The ID of the grantee. If supplied, only grants related to his user_id will be returned.
+ Returns:
+ A string containing the XML Response elements describing the grants.
+ """
+ return self.connection.list_grants(self.id, permission, email_address, user_id)
+
+ def get_timeout(self):
+ """
+ Get the visibility timeout for the queue.
+ Inputs:
+ None
+ Returns:
+ The number of seconds as an integer.
+ """
+ a = self.get_attributes('VisibilityTimeout')
+ return int(a['VisibilityTimeout'])
+
+ def set_timeout(self, visibility_timeout):
+ """
+ Set the visibility timeout for the queue.
+ Inputs:
+ visibility_timeout - The desired timeout in seconds
+ Returns:
+ Nothing
+ """
+ retval = self.set_attribute('VisibilityTimeout', visibility_timeout)
+ if retval:
+ self.visibility_timeout = visibility_timeout
+ return retval
+
+ def read(self, visibility_timeout=None):
+ """
+ Read a single message from the queue.
+ Inputs:
+ visibility_timeout - The timeout for this message in seconds
+ Returns:
+ A single message or None if queue is empty
+ """
+ rs = self.get_messages(1, visibility_timeout)
+ if len(rs) == 1:
+ return rs[0]
+ else:
+ return None
+
+ def write(self, message):
+ """
+ Add a single message to the queue.
+ Inputs:
+ message - The message to be written to the queue
+ Returns:
+ None
+ """
+ path = '%s/back' % self.id
+ message.queue = self
+ response = self.connection.make_request('PUT', path, None,
+ message.get_body_encoded())
+ body = response.read()
+ if response.status >= 300:
+ raise SQSError(response.status, response.reason, body)
+ handler = XmlHandler(message, self.connection)
+ xml.sax.parseString(body, handler)
+ return None
+
+ def new_message(self, body=''):
+ return self.message_class(self, body)
+
+ # get a variable number of messages, returns a list of messages
+ def get_messages(self, num_messages=1, visibility_timeout=None):
+ path = '%s/front?NumberOfMessages=%d' % (self.id, num_messages)
+ if visibility_timeout:
+ path = '%s&VisibilityTimeout=%d' % (path, visibility_timeout)
+ response = self.connection.make_request('GET', path)
+ body = response.read()
+ if response.status >= 300:
+ raise SQSError(response.status, response.reason, body)
+ rs = ResultSet([('Message', self.message_class)])
+ h = XmlHandler(rs, self)
+ xml.sax.parseString(body, h)
+ return rs
+
+ def delete_message(self, message):
+ path = '%s/%s' % (self.id, message.id)
+ response = self.connection.make_request('DELETE', path)
+ body = response.read()
+ if response.status >= 300:
+ raise SQSError(response.status, response.reason, body)
+ rs = ResultSet()
+ h = XmlHandler(rs, self.connection)
+ xml.sax.parseString(body, h)
+ return rs
+
+ def clear(self, page_size=100, vtimeout=10):
+ """Utility function to remove all messages from a queue"""
+ n = 0
+ l = self.get_messages(page_size, vtimeout)
+ while l:
+ for m in l:
+ self.delete_message(m)
+ n += 1
+ l = self.get_messages(page_size, vtimeout)
+ return n
+
+ def count(self, page_size=100, vtimeout=10):
+ """
+ Utility function to count the number of messages in a queue.
+ Note: This function now calls GetQueueAttributes to obtain
+ an 'approximate' count of the number of messages in a queue.
+ """
+ a = self.get_attributes('ApproximateNumberOfMessages')
+ return int(a['ApproximateNumberOfMessages'])
+
+ def count_slow(self, page_size=100, vtimeout=10):
+ """
+ Deprecated. This is the old 'count' method that actually counts
+ the messages by reading them all. This gives an accurate count but
+ is very slow for queues with non-trivial number of messasges.
+ Instead, use get_attribute('ApproximateNumberOfMessages') to take
+ advantage of the new SQS capability. This is retained only for
+ the unit tests.
+ """
+ n = 0
+ l = self.get_messages(page_size, vtimeout)
+ while l:
+ for m in l:
+ n += 1
+ l = self.get_messages(page_size, vtimeout)
+ return n
+
+ def dump(self, file_name, page_size=100, vtimeout=10, sep='\n'):
+ """Utility function to dump the messages in a queue to a file"""
+ fp = open(file_name, 'wb')
+ n = 0
+ l = self.get_messages(page_size, vtimeout)
+ while l:
+ for m in l:
+ fp.write(m.get_body())
+ if sep:
+ fp.write(sep)
+ n += 1
+ l = self.get_messages(page_size, vtimeout)
+ fp.close()
+ return n
+
+ def save(self, file_name, sep='\n'):
+ """
+ Read all messages from the queue and persist them to local file.
+ Messages are written to the file and the 'sep' string is written
+ in between messages. Messages are deleted from the queue after
+ being written to the file.
+ Returns the number of messages saved.
+ """
+ fp = open(file_name, 'wb')
+ n = 0
+ m = self.read()
+ while m:
+ n += 1
+ fp.write(m.get_body())
+ if sep:
+ fp.write(sep)
+ self.delete_message(m)
+ m = self.read()
+ fp.close()
+ return n
+
+ def save_to_s3(self, bucket):
+ """
+ Read all messages from the queue and persist them to S3.
+ Messages are stored in the S3 bucket using a naming scheme of:
+ /
+ Messages are deleted from the queue after being saved to S3.
+ Returns the number of messages saved.
+ """
+ n = 0
+ m = self.read()
+ while m:
+ n += 1
+ key = bucket.new_key('%s/%s' % (self.id, m.id))
+ key.set_contents_from_string(m.get_body())
+ self.delete_message(m)
+ m = self.read()
+ return n
+
+ def load_from_s3(self, bucket, prefix=None):
+ """
+ Load messages previously saved to S3.
+ """
+ n = 0
+ if prefix:
+ prefix = '%s/' % prefix
+ else:
+ prefix = '%s/' % self.id
+ rs = bucket.list(prefix=prefix)
+ for key in rs:
+ n += 1
+ m = self.new_message(key.get_contents_as_string())
+ self.write(m)
+ return n
+
+ def load(self, file_name, sep='\n'):
+ """Utility function to load messages from a file to a queue"""
+ fp = open(file_name, 'rb')
+ n = 0
+ body = ''
+ l = fp.readline()
+ while l:
+ if l == sep:
+ m = Message(self, body)
+ self.write(m)
+ n += 1
+ print 'writing message %d' % n
+ body = ''
+ else:
+ body = body + l
+ l = fp.readline()
+ fp.close()
+ return n
+
+
diff --git a/api/boto/sqs/20070501/readme.txt b/api/boto/sqs/20070501/readme.txt
new file mode 100644
index 0000000..7132579
--- /dev/null
+++ b/api/boto/sqs/20070501/readme.txt
@@ -0,0 +1,6 @@
+The main SQS implementation now uses the 2008-01-01 API verson. To use the older API version
+(2007-05-01) you need to edit your /etc/boto.cfg or ~/.boto file to add the following line:
+
+boto.sqs_extend = 20070501
+
+This will allow the code in the boto.sqs.20070501 module to override the code in boto.sqs.
diff --git a/api/boto/sqs/__init__.py b/api/boto/sqs/__init__.py
new file mode 100644
index 0000000..0b3924c
--- /dev/null
+++ b/api/boto/sqs/__init__.py
@@ -0,0 +1,46 @@
+# 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 boto
+
+boto.check_extensions(__name__, __path__)
+
+from queue import Queue
+from message import Message, MHMessage, EncodedMHMessage
+from regioninfo import SQSRegionInfo
+
+def regions():
+ """
+ Get all available regions for the SQS service.
+
+ :rtype: list
+ :return: A list of :class:`boto.ec2.regioninfo.RegionInfo`
+ """
+ return [SQSRegionInfo(name='us-east-1', endpoint='queue.amazonaws.com'),
+ SQSRegionInfo(name='eu-west-1', endpoint='eu-west-1.queue.amazonaws.com'),
+ SQSRegionInfo(name='us-west-1', endpoint='us-west-1.queue.amazonaws.com')]
+
+def connect_to_region(region_name):
+ for region in regions():
+ if region.name == region_name:
+ return region.connect()
+ return None
diff --git a/api/boto/sqs/attributes.py b/api/boto/sqs/attributes.py
new file mode 100644
index 0000000..26c7204
--- /dev/null
+++ b/api/boto/sqs/attributes.py
@@ -0,0 +1,46 @@
+# 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 SQS Attribute Name/Value set
+"""
+
+class Attributes(dict):
+
+ def __init__(self, parent):
+ self.parent = parent
+ self.current_key = None
+ self.current_value = None
+
+ def startElement(self, name, attrs, connection):
+ pass
+
+ def endElement(self, name, value, connection):
+ if name == 'Attribute':
+ self[self.current_key] = self.current_value
+ elif name == 'Name':
+ self.current_key = value
+ elif name == 'Value':
+ self.current_value = value
+ else:
+ setattr(self, name, value)
+
+
diff --git a/api/boto/sqs/connection.py b/api/boto/sqs/connection.py
new file mode 100644
index 0000000..fd13d2a
--- /dev/null
+++ b/api/boto/sqs/connection.py
@@ -0,0 +1,257 @@
+# 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.connection import AWSQueryConnection
+import xml.sax
+from boto.sqs.regioninfo import SQSRegionInfo
+from boto.sqs.queue import Queue
+from boto.sqs.message import Message
+from boto.sqs.attributes import Attributes
+from boto import handler
+from boto.resultset import ResultSet
+from boto.exception import SQSError
+
+class SQSConnection(AWSQueryConnection):
+ """
+ A Connection to the SQS Service.
+ """
+ DefaultRegionName = 'us-east-1'
+ DefaultRegionEndpoint = 'queue.amazonaws.com'
+ APIVersion = '2009-02-01'
+ SignatureVersion = '2'
+ DefaultContentType = 'text/plain'
+ ResponseError = SQSError
+
+ 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 = SQSRegionInfo(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 create_queue(self, queue_name, visibility_timeout=None):
+ """
+ Create an SQS Queue.
+
+ :type queue_name: str or unicode
+ :param queue_name: The name of the new queue. Names are scoped to an account and need to
+ be unique within that account. Calling this method on an existing
+ queue name will not return an error from SQS unless the value for
+ visibility_timeout is different than the value of the existing queue
+ of that name. This is still an expensive operation, though, and not
+ the preferred way to check for the existence of a queue. See the
+ :func:`boto.sqs.connection.SQSConnection.lookup` method.
+
+ :type visibility_timeout: int
+ :param visibility_timeout: The default visibility timeout for all messages written in the
+ queue. This can be overridden on a per-message.
+
+ :rtype: :class:`boto.sqs.queue.Queue`
+ :return: The newly created queue.
+
+ """
+ params = {'QueueName': queue_name}
+ if visibility_timeout:
+ params['DefaultVisibilityTimeout'] = '%d' % (visibility_timeout,)
+ return self.get_object('CreateQueue', params, Queue)
+
+ def delete_queue(self, queue, force_deletion=False):
+ """
+ Delete an SQS Queue.
+
+ :type queue: A Queue object
+ :param queue: The SQS queue to be deleted
+
+ :type force_deletion: Boolean
+ :param force_deletion: Normally, SQS will not delete a queue that contains messages.
+ However, if the force_deletion argument is True, the
+ queue will be deleted regardless of whether there are messages in
+ the queue or not. USE WITH CAUTION. This will delete all
+ messages in the queue as well.
+
+ :rtype: bool
+ :return: True if the command succeeded, False otherwise
+ """
+ return self.get_status('DeleteQueue', None, queue.id)
+
+ def get_queue_attributes(self, queue, attribute='All'):
+ """
+ Gets one or all attributes of a Queue
+
+ :type queue: A Queue object
+ :param queue: The SQS queue to be deleted
+
+ :type attribute: str
+ :type attribute: The specific attribute requested. If not supplied, the default
+ is to return all attributes. Valid attributes are:
+ ApproximateNumberOfMessages,
+ ApproximateNumberOfMessagesNotVisible,
+ VisibilityTimeout,
+ CreatedTimestamp,
+ LastModifiedTimestamp,
+ Policy
+
+ :rtype: :class:`boto.sqs.attributes.Attributes`
+ :return: An Attributes object containing request value(s).
+ """
+ params = {'AttributeName' : attribute}
+ return self.get_object('GetQueueAttributes', params, Attributes, queue.id)
+
+ def set_queue_attribute(self, queue, attribute, value):
+ params = {'Attribute.Name' : attribute, 'Attribute.Value' : value}
+ return self.get_status('SetQueueAttributes', params, queue.id)
+
+ def receive_message(self, queue, number_messages=1, visibility_timeout=None,
+ attributes=None):
+ """
+ Read messages from an SQS Queue.
+
+ :type queue: A Queue object
+ :param queue: The Queue from which messages are read.
+
+ :type number_messages: int
+ :param number_messages: The maximum number of messages to read (default=1)
+
+ :type visibility_timeout: int
+ :param visibility_timeout: The number of seconds the message should remain invisible
+ to other queue readers (default=None which uses the Queues default)
+
+ :type attributes: list of strings
+ :param attributes: A list of additional attributes that will be returned
+ with the response. Valid values:
+ All
+ SenderId
+ SentTimestamp
+ ApproximateReceiveCount
+ ApproximateFirstReceiveTimestamp
+
+ """
+ params = {'MaxNumberOfMessages' : number_messages}
+ if visibility_timeout:
+ params['VisibilityTimeout'] = visibility_timeout
+ if attributes:
+ self.build_list_params(self, params, attributes, 'AttributeName')
+ return self.get_list('ReceiveMessage', params, [('Message', queue.message_class)],
+ queue.id, queue)
+
+ def delete_message(self, queue, message):
+ params = {'ReceiptHandle' : message.receipt_handle}
+ return self.get_status('DeleteMessage', params, queue.id)
+
+ def send_message(self, queue, message_content):
+ params = {'MessageBody' : message_content}
+ return self.get_object('SendMessage', params, Message, queue.id, verb='POST')
+
+ def change_message_visibility(self, queue, receipt_handle, visibility_timeout):
+ """
+ Extends the read lock timeout for the specified message from the specified queue
+ to the specified value.
+
+ :type queue: A :class:`boto.sqs.queue.Queue` object
+ :param queue: The Queue from which messages are read.
+
+ :type receipt_handle: str
+ :param queue: The receipt handle associated with the message whose
+ visibility timeout will be changed.
+
+ :type visibility_timeout: int
+ :param visibility_timeout: The new value of the message's visibility timeout
+ in seconds.
+ """
+ params = {'ReceiptHandle' : receipt_handle,
+ 'VisibilityTimeout' : visibility_timeout}
+ return self.get_status('ChangeMessageVisibility', params, queue.id)
+
+ def get_all_queues(self, prefix=''):
+ params = {}
+ if prefix:
+ params['QueueNamePrefix'] = prefix
+ return self.get_list('ListQueues', params, [('QueueUrl', Queue)])
+
+ def get_queue(self, queue_name):
+ rs = self.get_all_queues(queue_name)
+ for q in rs:
+ if q.url.endswith(queue_name):
+ return q
+ return None
+
+ lookup = get_queue
+
+ #
+ # Permissions methods
+ #
+
+ def add_permission(self, queue, label, aws_account_id, action_name):
+ """
+ Add a permission to a queue.
+
+ :type queue: :class:`boto.sqs.queue.Queue`
+ :param queue: The queue object
+
+ :type label: str or unicode
+ :param label: A unique identification of the permission you are setting.
+ Maximum of 80 characters ``[0-9a-zA-Z_-]``
+ Example, AliceSendMessage
+
+ :type aws_account_id: str or unicode
+ :param principal_id: The AWS account number of the principal who will be given
+ permission. The principal must have an AWS account, but
+ does not need to be signed up for Amazon SQS. For information
+ about locating the AWS account identification.
+
+ :type action_name: str or unicode
+ :param action_name: The action. Valid choices are:
+ \*|SendMessage|ReceiveMessage|DeleteMessage|
+ ChangeMessageVisibility|GetQueueAttributes
+
+ :rtype: bool
+ :return: True if successful, False otherwise.
+
+ """
+ params = {'Label': label,
+ 'AWSAccountId' : aws_account_id,
+ 'ActionName' : action_name}
+ return self.get_status('AddPermission', params, queue.id)
+
+ def remove_permission(self, queue, label):
+ """
+ Remove a permission from a queue.
+
+ :type queue: :class:`boto.sqs.queue.Queue`
+ :param queue: The queue object
+
+ :type label: str or unicode
+ :param label: The unique label associated with the permission being removed.
+
+ :rtype: bool
+ :return: True if successful, False otherwise.
+ """
+ params = {'Label': label}
+ return self.get_status('RemovePermission', params, queue.id)
+
+
+
+
+
diff --git a/api/boto/sqs/jsonmessage.py b/api/boto/sqs/jsonmessage.py
new file mode 100644
index 0000000..ab05a60
--- /dev/null
+++ b/api/boto/sqs/jsonmessage.py
@@ -0,0 +1,42 @@
+# 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.
+
+from boto.sqs.message import MHMessage
+from boto.exception import SQSDecodeError
+import base64
+import simplejson
+
+class JSONMessage(MHMessage):
+ """
+ Acts like a dictionary but encodes it's data as a Base64 encoded JSON payload.
+ """
+
+ def decode(self, value):
+ try:
+ value = base64.b64decode(value)
+ value = simplejson.loads(value)
+ except:
+ raise SQSDecodeError('Unable to decode message', self)
+ return value
+
+ def encode(self, value):
+ value = simplejson.dumps(value)
+ return base64.b64encode(value)
diff --git a/api/boto/sqs/message.py b/api/boto/sqs/message.py
new file mode 100644
index 0000000..da1ba68
--- /dev/null
+++ b/api/boto/sqs/message.py
@@ -0,0 +1,251 @@
+# 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.
+
+"""
+SQS Message
+
+A Message represents the data stored in an SQS queue. The rules for what is allowed within an SQS
+Message are here:
+
+ http://docs.amazonwebservices.com/AWSSimpleQueueService/2008-01-01/SQSDeveloperGuide/Query_QuerySendMessage.html
+
+So, at it's simplest level a Message just needs to allow a developer to store bytes in it and get the bytes
+back out. However, to allow messages to have richer semantics, the Message class must support the
+following interfaces:
+
+The constructor for the Message class must accept a keyword parameter "queue" which is an instance of a
+boto Queue object and represents the queue that the message will be stored in. The default value for
+this parameter is None.
+
+The constructor for the Message class must accept a keyword parameter "body" which represents the
+content or body of the message. The format of this parameter will depend on the behavior of the
+particular Message subclass. For example, if the Message subclass provides dictionary-like behavior to the
+user the body passed to the constructor should be a dict-like object that can be used to populate
+the initial state of the message.
+
+The Message class must provide an encode method that accepts a value of the same type as the body
+parameter of the constructor and returns a string of characters that are able to be stored in an
+SQS message body (see rules above).
+
+The Message class must provide a decode method that accepts a string of characters that can be
+stored (and probably were stored!) in an SQS message and return an object of a type that is consistent
+with the "body" parameter accepted on the class constructor.
+
+The Message class must provide a __len__ method that will return the size of the encoded message
+that would be stored in SQS based on the current state of the Message object.
+
+The Message class must provide a get_body method that will return the body of the message in the
+same format accepted in the constructor of the class.
+
+The Message class must provide a set_body method that accepts a message body in the same format
+accepted by the constructor of the class. This method should alter to the internal state of the
+Message object to reflect the state represented in the message body parameter.
+
+The Message class must provide a get_body_encoded method that returns the current body of the message
+in the format in which it would be stored in SQS.
+"""
+
+import base64
+import StringIO
+from boto.sqs.attributes import Attributes
+from boto.exception import SQSDecodeError
+
+class RawMessage:
+ """
+ Base class for SQS messages. RawMessage does not encode the message
+ in any way. Whatever you store in the body of the message is what
+ will be written to SQS and whatever is returned from SQS is stored
+ directly into the body of the message.
+ """
+
+ def __init__(self, queue=None, body=''):
+ self.queue = queue
+ self.set_body(body)
+ self.id = None
+ self.receipt_handle = None
+ self.md5 = None
+ self.attributes = Attributes(self)
+
+ def __len__(self):
+ return len(self.encode(self._body))
+
+ def startElement(self, name, attrs, connection):
+ if name == 'Attribute':
+ return self.attributes
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'Body':
+ self.set_body(self.decode(value))
+ elif name == 'MessageId':
+ self.id = value
+ elif name == 'ReceiptHandle':
+ self.receipt_handle = value
+ elif name == 'MD5OfMessageBody':
+ self.md5 = value
+ else:
+ setattr(self, name, value)
+
+ def encode(self, value):
+ """Transform body object into serialized byte array format."""
+ return value
+
+ def decode(self, value):
+ """Transform seralized byte array into any object."""
+ return value
+
+ def set_body(self, body):
+ """Override the current body for this object, using decoded format."""
+ self._body = body
+
+ def get_body(self):
+ return self._body
+
+ def get_body_encoded(self):
+ """
+ This method is really a semi-private method used by the Queue.write
+ method when writing the contents of the message to SQS.
+ You probably shouldn't need to call this method in the normal course of events.
+ """
+ return self.encode(self.get_body())
+
+ def delete(self):
+ if self.queue:
+ return self.queue.delete_message(self)
+
+ def change_visibility(self, visibility_timeout):
+ if self.queue:
+ self.queue.connection.change_message_visibility(self.queue,
+ self.receipt_handle,
+ visibility_timeout)
+
+class Message(RawMessage):
+ """
+ The default Message class used for SQS queues. This class automatically
+ encodes/decodes the message body using Base64 encoding to avoid any
+ illegal characters in the message body. See:
+
+ http://developer.amazonwebservices.com/connect/thread.jspa?messageID=49680%EC%88%90
+
+ for details on why this is a good idea. The encode/decode is meant to
+ be transparent to the end-user.
+ """
+
+ def encode(self, value):
+ return base64.b64encode(value)
+
+ def decode(self, value):
+ try:
+ value = base64.b64decode(value)
+ except:
+ raise SQSDecodeError('Unable to decode message', self)
+ return value
+
+class MHMessage(Message):
+ """
+ The MHMessage class provides a message that provides RFC821-like
+ headers like this:
+
+ HeaderName: HeaderValue
+
+ The encoding/decoding of this is handled automatically and after
+ the message body has been read, the message instance can be treated
+ like a mapping object, i.e. m['HeaderName'] would return 'HeaderValue'.
+ """
+
+ def __init__(self, queue=None, body=None, xml_attrs=None):
+ if body == None or body == '':
+ body = {}
+ Message.__init__(self, queue, body)
+
+ def decode(self, value):
+ try:
+ msg = {}
+ fp = StringIO.StringIO(value)
+ line = fp.readline()
+ while line:
+ delim = line.find(':')
+ key = line[0:delim]
+ value = line[delim+1:].strip()
+ msg[key.strip()] = value.strip()
+ line = fp.readline()
+ except:
+ raise SQSDecodeError('Unable to decode message', self)
+ return msg
+
+ def encode(self, value):
+ s = ''
+ for item in value.items():
+ s = s + '%s: %s\n' % (item[0], item[1])
+ return s
+
+ def __getitem__(self, key):
+ if self._body.has_key(key):
+ return self._body[key]
+ else:
+ raise KeyError(key)
+
+ def __setitem__(self, key, value):
+ self._body[key] = value
+ self.set_body(self._body)
+
+ def keys(self):
+ return self._body.keys()
+
+ def values(self):
+ return self._body.values()
+
+ def items(self):
+ return self._body.items()
+
+ def has_key(self, key):
+ return self._body.has_key(key)
+
+ def update(self, d):
+ self._body.update(d)
+ self.set_body(self._body)
+
+ def get(self, key, default=None):
+ return self._body.get(key, default)
+
+class EncodedMHMessage(MHMessage):
+ """
+ The EncodedMHMessage class provides a message that provides RFC821-like
+ headers like this:
+
+ HeaderName: HeaderValue
+
+ This variation encodes/decodes the body of the message in base64 automatically.
+ The message instance can be treated like a mapping object,
+ i.e. m['HeaderName'] would return 'HeaderValue'.
+ """
+
+ def decode(self, value):
+ try:
+ value = base64.b64decode(value)
+ except:
+ raise SQSDecodeError('Unable to decode message', self)
+ return MHMessage.decode(self, value)
+
+ def encode(self, value):
+ value = MHMessage.encode(value)
+ return base64.b64encode(self, value)
+
diff --git a/api/boto/sqs/queue.py b/api/boto/sqs/queue.py
new file mode 100644
index 0000000..48b6115
--- /dev/null
+++ b/api/boto/sqs/queue.py
@@ -0,0 +1,415 @@
+# 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 SQS Queue
+"""
+
+import xml.sax
+import urlparse
+from boto.exception import SQSError
+from boto.handler import XmlHandler
+from boto.sqs.message import Message
+from boto.resultset import ResultSet
+
+class Queue:
+
+ def __init__(self, connection=None, url=None, message_class=Message):
+ self.connection = connection
+ self.url = url
+ self.message_class = message_class
+ self.visibility_timeout = None
+
+ def _id(self):
+ if self.url:
+ val = urlparse.urlparse(self.url)[2]
+ else:
+ val = self.url
+ return val
+ id = property(_id)
+
+ def _name(self):
+ if self.url:
+ val = urlparse.urlparse(self.url)[2].split('/')[2]
+ else:
+ val = self.url
+ return val
+ name = property(_name)
+
+ def startElement(self, name, attrs, connection):
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'QueueUrl':
+ self.url = value
+ elif name == 'VisibilityTimeout':
+ self.visibility_timeout = int(value)
+ else:
+ setattr(self, name, value)
+
+ def set_message_class(self, message_class):
+ """
+ Set the message class that should be used when instantiating messages read
+ from the queue. By default, the class boto.sqs.message.Message is used but
+ this can be overriden with any class that behaves like a message.
+
+ :type message_class: Message-like class
+ :param message_class: The new Message class
+ """
+ self.message_class = message_class
+
+ def get_attributes(self, attributes='All'):
+ """
+ Retrieves attributes about this queue object and returns
+ them in an Attribute instance (subclass of a Dictionary).
+
+ :type attributes: string
+ :param attributes: String containing one of:
+ ApproximateNumberOfMessages,
+ ApproximateNumberOfMessagesNotVisible,
+ VisibilityTimeout,
+ CreatedTimestamp,
+ LastModifiedTimestamp,
+ Policy
+ :rtype: Attribute object
+ :return: An Attribute object which is a mapping type holding the
+ requested name/value pairs
+ """
+ return self.connection.get_queue_attributes(self, attributes)
+
+ def set_attribute(self, attribute, value):
+ """
+ Set a new value for an attribute of the Queue.
+
+ :type attribute: String
+ :param attribute: The name of the attribute you want to set. The
+ only valid value at this time is: VisibilityTimeout
+ :type value: int
+ :param value: The new value for the attribute.
+ For VisibilityTimeout the value must be an
+ integer number of seconds from 0 to 86400.
+
+ :rtype: bool
+ :return: True if successful, otherwise False.
+ """
+ return self.connection.set_queue_attribute(self, attribute, value)
+
+ def get_timeout(self):
+ """
+ Get the visibility timeout for the queue.
+
+ :rtype: int
+ :return: The number of seconds as an integer.
+ """
+ a = self.get_attributes('VisibilityTimeout')
+ return int(a['VisibilityTimeout'])
+
+ def set_timeout(self, visibility_timeout):
+ """
+ Set the visibility timeout for the queue.
+
+ :type visibility_timeout: int
+ :param visibility_timeout: The desired timeout in seconds
+ """
+ retval = self.set_attribute('VisibilityTimeout', visibility_timeout)
+ if retval:
+ self.visibility_timeout = visibility_timeout
+ return retval
+
+ def add_permission(self, label, aws_account_id, action_name):
+ """
+ Add a permission to a queue.
+
+ :type label: str or unicode
+ :param label: A unique identification of the permission you are setting.
+ Maximum of 80 characters ``[0-9a-zA-Z_-]``
+ Example, AliceSendMessage
+
+ :type aws_account_id: str or unicode
+ :param principal_id: The AWS account number of the principal who will be given
+ permission. The principal must have an AWS account, but
+ does not need to be signed up for Amazon SQS. For information
+ about locating the AWS account identification.
+
+ :type action_name: str or unicode
+ :param action_name: The action. Valid choices are:
+ \*|SendMessage|ReceiveMessage|DeleteMessage|
+ ChangeMessageVisibility|GetQueueAttributes
+
+ :rtype: bool
+ :return: True if successful, False otherwise.
+
+ """
+ return self.connection.add_permission(self, label, aws_account_id, action_name)
+
+ def remove_permission(self, label):
+ """
+ Remove a permission from a queue.
+
+ :type label: str or unicode
+ :param label: The unique label associated with the permission being removed.
+
+ :rtype: bool
+ :return: True if successful, False otherwise.
+ """
+ return self.connection.remove_permission(self, label)
+
+ def read(self, visibility_timeout=None):
+ """
+ Read a single message from the queue.
+
+ :type visibility_timeout: int
+ :param visibility_timeout: The timeout for this message in seconds
+
+ :rtype: :class:`boto.sqs.message.Message`
+ :return: A single message or None if queue is empty
+ """
+ rs = self.get_messages(1, visibility_timeout)
+ if len(rs) == 1:
+ return rs[0]
+ else:
+ return None
+
+ def write(self, message):
+ """
+ Add a single message to the queue.
+
+ :type message: Message
+ :param message: The message to be written to the queue
+
+ :rtype: :class:`boto.sqs.message.Message`
+ :return: The :class:`boto.sqs.message.Message` object that was written.
+ """
+ new_msg = self.connection.send_message(self, message.get_body_encoded())
+ message.id = new_msg.id
+ message.md5 = new_msg.md5
+ return message
+
+ def new_message(self, body=''):
+ """
+ Create new message of appropriate class.
+
+ :type body: message body
+ :param body: The body of the newly created message (optional).
+
+ :rtype: :class:`boto.sqs.message.Message`
+ :return: A new Message object
+ """
+ m = self.message_class(self, body)
+ m.queue = self
+ return m
+
+ # get a variable number of messages, returns a list of messages
+ def get_messages(self, num_messages=1, visibility_timeout=None,
+ attributes=None):
+ """
+ Get a variable number of messages.
+
+ :type num_messages: int
+ :param num_messages: The maximum number of messages to read from the queue.
+
+ :type visibility_timeout: int
+ :param visibility_timeout: The VisibilityTimeout for the messages read.
+
+ :type attributes: list of strings
+ :param attributes: A list of additional attributes that will be returned
+ with the response. Valid values:
+ All
+ SenderId
+ SentTimestamp
+ ApproximateReceiveCount
+ ApproximateFirstReceiveTimestamp
+ :rtype: list
+ :return: A list of :class:`boto.sqs.message.Message` objects.
+ """
+ return self.connection.receive_message(self, number_messages=num_messages,
+ visibility_timeout=visibility_timeout,
+ attributes=attributes)
+
+ def delete_message(self, message):
+ """
+ Delete a message from the queue.
+
+ :type message: :class:`boto.sqs.message.Message`
+ :param message: The :class:`boto.sqs.message.Message` object to delete.
+
+ :rtype: bool
+ :return: True if successful, False otherwise
+ """
+ return self.connection.delete_message(self, message)
+
+ def delete(self):
+ """
+ Delete the queue.
+ """
+ return self.connection.delete_queue(self)
+
+ def clear(self, page_size=10, vtimeout=10):
+ """Utility function to remove all messages from a queue"""
+ n = 0
+ l = self.get_messages(page_size, vtimeout)
+ while l:
+ for m in l:
+ self.delete_message(m)
+ n += 1
+ l = self.get_messages(page_size, vtimeout)
+ return n
+
+ def count(self, page_size=10, vtimeout=10):
+ """
+ Utility function to count the number of messages in a queue.
+ Note: This function now calls GetQueueAttributes to obtain
+ an 'approximate' count of the number of messages in a queue.
+ """
+ a = self.get_attributes('ApproximateNumberOfMessages')
+ return int(a['ApproximateNumberOfMessages'])
+
+ def count_slow(self, page_size=10, vtimeout=10):
+ """
+ Deprecated. This is the old 'count' method that actually counts
+ the messages by reading them all. This gives an accurate count but
+ is very slow for queues with non-trivial number of messasges.
+ Instead, use get_attribute('ApproximateNumberOfMessages') to take
+ advantage of the new SQS capability. This is retained only for
+ the unit tests.
+ """
+ n = 0
+ l = self.get_messages(page_size, vtimeout)
+ while l:
+ for m in l:
+ n += 1
+ l = self.get_messages(page_size, vtimeout)
+ return n
+
+ def dump_(self, file_name, page_size=10, vtimeout=10, sep='\n'):
+ """Utility function to dump the messages in a queue to a file
+ NOTE: Page size must be < 10 else SQS errors"""
+ fp = open(file_name, 'wb')
+ n = 0
+ l = self.get_messages(page_size, vtimeout)
+ while l:
+ for m in l:
+ fp.write(m.get_body())
+ if sep:
+ fp.write(sep)
+ n += 1
+ l = self.get_messages(page_size, vtimeout)
+ fp.close()
+ return n
+
+ def save_to_file(self, fp, sep='\n'):
+ """
+ Read all messages from the queue and persist them to file-like object.
+ Messages are written to the file and the 'sep' string is written
+ in between messages. Messages are deleted from the queue after
+ being written to the file.
+ Returns the number of messages saved.
+ """
+ n = 0
+ m = self.read()
+ while m:
+ n += 1
+ fp.write(m.get_body())
+ if sep:
+ fp.write(sep)
+ self.delete_message(m)
+ m = self.read()
+ return n
+
+ def save_to_filename(self, file_name, sep='\n'):
+ """
+ Read all messages from the queue and persist them to local file.
+ Messages are written to the file and the 'sep' string is written
+ in between messages. Messages are deleted from the queue after
+ being written to the file.
+ Returns the number of messages saved.
+ """
+ fp = open(file_name, 'wb')
+ n = self.save_to_file(fp, sep)
+ fp.close()
+ return n
+
+ # for backwards compatibility
+ save = save_to_filename
+
+ def save_to_s3(self, bucket):
+ """
+ Read all messages from the queue and persist them to S3.
+ Messages are stored in the S3 bucket using a naming scheme of::
+
+ /
+
+ Messages are deleted from the queue after being saved to S3.
+ Returns the number of messages saved.
+ """
+ n = 0
+ m = self.read()
+ while m:
+ n += 1
+ key = bucket.new_key('%s/%s' % (self.id, m.id))
+ key.set_contents_from_string(m.get_body())
+ self.delete_message(m)
+ m = self.read()
+ return n
+
+ def load_from_s3(self, bucket, prefix=None):
+ """
+ Load messages previously saved to S3.
+ """
+ n = 0
+ if prefix:
+ prefix = '%s/' % prefix
+ else:
+ prefix = '%s/' % self.id[1:]
+ rs = bucket.list(prefix=prefix)
+ for key in rs:
+ n += 1
+ m = self.new_message(key.get_contents_as_string())
+ self.write(m)
+ return n
+
+ def load_from_file(self, fp, sep='\n'):
+ """Utility function to load messages from a file-like object to a queue"""
+ n = 0
+ body = ''
+ l = fp.readline()
+ while l:
+ if l == sep:
+ m = Message(self, body)
+ self.write(m)
+ n += 1
+ print 'writing message %d' % n
+ body = ''
+ else:
+ body = body + l
+ l = fp.readline()
+ return n
+
+ def load_from_filename(self, file_name, sep='\n'):
+ """Utility function to load messages from a local filename to a queue"""
+ fp = open(file_name, 'rb')
+ n = self.load_file_file(fp, sep)
+ fp.close()
+ return n
+
+ # for backward compatibility
+ load = load_from_filename
+
diff --git a/api/boto/sqs/regioninfo.py b/api/boto/sqs/regioninfo.py
new file mode 100644
index 0000000..1d13a40
--- /dev/null
+++ b/api/boto/sqs/regioninfo.py
@@ -0,0 +1,40 @@
+# 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.ec2.regioninfo import RegionInfo
+
+class SQSRegionInfo(RegionInfo):
+
+ def connect(self, **kw_params):
+ """
+ Connect to this Region's endpoint. Returns an SQSConnection
+ object pointing to the endpoint associated with this region.
+ You may pass any of the arguments accepted by the SQSConnection
+ object's constructor as keyword arguments and they will be
+ passed along to the SQSConnection object.
+
+ :rtype: :class:`boto.sqs.connection.SQSConnection`
+ :return: The connection to this regions endpoint
+ """
+ from boto.sqs.connection import SQSConnection
+ return SQSConnection(region=self, **kw_params)
+
diff --git a/api/boto/tests/__init__.py b/api/boto/tests/__init__.py
new file mode 100644
index 0000000..449bd16
--- /dev/null
+++ b/api/boto/tests/__init__.py
@@ -0,0 +1,23 @@
+# 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/api/boto/tests/devpay_s3.py b/api/boto/tests/devpay_s3.py
new file mode 100644
index 0000000..bb91125
--- /dev/null
+++ b/api/boto/tests/devpay_s3.py
@@ -0,0 +1,177 @@
+#!/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.
+
+"""
+Some unit tests for the S3Connection
+"""
+
+import time
+import os
+import urllib
+
+from boto.s3.connection import S3Connection
+from boto.exception import S3PermissionsError
+
+# this test requires a devpay product and user token to run:
+
+AMAZON_USER_TOKEN = '{UserToken}...your token here...'
+DEVPAY_HEADERS = { 'x-amz-security-token': AMAZON_USER_TOKEN }
+
+print '--- running S3Connection tests (DevPay) ---'
+c = S3Connection()
+# create a new, empty bucket
+bucket_name = 'test-%d' % int(time.time())
+bucket = c.create_bucket(bucket_name, headers=DEVPAY_HEADERS)
+# now try a get_bucket call and see if it's really there
+bucket = c.get_bucket(bucket_name, headers=DEVPAY_HEADERS)
+# test logging
+logging_bucket = c.create_bucket(bucket_name + '-log', headers=DEVPAY_HEADERS)
+logging_bucket.set_as_logging_target(headers=DEVPAY_HEADERS)
+bucket.enable_logging(target_bucket=logging_bucket, target_prefix=bucket.name, headers=DEVPAY_HEADERS)
+bucket.disable_logging(headers=DEVPAY_HEADERS)
+c.delete_bucket(logging_bucket, headers=DEVPAY_HEADERS)
+# create a new key and store it's content from a string
+k = bucket.new_key()
+k.name = 'foobar'
+s1 = 'This is a test of file upload and download'
+s2 = 'This is a second string to test file upload and download'
+k.set_contents_from_string(s1, headers=DEVPAY_HEADERS)
+fp = open('foobar', 'wb')
+# now get the contents from s3 to a local file
+k.get_contents_to_file(fp, headers=DEVPAY_HEADERS)
+fp.close()
+fp = open('foobar')
+# check to make sure content read from s3 is identical to original
+assert s1 == fp.read(), 'corrupted file'
+fp.close()
+# test generated URLs
+url = k.generate_url(3600, headers=DEVPAY_HEADERS)
+file = urllib.urlopen(url)
+assert s1 == file.read(), 'invalid URL %s' % url
+url = k.generate_url(3600, force_http=True, headers=DEVPAY_HEADERS)
+file = urllib.urlopen(url)
+assert s1 == file.read(), 'invalid URL %s' % url
+bucket.delete_key(k, headers=DEVPAY_HEADERS)
+# test a few variations on get_all_keys - first load some data
+# for the first one, let's override the content type
+phony_mimetype = 'application/x-boto-test'
+headers = {'Content-Type': phony_mimetype}
+headers.update(DEVPAY_HEADERS)
+k.name = 'foo/bar'
+k.set_contents_from_string(s1, headers)
+k.name = 'foo/bas'
+k.set_contents_from_filename('foobar', headers=DEVPAY_HEADERS)
+k.name = 'foo/bat'
+k.set_contents_from_string(s1, headers=DEVPAY_HEADERS)
+k.name = 'fie/bar'
+k.set_contents_from_string(s1, headers=DEVPAY_HEADERS)
+k.name = 'fie/bas'
+k.set_contents_from_string(s1, headers=DEVPAY_HEADERS)
+k.name = 'fie/bat'
+k.set_contents_from_string(s1, headers=DEVPAY_HEADERS)
+# try resetting the contents to another value
+md5 = k.md5
+k.set_contents_from_string(s2, headers=DEVPAY_HEADERS)
+assert k.md5 != md5
+os.unlink('foobar')
+all = bucket.get_all_keys(headers=DEVPAY_HEADERS)
+assert len(all) == 6
+rs = bucket.get_all_keys(prefix='foo', headers=DEVPAY_HEADERS)
+assert len(rs) == 3
+rs = bucket.get_all_keys(prefix='', delimiter='/', headers=DEVPAY_HEADERS)
+assert len(rs) == 2
+rs = bucket.get_all_keys(maxkeys=5, headers=DEVPAY_HEADERS)
+assert len(rs) == 5
+# test the lookup method
+k = bucket.lookup('foo/bar', headers=DEVPAY_HEADERS)
+assert isinstance(k, bucket.key_class)
+assert k.content_type == phony_mimetype
+k = bucket.lookup('notthere', headers=DEVPAY_HEADERS)
+assert k == None
+# try some metadata stuff
+k = bucket.new_key()
+k.name = 'has_metadata'
+mdkey1 = 'meta1'
+mdval1 = 'This is the first metadata value'
+k.set_metadata(mdkey1, mdval1)
+mdkey2 = 'meta2'
+mdval2 = 'This is the second metadata value'
+k.set_metadata(mdkey2, mdval2)
+k.set_contents_from_string(s1, headers=DEVPAY_HEADERS)
+k = bucket.lookup('has_metadata', headers=DEVPAY_HEADERS)
+assert k.get_metadata(mdkey1) == mdval1
+assert k.get_metadata(mdkey2) == mdval2
+k = bucket.new_key()
+k.name = 'has_metadata'
+k.get_contents_as_string(headers=DEVPAY_HEADERS)
+assert k.get_metadata(mdkey1) == mdval1
+assert k.get_metadata(mdkey2) == mdval2
+bucket.delete_key(k, headers=DEVPAY_HEADERS)
+# test list and iterator
+rs1 = bucket.list(headers=DEVPAY_HEADERS)
+num_iter = 0
+for r in rs1:
+ num_iter = num_iter + 1
+rs = bucket.get_all_keys(headers=DEVPAY_HEADERS)
+num_keys = len(rs)
+assert num_iter == num_keys
+# try a key with a funny character
+k = bucket.new_key()
+k.name = 'testnewline\n'
+k.set_contents_from_string('This is a test', headers=DEVPAY_HEADERS)
+rs = bucket.get_all_keys(headers=DEVPAY_HEADERS)
+assert len(rs) == num_keys + 1
+bucket.delete_key(k, headers=DEVPAY_HEADERS)
+rs = bucket.get_all_keys(headers=DEVPAY_HEADERS)
+assert len(rs) == num_keys
+# try some acl stuff
+bucket.set_acl('public-read', headers=DEVPAY_HEADERS)
+policy = bucket.get_acl(headers=DEVPAY_HEADERS)
+assert len(policy.acl.grants) == 2
+bucket.set_acl('private', headers=DEVPAY_HEADERS)
+policy = bucket.get_acl(headers=DEVPAY_HEADERS)
+assert len(policy.acl.grants) == 1
+k = bucket.lookup('foo/bar', headers=DEVPAY_HEADERS)
+k.set_acl('public-read', headers=DEVPAY_HEADERS)
+policy = k.get_acl(headers=DEVPAY_HEADERS)
+assert len(policy.acl.grants) == 2
+k.set_acl('private', headers=DEVPAY_HEADERS)
+policy = k.get_acl(headers=DEVPAY_HEADERS)
+assert len(policy.acl.grants) == 1
+# try the convenience methods for grants
+# this doesn't work with devpay
+#bucket.add_user_grant('FULL_CONTROL',
+# 'c1e724fbfa0979a4448393c59a8c055011f739b6d102fb37a65f26414653cd67',
+# headers=DEVPAY_HEADERS)
+try:
+ bucket.add_email_grant('foobar', 'foo@bar.com', headers=DEVPAY_HEADERS)
+except S3PermissionsError:
+ pass
+# now delete all keys in bucket
+for k in all:
+ bucket.delete_key(k, headers=DEVPAY_HEADERS)
+# now delete bucket
+
+c.delete_bucket(bucket, headers=DEVPAY_HEADERS)
+
+print '--- tests completed ---'
diff --git a/api/boto/tests/test.py b/api/boto/tests/test.py
new file mode 100755
index 0000000..c6175ca
--- /dev/null
+++ b/api/boto/tests/test.py
@@ -0,0 +1,81 @@
+#!/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.
+
+"""
+do the unit tests!
+"""
+
+import sys, os, unittest
+import getopt, sys
+import boto
+
+from boto.tests.test_sqsconnection import SQSConnectionTest
+from boto.tests.test_s3connection import S3ConnectionTest
+from boto.tests.test_ec2connection import EC2ConnectionTest
+from boto.tests.test_sdbconnection import SDBConnectionTest
+
+def usage():
+ print 'test.py [-t testsuite] [-v verbosity]'
+ print ' -t run specific testsuite (s3|sqs|ec2|sdb|all)'
+ print ' -v verbosity (0|1|2)'
+
+def main():
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'ht:v:',
+ ['help', 'testsuite', 'verbosity'])
+ except:
+ usage()
+ sys.exit(2)
+ testsuite = 'all'
+ verbosity = 1
+ for o, a in opts:
+ if o in ('-h', '--help'):
+ usage()
+ sys.exit()
+ if o in ('-t', '--testsuite'):
+ testsuite = a
+ if o in ('-v', '--verbosity'):
+ verbosity = int(a)
+ if len(args) != 0:
+ usage()
+ sys.exit()
+ suite = unittest.TestSuite()
+ if testsuite == 'all':
+ suite.addTest(unittest.makeSuite(SQSConnectionTest))
+ suite.addTest(unittest.makeSuite(S3ConnectionTest))
+ suite.addTest(unittest.makeSuite(EC2ConnectionTest))
+ suite.addTest(unittest.makeSuite(SDBConnectionTest))
+ elif testsuite == 's3':
+ suite.addTest(unittest.makeSuite(S3ConnectionTest))
+ elif testsuite == 'sqs':
+ suite.addTest(unittest.makeSuite(SQSConnectionTest))
+ elif testsuite == 'ec2':
+ suite.addTest(unittest.makeSuite(EC2ConnectionTest))
+ elif testsuite == 'sdb':
+ suite.addTest(unittest.makeSuite(SDBConnectionTest))
+ else:
+ usage()
+ sys.exit()
+ unittest.TextTestRunner(verbosity=verbosity).run(suite)
+
+if __name__ == "__main__":
+ main()
diff --git a/api/boto/tests/test_ec2connection.py b/api/boto/tests/test_ec2connection.py
new file mode 100644
index 0000000..8f1fb59
--- /dev/null
+++ b/api/boto/tests/test_ec2connection.py
@@ -0,0 +1,154 @@
+#!/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.
+
+"""
+Some unit tests for the EC2Connection
+"""
+
+import unittest
+import time
+import os
+from boto.ec2.connection import EC2Connection
+import telnetlib
+import socket
+
+class EC2ConnectionTest (unittest.TestCase):
+
+ def test_1_basic(self):
+ # this is my user_id, if you want to run these tests you should
+ # replace this with yours or they won't work
+ user_id = '084307701560'
+ print '--- running EC2Connection tests ---'
+ c = EC2Connection()
+ # get list of private AMI's
+ rs = c.get_all_images(owners=[user_id])
+ assert len(rs) > 0
+ # now pick the first one
+ image = rs[0]
+ # temporarily make this image runnable by everyone
+ status = image.set_launch_permissions(group_names=['all'])
+ assert status
+ d = image.get_launch_permissions()
+ assert d.has_key('groups')
+ assert len(d['groups']) > 0
+ # now remove that permission
+ status = image.remove_launch_permissions(group_names=['all'])
+ assert status
+ d = image.get_launch_permissions()
+ assert not d.has_key('groups')
+
+ # create a new security group
+ group_name = 'test-%d' % int(time.time())
+ group_desc = 'This is a security group created during unit testing'
+ group = c.create_security_group(group_name, group_desc)
+ # now get a listing of all security groups and look for our new one
+ rs = c.get_all_security_groups()
+ found = False
+ for g in rs:
+ if g.name == group_name:
+ found = True
+ assert found
+ # now pass arg to filter results to only our new group
+ rs = c.get_all_security_groups([group_name])
+ assert len(rs) == 1
+ group = rs[0]
+ #
+ # now delete the security group
+ status = c.delete_security_group(group_name)
+ # now make sure it's really gone
+ rs = c.get_all_security_groups()
+ found = False
+ for g in rs:
+ if g.name == group_name:
+ found = True
+ assert not found
+ # now create it again for use with the instance test
+ group = c.create_security_group(group_name, group_desc)
+
+ # now try to launch apache image with our new security group
+ rs = c.get_all_images()
+ img_loc = 'ec2-public-images/fedora-core4-apache.manifest.xml'
+ for image in rs:
+ if image.location == img_loc:
+ break
+ reservation = image.run(security_groups=[group.name])
+ instance = reservation.instances[0]
+ while instance.state != 'running':
+ print '\tinstance is %s' % instance.state
+ time.sleep(30)
+ instance.update()
+ # instance in now running, try to telnet to port 80
+ t = telnetlib.Telnet()
+ try:
+ t.open(instance.dns_name, 80)
+ except socket.error:
+ pass
+ # now open up port 80 and try again, it should work
+ group.authorize('tcp', 80, 80, '0.0.0.0/0')
+ t.open(instance.dns_name, 80)
+ t.close()
+ # now revoke authorization and try again
+ group.revoke('tcp', 80, 80, '0.0.0.0/0')
+ try:
+ t.open(instance.dns_name, 80)
+ except socket.error:
+ pass
+ # now kill the instance and delete the security group
+ instance.stop()
+ # unfortunately, I can't delete the sg within this script
+ #sg.delete()
+
+ # create a new key pair
+ key_name = 'test-%d' % int(time.time())
+ status = c.create_key_pair(key_name)
+ assert status
+ # now get a listing of all key pairs and look for our new one
+ rs = c.get_all_key_pairs()
+ found = False
+ for k in rs:
+ if k.name == key_name:
+ found = True
+ assert found
+ # now pass arg to filter results to only our new key pair
+ rs = c.get_all_key_pairs([key_name])
+ assert len(rs) == 1
+ key_pair = rs[0]
+ # now delete the key pair
+ status = c.delete_key_pair(key_name)
+ # now make sure it's really gone
+ rs = c.get_all_key_pairs()
+ found = False
+ for k in rs:
+ if k.name == key_name:
+ found = True
+ assert not found
+
+ # short test around Paid AMI capability
+ demo_paid_ami_id = 'ami-bd9d78d4'
+ demo_paid_ami_product_code = 'A79EC0DB'
+ l = c.get_all_images([demo_paid_ami_id])
+ assert len(l) == 1
+ assert len(l[0].product_codes) == 1
+ assert l[0].product_codes[0] == demo_paid_ami_product_code
+
+ print '--- tests completed ---'
diff --git a/api/boto/tests/test_s3connection.py b/api/boto/tests/test_s3connection.py
new file mode 100644
index 0000000..7afc8d2
--- /dev/null
+++ b/api/boto/tests/test_s3connection.py
@@ -0,0 +1,169 @@
+#!/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.
+
+"""
+Some unit tests for the S3Connection
+"""
+
+import unittest
+import time
+import os
+import urllib
+from boto.s3.connection import S3Connection
+from boto.exception import S3PermissionsError
+
+class S3ConnectionTest (unittest.TestCase):
+
+ def test_1_basic(self):
+ print '--- running S3Connection tests ---'
+ c = S3Connection()
+ # create a new, empty bucket
+ bucket_name = 'test-%d' % int(time.time())
+ bucket = c.create_bucket(bucket_name)
+ # now try a get_bucket call and see if it's really there
+ bucket = c.get_bucket(bucket_name)
+ # test logging
+ logging_bucket = c.create_bucket(bucket_name + '-log')
+ logging_bucket.set_as_logging_target()
+ bucket.enable_logging(target_bucket=logging_bucket, target_prefix=bucket.name)
+ bucket.disable_logging()
+ c.delete_bucket(logging_bucket)
+ k = bucket.new_key()
+ k.name = 'foobar'
+ s1 = 'This is a test of file upload and download'
+ s2 = 'This is a second string to test file upload and download'
+ k.set_contents_from_string(s1)
+ fp = open('foobar', 'wb')
+ # now get the contents from s3 to a local file
+ k.get_contents_to_file(fp)
+ fp.close()
+ fp = open('foobar')
+ # check to make sure content read from s3 is identical to original
+ assert s1 == fp.read(), 'corrupted file'
+ fp.close()
+ # test generated URLs
+ url = k.generate_url(3600)
+ file = urllib.urlopen(url)
+ assert s1 == file.read(), 'invalid URL %s' % url
+ url = k.generate_url(3600, force_http=True)
+ file = urllib.urlopen(url)
+ assert s1 == file.read(), 'invalid URL %s' % url
+ bucket.delete_key(k)
+ # test a few variations on get_all_keys - first load some data
+ # for the first one, let's override the content type
+ phony_mimetype = 'application/x-boto-test'
+ headers = {'Content-Type': phony_mimetype}
+ k.name = 'foo/bar'
+ k.set_contents_from_string(s1, headers)
+ k.name = 'foo/bas'
+ k.set_contents_from_filename('foobar')
+ k.name = 'foo/bat'
+ k.set_contents_from_string(s1)
+ k.name = 'fie/bar'
+ k.set_contents_from_string(s1)
+ k.name = 'fie/bas'
+ k.set_contents_from_string(s1)
+ k.name = 'fie/bat'
+ k.set_contents_from_string(s1)
+ # try resetting the contents to another value
+ md5 = k.md5
+ k.set_contents_from_string(s2)
+ assert k.md5 != md5
+ os.unlink('foobar')
+ all = bucket.get_all_keys()
+ assert len(all) == 6
+ rs = bucket.get_all_keys(prefix='foo')
+ assert len(rs) == 3
+ rs = bucket.get_all_keys(prefix='', delimiter='/')
+ assert len(rs) == 2
+ rs = bucket.get_all_keys(maxkeys=5)
+ assert len(rs) == 5
+ # test the lookup method
+ k = bucket.lookup('foo/bar')
+ assert isinstance(k, bucket.key_class)
+ assert k.content_type == phony_mimetype
+ k = bucket.lookup('notthere')
+ assert k == None
+ # try some metadata stuff
+ k = bucket.new_key()
+ k.name = 'has_metadata'
+ mdkey1 = 'meta1'
+ mdval1 = 'This is the first metadata value'
+ k.set_metadata(mdkey1, mdval1)
+ mdkey2 = 'meta2'
+ mdval2 = 'This is the second metadata value'
+ k.set_metadata(mdkey2, mdval2)
+ k.set_contents_from_string(s1)
+ k = bucket.lookup('has_metadata')
+ assert k.get_metadata(mdkey1) == mdval1
+ assert k.get_metadata(mdkey2) == mdval2
+ k = bucket.new_key()
+ k.name = 'has_metadata'
+ k.get_contents_as_string()
+ assert k.get_metadata(mdkey1) == mdval1
+ assert k.get_metadata(mdkey2) == mdval2
+ bucket.delete_key(k)
+ # test list and iterator
+ rs1 = bucket.list()
+ num_iter = 0
+ for r in rs1:
+ num_iter = num_iter + 1
+ rs = bucket.get_all_keys()
+ num_keys = len(rs)
+ assert num_iter == num_keys
+ # try a key with a funny character
+ k = bucket.new_key()
+ k.name = 'testnewline\n'
+ k.set_contents_from_string('This is a test')
+ rs = bucket.get_all_keys()
+ assert len(rs) == num_keys + 1
+ bucket.delete_key(k)
+ rs = bucket.get_all_keys()
+ assert len(rs) == num_keys
+ # try some acl stuff
+ bucket.set_acl('public-read')
+ policy = bucket.get_acl()
+ assert len(policy.acl.grants) == 2
+ bucket.set_acl('private')
+ policy = bucket.get_acl()
+ assert len(policy.acl.grants) == 1
+ k = bucket.lookup('foo/bar')
+ k.set_acl('public-read')
+ policy = k.get_acl()
+ assert len(policy.acl.grants) == 2
+ k.set_acl('private')
+ policy = k.get_acl()
+ assert len(policy.acl.grants) == 1
+ # try the convenience methods for grants
+ bucket.add_user_grant('FULL_CONTROL',
+ 'c1e724fbfa0979a4448393c59a8c055011f739b6d102fb37a65f26414653cd67')
+ try:
+ bucket.add_email_grant('foobar', 'foo@bar.com')
+ except S3PermissionsError:
+ pass
+ # now delete all keys in bucket
+ for k in all:
+ bucket.delete_key(k)
+ # now delete bucket
+ c.delete_bucket(bucket)
+ print '--- tests completed ---'
diff --git a/api/boto/tests/test_sdbconnection.py b/api/boto/tests/test_sdbconnection.py
new file mode 100644
index 0000000..c2bb74e
--- /dev/null
+++ b/api/boto/tests/test_sdbconnection.py
@@ -0,0 +1,104 @@
+#!/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.
+
+"""
+Some unit tests for the SDBConnection
+"""
+
+import unittest
+import time
+from boto.sdb.connection import SDBConnection
+from boto.exception import SDBResponseError
+
+class SDBConnectionTest (unittest.TestCase):
+
+ def test_1_basic(self):
+ print '--- running SDBConnection tests ---'
+ c = SDBConnection()
+ rs = c.get_all_domains()
+ num_domains = len(rs)
+
+ # try illegal name
+ try:
+ domain = c.create_domain('bad:domain:name')
+ except SDBResponseError:
+ pass
+
+ # now create one that should work and should be unique (i.e. a new one)
+ domain_name = 'test%d' % int(time.time())
+ domain = c.create_domain(domain_name)
+ rs = c.get_all_domains()
+ assert len(rs) == num_domains+1
+
+ # now let's a couple of items and attributes
+ item_1 = 'item1'
+ same_value = 'same_value'
+ attrs_1 = {'name1' : same_value, 'name2' : 'diff_value_1'}
+ domain.put_attributes(item_1, attrs_1)
+ item_2 = 'item2'
+ attrs_2 = {'name1' : same_value, 'name2' : 'diff_value_2'}
+ domain.put_attributes(item_2, attrs_2)
+ time.sleep(10)
+
+ # try to get the attributes and see if they match
+ item = domain.get_attributes(item_1)
+ assert len(item.keys()) == len(attrs_1.keys())
+ assert item['name1'] == attrs_1['name1']
+ assert item['name2'] == attrs_1['name2']
+
+ # try a search or two
+ rs = domain.query("['name1'='%s']" % same_value)
+ n = 0
+ for item in rs:
+ n += 1
+ assert n == 2
+ rs = domain.query("['name2'='diff_value_2']")
+ n = 0
+ for item in rs:
+ n += 1
+ assert n == 1
+
+ # delete all attributes associated with item_1
+ stat = domain.delete_attributes(item_1)
+ assert stat
+
+ # now try a batch put operation on the domain
+ item3 = {'name3_1' : 'value3_1',
+ 'name3_2' : 'value3_2',
+ 'name3_3' : ['value3_3_1', 'value3_3_2']}
+
+ item4 = {'name4_1' : 'value4_1',
+ 'name4_2' : ['value4_2_1', 'value4_2_2'],
+ 'name4_3' : 'value4_3'}
+ items = {'item3' : item3, 'item4' : item4}
+ domain.batch_put_attributes(items)
+ time.sleep(10)
+ item = domain.get_attributes('item3')
+ assert item['name3_2'] == 'value3_2'
+
+ # now delete the domain
+ stat = c.delete_domain(domain)
+ assert stat
+
+ print '--- tests completed ---'
+
diff --git a/api/boto/tests/test_sqsconnection.py b/api/boto/tests/test_sqsconnection.py
new file mode 100644
index 0000000..f24ad32
--- /dev/null
+++ b/api/boto/tests/test_sqsconnection.py
@@ -0,0 +1,142 @@
+#!/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.
+
+"""
+Some unit tests for the SQSConnection
+"""
+
+import unittest
+import time
+from boto.sqs.connection import SQSConnection
+from boto.sqs.message import MHMessage
+from boto.exception import SQSError
+
+class SQSConnectionTest (unittest.TestCase):
+
+ def test_1_basic(self):
+ print '--- running SQSConnection tests ---'
+ c = SQSConnection()
+ rs = c.get_all_queues()
+ num_queues = 0
+ for q in rs:
+ num_queues += 1
+
+ # try illegal name
+ try:
+ queue = c.create_queue('bad_queue_name')
+ except SQSError:
+ pass
+
+ # now create one that should work and should be unique (i.e. a new one)
+ queue_name = 'test%d' % int(time.time())
+ timeout = 60
+ queue = c.create_queue(queue_name, timeout)
+ time.sleep(60)
+ rs = c.get_all_queues()
+ i = 0
+ for q in rs:
+ i += 1
+ assert i == num_queues+1
+ assert queue.count_slow() == 0
+
+ # check the visibility timeout
+ t = queue.get_timeout()
+ assert t == timeout, '%d != %d' % (t, timeout)
+
+ # now try to get queue attributes
+ a = q.get_attributes()
+ assert a.has_key('ApproximateNumberOfMessages')
+ assert a.has_key('VisibilityTimeout')
+ a = q.get_attributes('ApproximateNumberOfMessages')
+ assert a.has_key('ApproximateNumberOfMessages')
+ assert not a.has_key('VisibilityTimeout')
+ a = q.get_attributes('VisibilityTimeout')
+ assert not a.has_key('ApproximateNumberOfMessages')
+ assert a.has_key('VisibilityTimeout')
+
+ # now change the visibility timeout
+ timeout = 45
+ queue.set_timeout(timeout)
+ time.sleep(60)
+ t = queue.get_timeout()
+ assert t == timeout, '%d != %d' % (t, timeout)
+
+ # now add a message
+ message_body = 'This is a test\n'
+ message = queue.new_message(message_body)
+ queue.write(message)
+ time.sleep(30)
+ assert queue.count_slow() == 1
+ time.sleep(30)
+
+ # now read the message from the queue with a 10 second timeout
+ message = queue.read(visibility_timeout=10)
+ assert message
+ assert message.get_body() == message_body
+
+ # now immediately try another read, shouldn't find anything
+ message = queue.read()
+ assert message == None
+
+ # now wait 10 seconds and try again
+ time.sleep(10)
+ message = queue.read()
+ assert message
+
+ if c.APIVersion == '2007-05-01':
+ # now terminate the visibility timeout for this message
+ message.change_visibility(0)
+ # now see if we can read it in the queue
+ message = queue.read()
+ assert message
+
+ # now delete the message
+ queue.delete_message(message)
+ time.sleep(30)
+ assert queue.count_slow() == 0
+
+ # create another queue so we can test force deletion
+ # we will also test MHMessage with this queue
+ queue_name = 'test%d' % int(time.time())
+ timeout = 60
+ queue = c.create_queue(queue_name, timeout)
+ queue.set_message_class(MHMessage)
+ time.sleep(30)
+
+ # now add a couple of messages
+ message = queue.new_message()
+ message['foo'] = 'bar'
+ queue.write(message)
+ message_body = {'fie' : 'baz', 'foo' : 'bar'}
+ message = queue.new_message(body=message_body)
+ queue.write(message)
+ time.sleep(30)
+
+ m = queue.read()
+ assert m['foo'] == 'bar'
+
+ # now delete that queue and messages
+ c.delete_queue(queue, True)
+
+ print '--- tests completed ---'
+
diff --git a/api/boto/utils.py b/api/boto/utils.py
new file mode 100644
index 0000000..db16d30
--- /dev/null
+++ b/api/boto/utils.py
@@ -0,0 +1,560 @@
+# 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.
+
+#
+# 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.
+
+"""
+Some handy utility functions used by several classes.
+"""
+
+import base64
+import hmac
+import re
+import urllib, urllib2
+import imp
+import subprocess, os, StringIO
+import time, datetime
+import logging.handlers
+import boto
+import tempfile
+import smtplib
+import datetime
+from email.MIMEMultipart import MIMEMultipart
+from email.MIMEBase import MIMEBase
+from email.MIMEText import MIMEText
+from email.Utils import formatdate
+from email import Encoders
+
+try:
+ import hashlib
+ _hashfn = hashlib.sha512
+except ImportError:
+ import md5
+ _hashfn = md5.md5
+
+METADATA_PREFIX = 'x-amz-meta-'
+AMAZON_HEADER_PREFIX = 'x-amz-'
+
+# generates the aws canonical string for the given parameters
+def canonical_string(method, path, headers, expires=None):
+ interesting_headers = {}
+ for key in headers:
+ lk = key.lower()
+ if lk in ['content-md5', 'content-type', 'date'] or lk.startswith(AMAZON_HEADER_PREFIX):
+ interesting_headers[lk] = headers[key].strip()
+
+ # these keys get empty strings if they don't exist
+ if not interesting_headers.has_key('content-type'):
+ interesting_headers['content-type'] = ''
+ if not interesting_headers.has_key('content-md5'):
+ interesting_headers['content-md5'] = ''
+
+ # just in case someone used this. it's not necessary in this lib.
+ if interesting_headers.has_key('x-amz-date'):
+ interesting_headers['date'] = ''
+
+ # if you're using expires for query string auth, then it trumps date
+ # (and x-amz-date)
+ if expires:
+ interesting_headers['date'] = str(expires)
+
+ sorted_header_keys = interesting_headers.keys()
+ sorted_header_keys.sort()
+
+ buf = "%s\n" % method
+ for key in sorted_header_keys:
+ if key.startswith(AMAZON_HEADER_PREFIX):
+ buf += "%s:%s\n" % (key, interesting_headers[key])
+ else:
+ buf += "%s\n" % interesting_headers[key]
+
+ # don't include anything after the first ? in the resource...
+ buf += "%s" % path.split('?')[0]
+
+ # ...unless there is an acl or torrent parameter
+ if re.search("[&?]acl($|=|&)", path):
+ buf += "?acl"
+ elif re.search("[&?]logging($|=|&)", path):
+ buf += "?logging"
+ elif re.search("[&?]torrent($|=|&)", path):
+ buf += "?torrent"
+ elif re.search("[&?]location($|=|&)", path):
+ buf += "?location"
+ elif re.search("[&?]requestPayment($|=|&)", path):
+ buf += "?requestPayment"
+
+ return buf
+
+def merge_meta(headers, metadata):
+ final_headers = headers.copy()
+ for k in metadata.keys():
+ if k.lower() in ['cache-control', 'content-md5', 'content-type',
+ 'content-encoding', 'content-disposition',
+ 'date', 'expires']:
+ final_headers[k] = metadata[k]
+ else:
+ final_headers[METADATA_PREFIX + k] = metadata[k]
+
+ return final_headers
+
+def get_aws_metadata(headers):
+ metadata = {}
+ for hkey in headers.keys():
+ if hkey.lower().startswith(METADATA_PREFIX):
+ metadata[hkey[len(METADATA_PREFIX):]] = headers[hkey]
+ del headers[hkey]
+ return metadata
+
+def retry_url(url, retry_on_404=True):
+ for i in range(0, 10):
+ try:
+ req = urllib2.Request(url)
+ resp = urllib2.urlopen(req)
+ return resp.read()
+ except urllib2.HTTPError, e:
+ # in 2.6 you use getcode(), in 2.5 and earlier you use code
+ if hasattr(e, 'getcode'):
+ code = e.getcode()
+ else:
+ code = e.code
+ if code == 404 and not retry_on_404:
+ return ''
+ except:
+ pass
+ boto.log.exception('Caught exception reading instance data')
+ time.sleep(2**i)
+ boto.log.error('Unable to read instance data, giving up')
+ return ''
+
+def _get_instance_metadata(url):
+ d = {}
+ data = retry_url(url)
+ if data:
+ fields = data.split('\n')
+ for field in fields:
+ if field.endswith('/'):
+ d[field[0:-1]] = _get_instance_metadata(url + field)
+ else:
+ p = field.find('=')
+ if p > 0:
+ key = field[p+1:]
+ resource = field[0:p] + '/openssh-key'
+ else:
+ key = resource = field
+ val = retry_url(url + resource)
+ p = val.find('\n')
+ if p > 0:
+ val = val.split('\n')
+ d[key] = val
+ return d
+
+def get_instance_metadata(version='latest'):
+ """
+ Returns the instance metadata as a nested Python dictionary.
+ Simple values (e.g. local_hostname, hostname, etc.) will be
+ stored as string values. Values such as ancestor-ami-ids will
+ be stored in the dict as a list of string values. More complex
+ fields such as public-keys and will be stored as nested dicts.
+ """
+ url = 'http://169.254.169.254/%s/meta-data/' % version
+ return _get_instance_metadata(url)
+
+def get_instance_userdata(version='latest', sep=None):
+ url = 'http://169.254.169.254/%s/user-data' % version
+ user_data = retry_url(url, retry_on_404=False)
+ if user_data:
+ if sep:
+ l = user_data.split(sep)
+ user_data = {}
+ for nvpair in l:
+ t = nvpair.split('=')
+ user_data[t[0].strip()] = t[1].strip()
+ return user_data
+
+ISO8601 = '%Y-%m-%dT%H:%M:%SZ'
+
+def get_ts(ts=None):
+ if not ts:
+ ts = time.gmtime()
+ return time.strftime(ISO8601, ts)
+
+def parse_ts(ts):
+ return datetime.datetime.strptime(ts, ISO8601)
+
+def find_class(module_name, class_name=None):
+ if class_name:
+ module_name = "%s.%s" % (module_name, class_name)
+ modules = module_name.split('.')
+ path = None
+ c = None
+
+ try:
+ for m in modules[1:]:
+ if c:
+ c = getattr(c, m)
+ else:
+ c = getattr(__import__(".".join(modules[0:-1])), m)
+ return c
+ except:
+ return None
+
+def update_dme(username, password, dme_id, ip_address):
+ """
+ Update your Dynamic DNS record with DNSMadeEasy.com
+ """
+ dme_url = 'https://www.dnsmadeeasy.com/servlet/updateip'
+ dme_url += '?username=%s&password=%s&id=%s&ip=%s'
+ s = urllib2.urlopen(dme_url % (username, password, dme_id, ip_address))
+ return s.read()
+
+def fetch_file(uri, file=None, username=None, password=None):
+ """
+ Fetch a file based on the URI provided. If you do not pass in a file pointer
+ a tempfile.NamedTemporaryFile, or None if the file could not be
+ retrieved is returned.
+ The URI can be either an HTTP url, or "s3://bucket_name/key_name"
+ """
+ boto.log.info('Fetching %s' % uri)
+ if file == None:
+ file = tempfile.NamedTemporaryFile()
+ try:
+ working_dir = boto.config.get("General", "working_dir")
+ if uri.startswith('s3://'):
+ bucket_name, key_name = uri[len('s3://'):].split('/', 1)
+ c = boto.connect_s3()
+ bucket = c.get_bucket(bucket_name)
+ key = bucket.get_key(key_name)
+ key.get_contents_to_file(file)
+ else:
+ if username and password:
+ passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
+ passman.add_password(None, uri, username, password)
+ authhandler = urllib2.HTTPBasicAuthHandler(passman)
+ opener = urllib2.build_opener(authhandler)
+ urllib2.install_opener(opener)
+ s = urllib2.urlopen(uri)
+ file.write(s.read())
+ file.seek(0)
+ except:
+ raise
+ boto.log.exception('Problem Retrieving file: %s' % uri)
+ file = None
+ return file
+
+class ShellCommand(object):
+
+ def __init__(self, command, wait=True):
+ self.exit_code = 0
+ self.command = command
+ self.log_fp = StringIO.StringIO()
+ self.wait = wait
+ self.run()
+
+ def run(self):
+ boto.log.info('running:%s' % self.command)
+ self.process = subprocess.Popen(self.command, shell=True, stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ if(self.wait):
+ while self.process.poll() == None:
+ time.sleep(1)
+ t = self.process.communicate()
+ self.log_fp.write(t[0])
+ self.log_fp.write(t[1])
+ boto.log.info(self.log_fp.getvalue())
+ self.exit_code = self.process.returncode
+ return self.exit_code
+
+ def setReadOnly(self, value):
+ raise AttributeError
+
+ def getStatus(self):
+ return self.exit_code
+
+ status = property(getStatus, setReadOnly, None, 'The exit code for the command')
+
+ def getOutput(self):
+ return self.log_fp.getvalue()
+
+ output = property(getOutput, setReadOnly, None, 'The STDIN and STDERR output of the command')
+
+class AuthSMTPHandler(logging.handlers.SMTPHandler):
+ """
+ This class extends the SMTPHandler in the standard Python logging module
+ to accept a username and password on the constructor and to then use those
+ credentials to authenticate with the SMTP server. To use this, you could
+ add something like this in your boto config file:
+
+ [handler_hand07]
+ class=boto.utils.AuthSMTPHandler
+ level=WARN
+ formatter=form07
+ args=('localhost', 'username', 'password', 'from@abc', ['user1@abc', 'user2@xyz'], 'Logger Subject')
+ """
+
+ def __init__(self, mailhost, username, password, fromaddr, toaddrs, subject):
+ """
+ Initialize the handler.
+
+ We have extended the constructor to accept a username/password
+ for SMTP authentication.
+ """
+ logging.handlers.SMTPHandler.__init__(self, mailhost, fromaddr, toaddrs, subject)
+ self.username = username
+ self.password = password
+
+ def emit(self, record):
+ """
+ Emit a record.
+
+ Format the record and send it to the specified addressees.
+ It would be really nice if I could add authorization to this class
+ without having to resort to cut and paste inheritance but, no.
+ """
+ try:
+ import smtplib
+ try:
+ from email.Utils import formatdate
+ except:
+ formatdate = self.date_time
+ port = self.mailport
+ if not port:
+ port = smtplib.SMTP_PORT
+ smtp = smtplib.SMTP(self.mailhost, port)
+ smtp.login(self.username, self.password)
+ msg = self.format(record)
+ msg = "From: %s\r\nTo: %s\r\nSubject: %s\r\nDate: %s\r\n\r\n%s" % (
+ self.fromaddr,
+ string.join(self.toaddrs, ","),
+ self.getSubject(record),
+ formatdate(), msg)
+ smtp.sendmail(self.fromaddr, self.toaddrs, msg)
+ smtp.quit()
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except:
+ self.handleError(record)
+
+class LRUCache(dict):
+ """A dictionary-like object that stores only a certain number of items, and
+ discards its least recently used item when full.
+
+ >>> cache = LRUCache(3)
+ >>> cache['A'] = 0
+ >>> cache['B'] = 1
+ >>> cache['C'] = 2
+ >>> len(cache)
+ 3
+
+ >>> cache['A']
+ 0
+
+ Adding new items to the cache does not increase its size. Instead, the least
+ recently used item is dropped:
+
+ >>> cache['D'] = 3
+ >>> len(cache)
+ 3
+ >>> 'B' in cache
+ False
+
+ Iterating over the cache returns the keys, starting with the most recently
+ used:
+
+ >>> for key in cache:
+ ... print key
+ D
+ A
+ C
+
+ This code is based on the LRUCache class from Genshi which is based on
+ Mighty's LRUCache from ``myghtyutils.util``, written
+ by Mike Bayer and released under the MIT license (Genshi uses the
+ BSD License). See:
+
+ http://svn.myghty.org/myghtyutils/trunk/lib/myghtyutils/util.py
+ """
+
+ class _Item(object):
+ def __init__(self, key, value):
+ self.previous = self.next = None
+ self.key = key
+ self.value = value
+ def __repr__(self):
+ return repr(self.value)
+
+ def __init__(self, capacity):
+ self._dict = dict()
+ self.capacity = capacity
+ self.head = None
+ self.tail = None
+
+ def __contains__(self, key):
+ return key in self._dict
+
+ def __iter__(self):
+ cur = self.head
+ while cur:
+ yield cur.key
+ cur = cur.next
+
+ def __len__(self):
+ return len(self._dict)
+
+ def __getitem__(self, key):
+ item = self._dict[key]
+ self._update_item(item)
+ return item.value
+
+ def __setitem__(self, key, value):
+ item = self._dict.get(key)
+ if item is None:
+ item = self._Item(key, value)
+ self._dict[key] = item
+ self._insert_item(item)
+ else:
+ item.value = value
+ self._update_item(item)
+ self._manage_size()
+
+ def __repr__(self):
+ return repr(self._dict)
+
+ def _insert_item(self, item):
+ item.previous = None
+ item.next = self.head
+ if self.head is not None:
+ self.head.previous = item
+ else:
+ self.tail = item
+ self.head = item
+ self._manage_size()
+
+ def _manage_size(self):
+ while len(self._dict) > self.capacity:
+ olditem = self._dict[self.tail.key]
+ del self._dict[self.tail.key]
+ if self.tail != self.head:
+ self.tail = self.tail.previous
+ self.tail.next = None
+ else:
+ self.head = self.tail = None
+
+ def _update_item(self, item):
+ if self.head == item:
+ return
+
+ previous = item.previous
+ previous.next = item.next
+ if item.next is not None:
+ item.next.previous = previous
+ else:
+ self.tail = previous
+
+ item.previous = None
+ item.next = self.head
+ self.head.previous = self.head = item
+
+class Password(object):
+ """
+ Password object that stores itself as SHA512 hashed.
+ """
+ def __init__(self, str=None):
+ """
+ Load the string from an initial value, this should be the raw SHA512 hashed password
+ """
+ self.str = str
+
+ def set(self, value):
+ self.str = _hashfn(value).hexdigest()
+
+ def __str__(self):
+ return str(self.str)
+
+ def __eq__(self, other):
+ if other == None:
+ return False
+ return str(_hashfn(other).hexdigest()) == str(self.str)
+
+ def __len__(self):
+ if self.str:
+ return len(self.str)
+ else:
+ return 0
+
+def notify(subject, body=None, html_body=None, to_string=None, attachments=[], append_instance_id=True):
+ if append_instance_id:
+ subject = "[%s] %s" % (boto.config.get_value("Instance", "instance-id"), subject)
+ if not to_string:
+ to_string = boto.config.get_value('Notification', 'smtp_to', None)
+ if to_string:
+ try:
+ from_string = boto.config.get_value('Notification', 'smtp_from', 'boto')
+ msg = MIMEMultipart()
+ msg['From'] = from_string
+ msg['To'] = to_string
+ msg['Date'] = formatdate(localtime=True)
+ msg['Subject'] = subject
+
+ if body:
+ msg.attach(MIMEText(body))
+
+ if html_body:
+ part = MIMEBase('text', 'html')
+ part.set_payload(html_body)
+ Encoders.encode_base64(part)
+ msg.attach(part)
+
+ for part in attachments:
+ msg.attach(part)
+
+ smtp_host = boto.config.get_value('Notification', 'smtp_host', 'localhost')
+
+ # Alternate port support
+ if boto.config.get_value("Notification", "smtp_port"):
+ server = smtplib.SMTP(smtp_host, int(boto.config.get_value("Notification", "smtp_port")))
+ else:
+ server = smtplib.SMTP(smtp_host)
+
+ # TLS support
+ if boto.config.getbool("Notification", "smtp_tls"):
+ server.ehlo()
+ server.starttls()
+ server.ehlo()
+ smtp_user = boto.config.get_value('Notification', 'smtp_user', '')
+ smtp_pass = boto.config.get_value('Notification', 'smtp_pass', '')
+ if smtp_user:
+ server.login(smtp_user, smtp_pass)
+ server.sendmail(from_string, to_string, msg.as_string())
+ server.quit()
+ except:
+ boto.log.exception('notify failed')
+
diff --git a/api/boto/vpc/__init__.py b/api/boto/vpc/__init__.py
new file mode 100644
index 0000000..80b0073
--- /dev/null
+++ b/api/boto/vpc/__init__.py
@@ -0,0 +1,478 @@
+# 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.
+
+"""
+Represents a connection to the EC2 service.
+"""
+
+import urllib
+import base64
+import boto
+from boto import config
+from boto.ec2.connection import EC2Connection
+from boto.resultset import ResultSet
+from boto.vpc.vpc import VPC
+from boto.vpc.customergateway import CustomerGateway
+from boto.vpc.vpngateway import VpnGateway, Attachment
+from boto.vpc.dhcpoptions import DhcpOptions
+from boto.vpc.subnet import Subnet
+from boto.vpc.vpnconnection import VpnConnection
+
+class VPCConnection(EC2Connection):
+
+ # VPC methods
+
+ def get_all_vpcs(self, vpc_ids=None, filters=None):
+ """
+ Retrieve information about your VPCs. You can filter results to
+ return information only about those VPCs that match your search
+ parameters. Otherwise, all VPCs associated with your account
+ are returned.
+
+ :type vpc_ids: list
+ :param vpc_ids: A list of strings with the desired VPC ID's
+
+ :type filters: list of tuples
+ :param filters: A list of tuples containing filters. Each tuple
+ consists of a filter key and a filter value.
+ Possible filter keys are:
+
+ - *state*, the state of the VPC (pending or available)
+ - *cidrBlock*, CIDR block of the VPC
+ - *dhcpOptionsId*, the ID of a set of DHCP options
+
+ :rtype: list
+ :return: A list of :class:`boto.vpc.vpc.VPC`
+ """
+ params = {}
+ if vpc_ids:
+ self.build_list_params(params, vpc_ids, 'VpcId')
+ if filters:
+ i = 1
+ for filter in filters:
+ params[('Filter.%d.Key' % i)] = filter[0]
+ params[('Filter.%d.Value.1')] = filter[1]
+ i += 1
+ return self.get_list('DescribeVpcs', params, [('item', VPC)])
+
+ def create_vpc(self, cidr_block):
+ """
+ Create a new Virtual Private Cloud.
+
+ :type cidr_block: str
+ :param cidr_block: A valid CIDR block
+
+ :rtype: The newly created VPC
+ :return: A :class:`boto.vpc.vpc.VPC` object
+ """
+ params = {'CidrBlock' : cidr_block}
+ return self.get_object('CreateVpc', params, VPC)
+
+ def delete_vpc(self, vpc_id):
+ """
+ Delete a Virtual Private Cloud.
+
+ :type vpc_id: str
+ :param vpc_id: The ID of the vpc to be deleted.
+
+ :rtype: bool
+ :return: True if successful
+ """
+ params = {'VpcId': vpc_id}
+ return self.get_status('DeleteVpc', params)
+
+ # Customer Gateways
+
+ def get_all_customer_gateways(self, customer_gateway_ids=None, filters=None):
+ """
+ Retrieve information about your CustomerGateways. You can filter results to
+ return information only about those CustomerGateways that match your search
+ parameters. Otherwise, all CustomerGateways associated with your account
+ are returned.
+
+ :type customer_gateway_ids: list
+ :param customer_gateway_ids: A list of strings with the desired CustomerGateway ID's
+
+ :type filters: list of tuples
+ :param filters: A list of tuples containing filters. Each tuple
+ consists of a filter key and a filter value.
+ Possible filter keys are:
+
+ - *state*, the state of the CustomerGateway
+ (pending,available,deleting,deleted)
+ - *type*, the type of customer gateway (ipsec.1)
+ - *ipAddress* the IP address of customer gateway's
+ internet-routable external inteface
+
+ :rtype: list
+ :return: A list of :class:`boto.vpc.customergateway.CustomerGateway`
+ """
+ params = {}
+ if customer_gateway_ids:
+ self.build_list_params(params, customer_gateway_ids, 'CustomerGatewayId')
+ if filters:
+ i = 1
+ for filter in filters:
+ params[('Filter.%d.Key' % i)] = filter[0]
+ params[('Filter.%d.Value.1')] = filter[1]
+ i += 1
+ return self.get_list('DescribeCustomerGateways', params, [('item', CustomerGateway)])
+
+ def create_customer_gateway(self, type, ip_address, bgp_asn):
+ """
+ Create a new Customer Gateway
+
+ :type type: str
+ :param type: Type of VPN Connection. Only valid valid currently is 'ipsec.1'
+
+ :type ip_address: str
+ :param ip_address: Internet-routable IP address for customer's gateway.
+ Must be a static address.
+
+ :type bgp_asn: str
+ :param bgp_asn: Customer gateway's Border Gateway Protocol (BGP)
+ Autonomous System Number (ASN)
+
+ :rtype: The newly created CustomerGateway
+ :return: A :class:`boto.vpc.customergateway.CustomerGateway` object
+ """
+ params = {'Type' : type,
+ 'IpAddress' : ip_address,
+ 'BgpAsn' : bgp_asn}
+ return self.get_object('CreateCustomerGateway', params, CustomerGateway)
+
+ def delete_customer_gateway(self, customer_gateway_id):
+ """
+ Delete a Customer Gateway.
+
+ :type customer_gateway_id: str
+ :param customer_gateway_id: The ID of the customer_gateway to be deleted.
+
+ :rtype: bool
+ :return: True if successful
+ """
+ params = {'CustomerGatewayId': customer_gateway_id}
+ return self.get_status('DeleteCustomerGateway', params)
+
+ # VPN Gateways
+
+ def get_all_vpn_gateways(self, vpn_gateway_ids=None, filters=None):
+ """
+ Retrieve information about your VpnGateways. You can filter results to
+ return information only about those VpnGateways that match your search
+ parameters. Otherwise, all VpnGateways associated with your account
+ are returned.
+
+ :type vpn_gateway_ids: list
+ :param vpn_gateway_ids: A list of strings with the desired VpnGateway ID's
+
+ :type filters: list of tuples
+ :param filters: A list of tuples containing filters. Each tuple
+ consists of a filter key and a filter value.
+ Possible filter keys are:
+
+ - *state*, the state of the VpnGateway
+ (pending,available,deleting,deleted)
+ - *type*, the type of customer gateway (ipsec.1)
+ - *availabilityZone*, the Availability zone the
+ VPN gateway is in.
+
+ :rtype: list
+ :return: A list of :class:`boto.vpc.customergateway.VpnGateway`
+ """
+ params = {}
+ if vpn_gateway_ids:
+ self.build_list_params(params, vpn_gateway_ids, 'VpnGatewayId')
+ if filters:
+ i = 1
+ for filter in filters:
+ params[('Filter.%d.Key' % i)] = filter[0]
+ params[('Filter.%d.Value.1')] = filter[1]
+ i += 1
+ return self.get_list('DescribeVpnGateways', params, [('item', VpnGateway)])
+
+ def create_vpn_gateway(self, type, availability_zone=None):
+ """
+ Create a new Vpn Gateway
+
+ :type type: str
+ :param type: Type of VPN Connection. Only valid valid currently is 'ipsec.1'
+
+ :type availability_zone: str
+ :param availability_zone: The Availability Zone where you want the VPN gateway.
+
+ :rtype: The newly created VpnGateway
+ :return: A :class:`boto.vpc.vpngateway.VpnGateway` object
+ """
+ params = {'Type' : type}
+ if availability_zone:
+ params['AvailabilityZone'] = availability_zone
+ return self.get_object('CreateVpnGateway', params, VpnGateway)
+
+ def delete_vpn_gateway(self, vpn_gateway_id):
+ """
+ Delete a Vpn Gateway.
+
+ :type vpn_gateway_id: str
+ :param vpn_gateway_id: The ID of the vpn_gateway to be deleted.
+
+ :rtype: bool
+ :return: True if successful
+ """
+ params = {'VpnGatewayId': vpn_gateway_id}
+ return self.get_status('DeleteVpnGateway', params)
+
+ def attach_vpn_gateway(self, vpn_gateway_id, vpc_id):
+ """
+ Attaches a VPN gateway to a VPC.
+
+ :type vpn_gateway_id: str
+ :param vpn_gateway_id: The ID of the vpn_gateway to attach
+
+ :type vpc_id: str
+ :param vpc_id: The ID of the VPC you want to attach the gateway to.
+
+ :rtype: An attachment
+ :return: a :class:`boto.vpc.vpngateway.Attachment`
+ """
+ params = {'VpnGatewayId': vpn_gateway_id,
+ 'VpcId' : vpc_id}
+ return self.get_object('AttachVpnGateway', params, Attachment)
+
+ # Subnets
+
+ def get_all_subnets(self, subnet_ids=None, filters=None):
+ """
+ Retrieve information about your Subnets. You can filter results to
+ return information only about those Subnets that match your search
+ parameters. Otherwise, all Subnets associated with your account
+ are returned.
+
+ :type subnet_ids: list
+ :param subnet_ids: A list of strings with the desired Subnet ID's
+
+ :type filters: list of tuples
+ :param filters: A list of tuples containing filters. Each tuple
+ consists of a filter key and a filter value.
+ Possible filter keys are:
+
+ - *state*, the state of the Subnet
+ (pending,available)
+ - *vpdId*, the ID of teh VPC the subnet is in.
+ - *cidrBlock*, CIDR block of the subnet
+ - *availabilityZone*, the Availability Zone
+ the subnet is in.
+
+
+ :rtype: list
+ :return: A list of :class:`boto.vpc.subnet.Subnet`
+ """
+ params = {}
+ if subnet_ids:
+ self.build_list_params(params, subnet_ids, 'SubnetId')
+ if filters:
+ i = 1
+ for filter in filters:
+ params[('Filter.%d.Key' % i)] = filter[0]
+ params[('Filter.%d.Value.1')] = filter[1]
+ i += 1
+ return self.get_list('DescribeSubnets', params, [('item', Subnet)])
+
+ def create_subnet(self, vpc_id, cidr_block, availability_zone=None):
+ """
+ Create a new Subnet
+
+ :type vpc_id: str
+ :param vpc_id: The ID of the VPC where you want to create the subnet.
+
+ :type cidr_block: str
+ :param cidr_block: The CIDR block you want the subnet to cover.
+
+ :type availability_zone: str
+ :param availability_zone: The AZ you want the subnet in
+
+ :rtype: The newly created Subnet
+ :return: A :class:`boto.vpc.customergateway.Subnet` object
+ """
+ params = {'VpcId' : vpc_id,
+ 'CidrBlock' : cidr_block}
+ if availability_zone:
+ params['AvailabilityZone'] = availability_zone
+ return self.get_object('CreateSubnet', params, Subnet)
+
+ def delete_subnet(self, subnet_id):
+ """
+ Delete a subnet.
+
+ :type subnet_id: str
+ :param subnet_id: The ID of the subnet to be deleted.
+
+ :rtype: bool
+ :return: True if successful
+ """
+ params = {'SubnetId': subnet_id}
+ return self.get_status('DeleteSubnet', params)
+
+
+ # DHCP Options
+
+ def get_all_dhcp_options(self, dhcp_options_ids=None):
+ """
+ Retrieve information about your DhcpOptions.
+
+ :type dhcp_options_ids: list
+ :param dhcp_options_ids: A list of strings with the desired DhcpOption ID's
+
+ :rtype: list
+ :return: A list of :class:`boto.vpc.dhcpoptions.DhcpOptions`
+ """
+ params = {}
+ if dhcp_options_ids:
+ self.build_list_params(params, dhcp_options_ids, 'DhcpOptionsId')
+ return self.get_list('DescribeDhcpOptions', params, [('item', DhcpOptions)])
+
+ def create_dhcp_options(self, vpc_id, cidr_block, availability_zone=None):
+ """
+ Create a new DhcpOption
+
+ :type vpc_id: str
+ :param vpc_id: The ID of the VPC where you want to create the subnet.
+
+ :type cidr_block: str
+ :param cidr_block: The CIDR block you want the subnet to cover.
+
+ :type availability_zone: str
+ :param availability_zone: The AZ you want the subnet in
+
+ :rtype: The newly created DhcpOption
+ :return: A :class:`boto.vpc.customergateway.DhcpOption` object
+ """
+ params = {'VpcId' : vpc_id,
+ 'CidrBlock' : cidr_block}
+ if availability_zone:
+ params['AvailabilityZone'] = availability_zone
+ return self.get_object('CreateDhcpOption', params, DhcpOptions)
+
+ def delete_dhcp_options(self, dhcp_options_id):
+ """
+ Delete a DHCP Options
+
+ :type dhcp_options_id: str
+ :param dhcp_options_id: The ID of the DHCP Options to be deleted.
+
+ :rtype: bool
+ :return: True if successful
+ """
+ params = {'DhcpOptionsId': subnet_id}
+ return self.get_status('DeleteDhcpOptions', params)
+
+ def associate_dhcp_options(self, dhcp_options_id, vpc_id):
+ """
+ Associate a set of Dhcp Options with a VPC.
+
+ :type dhcp_options_id: str
+ :param dhcp_options_id: The ID of the Dhcp Options
+
+ :type vpc_id: str
+ :param vpc_id: The ID of the VPC.
+
+ :rtype: bool
+ :return: True if successful
+ """
+ params = {'DhcpOptionsId': dhcp_option,
+ 'VpcId' : vpc_id}
+ return self.get_status('AssociateDhcpOptions', params)
+
+ # VPN Connection
+
+ def get_all_vpn_connections(self, vpn_connection_ids=None, filters=None):
+ """
+ Retrieve information about your VPN_CONNECTIONs. You can filter results to
+ return information only about those VPN_CONNECTIONs that match your search
+ parameters. Otherwise, all VPN_CONNECTIONs associated with your account
+ are returned.
+
+ :type vpn_connection_ids: list
+ :param vpn_connection_ids: A list of strings with the desired VPN_CONNECTION ID's
+
+ :type filters: list of tuples
+ :param filters: A list of tuples containing filters. Each tuple
+ consists of a filter key and a filter value.
+ Possible filter keys are:
+
+ - *state*, the state of the VPN_CONNECTION
+ pending,available,deleting,deleted
+ - *type*, the type of connection, currently 'ipsec.1'
+ - *customerGatewayId*, the ID of the customer gateway
+ associated with the VPN
+ - *vpnGatewayId*, the ID of the VPN gateway associated
+ with the VPN connection
+
+ :rtype: list
+ :return: A list of :class:`boto.vpn_connection.vpnconnection.VpnConnection`
+ """
+ params = {}
+ if vpn_connection_ids:
+ self.build_list_params(params, vpn_connection_ids, 'Vpn_ConnectionId')
+ if filters:
+ i = 1
+ for filter in filters:
+ params[('Filter.%d.Key' % i)] = filter[0]
+ params[('Filter.%d.Value.1')] = filter[1]
+ i += 1
+ return self.get_list('DescribeVpnConnections', params, [('item', VPNConnection)])
+
+ def create_vpn_connection(self, type, customer_gateway_id, vpn_gateway_id):
+ """
+ Create a new VPN Connection.
+
+ :type type: str
+ :param type: The type of VPN Connection. Currently only 'ipsec.1'
+ is supported
+
+ :type customer_gateway_id: str
+ :param customer_gateway_id: The ID of the customer gateway.
+
+ :type vpn_gateway_id: str
+ :param vpn_gateway_id: The ID of the VPN gateway.
+
+ :rtype: The newly created VpnConnection
+ :return: A :class:`boto.vpc.vpnconnection.VpnConnection` object
+ """
+ params = {'Type' : type,
+ 'CustomerGatewayId' : customer_gateway_id,
+ 'VpnGatewayId' : vpn_gateway_id}
+ return self.get_object('CreateVpnConnection', params, VpnConnection)
+
+ def delete_vpn_connection(self, vpn_connection_id):
+ """
+ Delete a VPN Connection.
+
+ :type vpn_connection_id: str
+ :param vpn_connection_id: The ID of the vpn_connection to be deleted.
+
+ :rtype: bool
+ :return: True if successful
+ """
+ params = {'VpnConnectionId': vpn_connection_id}
+ return self.get_status('DeleteVpnConnection', params)
+
+
diff --git a/api/boto/vpc/customergateway.py b/api/boto/vpc/customergateway.py
new file mode 100644
index 0000000..c50a616
--- /dev/null
+++ b/api/boto/vpc/customergateway.py
@@ -0,0 +1,54 @@
+# 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.
+
+"""
+Represents a Customer Gateway
+"""
+
+from boto.ec2.ec2object import EC2Object
+
+class CustomerGateway(EC2Object):
+
+ def __init__(self, connection=None):
+ EC2Object.__init__(self, connection)
+ self.id = None
+ self.type = None
+ self.state = None
+ self.ip_address = None
+ self.bgp_asn = None
+
+ def __repr__(self):
+ return 'CustomerGateway:%s' % self.id
+
+ def endElement(self, name, value, connection):
+ if name == 'customerGatewayId':
+ self.id = value
+ elif name == 'ipAddress':
+ self.ip_address = value
+ elif name == 'type':
+ self.type = value
+ elif name == 'state':
+ self.state = value
+ elif name == 'bgpAsn':
+ self.bgp_asn = value
+ else:
+ setattr(self, name, value)
+
diff --git a/api/boto/vpc/dhcpoptions.py b/api/boto/vpc/dhcpoptions.py
new file mode 100644
index 0000000..4fce7dc
--- /dev/null
+++ b/api/boto/vpc/dhcpoptions.py
@@ -0,0 +1,69 @@
+# 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.
+
+"""
+Represents a DHCP Options set
+"""
+
+from boto.ec2.ec2object import EC2Object
+
+class DhcpValueSet(list):
+
+ def startElement(self, name, attrs, connection):
+ pass
+
+ def endElement(self, name, value, connection):
+ if name == 'value':
+ self.append(value)
+
+class DhcpConfigSet(dict):
+
+ def startElement(self, name, attrs, connection):
+ if name == 'valueSet':
+ if not self.has_key(self._name):
+ self[self._name] = DhcpValueSet()
+ return self[self._name]
+
+ def endElement(self, name, value, connection):
+ if name == 'key':
+ self._name = value
+
+class DhcpOptions(EC2Object):
+
+ def __init__(self, connection=None):
+ EC2Object.__init__(self, connection)
+ self.id = None
+ self.options = None
+
+ def __repr__(self):
+ return 'DhcpOptions:%s' % self.id
+
+ def startElement(self, name, attrs, connection):
+ if name == 'dhcpConfigurationSet':
+ self.options = DhcpConfigSet()
+ return self.options
+
+ def endElement(self, name, value, connection):
+ if name == 'dhcpOptionsId':
+ self.id = value
+ else:
+ setattr(self, name, value)
+
diff --git a/api/boto/vpc/subnet.py b/api/boto/vpc/subnet.py
new file mode 100644
index 0000000..de8a959
--- /dev/null
+++ b/api/boto/vpc/subnet.py
@@ -0,0 +1,54 @@
+# 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.
+
+"""
+Represents a Subnet
+"""
+
+from boto.ec2.ec2object import EC2Object
+
+class Subnet(EC2Object):
+
+ def __init__(self, connection=None):
+ EC2Object.__init__(self, connection)
+ self.id = None
+ self.state = None
+ self.cidr_block = None
+ self.available_ip_address_count = 0
+ self.availability_zone = None
+
+ def __repr__(self):
+ return 'Subnet:%s' % self.id
+
+ def endElement(self, name, value, connection):
+ if name == 'subnetId':
+ self.id = value
+ elif name == 'state':
+ self.state = value
+ elif name == 'cidrBlock':
+ self.cidr_block = value
+ elif name == 'availableIpAddressCount':
+ self.available_ip_address_count = int(value)
+ elif name == 'availabilityZone':
+ self.availability_zone = value
+ else:
+ setattr(self, name, value)
+
diff --git a/api/boto/vpc/vpc.py b/api/boto/vpc/vpc.py
new file mode 100644
index 0000000..152cff3
--- /dev/null
+++ b/api/boto/vpc/vpc.py
@@ -0,0 +1,54 @@
+# 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.
+
+"""
+Represents a Virtual Private Cloud.
+"""
+
+from boto.ec2.ec2object import EC2Object
+
+class VPC(EC2Object):
+
+ def __init__(self, connection=None):
+ EC2Object.__init__(self, connection)
+ self.id = None
+ self.dhcp_options_id = None
+ self.state = None
+ self.cidr_block = None
+
+ def __repr__(self):
+ return 'VPC:%s' % self.id
+
+ def endElement(self, name, value, connection):
+ if name == 'vpcId':
+ self.id = value
+ elif name == 'dhcpOptionsId':
+ self.dhcp_options_id = value
+ elif name == 'state':
+ self.state = value
+ elif name == 'cidrBlock':
+ self.cidr_block = value
+ else:
+ setattr(self, name, value)
+
+ def delete(self):
+ return self.connection.delete_vpc(self.id)
+
diff --git a/api/boto/vpc/vpnconnection.py b/api/boto/vpc/vpnconnection.py
new file mode 100644
index 0000000..42739d9
--- /dev/null
+++ b/api/boto/vpc/vpnconnection.py
@@ -0,0 +1,60 @@
+# 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.
+
+"""
+Represents a VPN Connectionn
+"""
+
+from boto.ec2.ec2object import EC2Object
+
+class VpnConnection(EC2Object):
+
+ def __init__(self, connection=None):
+ EC2Object.__init__(self, connection)
+ self.id = None
+ self.state = None
+ self.customer_gateway_configuration = None
+ self.type = None
+ self.customer_gateway_id = None
+ self.vpn_gateway_id = Nonen
+
+ def __repr__(self):
+ return 'VpnConnection:%s' % self.id
+
+ def endElement(self, name, value, connection):
+ if name == 'vpnConnectionId':
+ self.id = value
+ elif name == 'state':
+ self.state = value
+ elif name == 'CustomerGatewayConfiguration':
+ self.customer_gateway_configuration = value
+ elif name == 'type':
+ self.type = value
+ elif name == 'customerGatewayId':
+ self.customer_gateway_id = value
+ elif name == 'vpnGatewayId':
+ self.vpn_gateway_id = value
+ else:
+ setattr(self, name, value)
+
+ def delete(self):
+ return self.connection.delete_vpn_connection(self.id)
+
diff --git a/api/boto/vpc/vpngateway.py b/api/boto/vpc/vpngateway.py
new file mode 100644
index 0000000..0fa0a9e
--- /dev/null
+++ b/api/boto/vpc/vpngateway.py
@@ -0,0 +1,80 @@
+# 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.
+
+"""
+Represents a Vpn Gateway
+"""
+
+from boto.ec2.ec2object import EC2Object
+
+class Attachment(object):
+
+ def __init__(self, connection=None):
+ self.vpc_id = None
+ self.state = None
+
+ def startElement(self, name, attrs, connection):
+ pass
+
+ def endElement(self, name, value, connection):
+ if name == 'vpcId':
+ self.vpc_id = value
+ elif name == 'state':
+ self.state = value
+ else:
+ setattr(self, name, value)
+
+class VpnGateway(EC2Object):
+
+ def __init__(self, connection=None):
+ EC2Object.__init__(self, connection)
+ self.id = None
+ self.type = None
+ self.state = None
+ self.availability_zone = None
+ self.attachments = []
+
+ def __repr__(self):
+ return 'VpnGateway:%s' % self.id
+
+ def startElement(self, name, attrs, connection):
+ if name == 'item':
+ att = Attachment()
+ self.attachments.append(att)
+ return att
+
+ def endElement(self, name, value, connection):
+ if name == 'vpnGatewayId':
+ self.id = value
+ elif name == 'type':
+ self.type = value
+ elif name == 'state':
+ self.state = value
+ elif name == 'availabilityZone':
+ self.availability_zone = value
+ elif name == 'attachments':
+ pass
+ else:
+ setattr(self, name, value)
+
+ def attach(self, vpc_id):
+ return self.connection.attach_vpn_gateway(self.id, vpc_id)
+
diff --git a/api/dokillapi.sh b/api/dokillapi.sh
new file mode 100755
index 0000000..880cf85
--- /dev/null
+++ b/api/dokillapi.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+TEE="tee -a /data/logs/api.log"
+ROTATELOG="/usr/sbin/rotatelogs -l -f /data/logs/api-rot/api.log.%Y-%m-%d_%H.%M.%S 200M"
+
+echo "######################## KILL API "`date`" ##########################" | $TEE | $ROTATELOG
+
+kill -9 `cat pid`
diff --git a/api/dostartapi.sh b/api/dostartapi.sh
new file mode 100755
index 0000000..babe4b2
--- /dev/null
+++ b/api/dostartapi.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+TEE="tee -a /data/logs/api.log"
+ROTATELOG="/usr/sbin/rotatelogs -l -f /data/logs/api-rot/api.log.%Y-%m-%d_%H.%M.%S 200M"
+ERRORLOG="/data/logs/apierrors.log"
+
+echo | $TEE | $ROTATELOG
+echo "######################## START API "`date`" ##########################" | $TEE | $ROTATELOG
+echo "######################## START API "`date`" ##########################" > $ERRORLOG
+
+export PYTHONPATH=.:..
+
+nohup uwsgi-python2.6 -C -s /var/nginx/api-uwsgi.sock -i -M -w wsgi -z 30 -p 150 -l 50 -L -R 10000 -b 8192 --no-orphans --pidfile pid 2>$ERRORLOG | $TEE | $ROTATELOG &
+
+
diff --git a/api/keepalive.sh b/api/keepalive.sh
new file mode 100755
index 0000000..f596c5c
--- /dev/null
+++ b/api/keepalive.sh
@@ -0,0 +1,16 @@
+if [ -f "DONTSTART" ]; then
+ exit 0;
+fi
+
+count=`ps aux|grep api-uwsgi.sock | grep -v grep | wc -l`
+response=`curl http://api.indextank.com/ 2> /dev/null`
+
+if [ "$response" != "\"IndexTank API. Please refer to the documentation: http://indextank.com/documentation/api\"" ]; then
+ date
+ echo Restarting webapp. Process count is $count, response was:
+ echo "$response"
+ ./dokillapi.sh
+ sleep 5s
+ ./dostartapi.sh
+fi
+
diff --git a/api/kill_webapp.sh b/api/kill_webapp.sh
new file mode 100755
index 0000000..5174d9d
--- /dev/null
+++ b/api/kill_webapp.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+touch DONTSTART
+./dokillapi.sh
diff --git a/api/lib/__init__.py b/api/lib/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/api/lib/authorizenet.py b/api/lib/authorizenet.py
new file mode 100644
index 0000000..00bcd6c
--- /dev/null
+++ b/api/lib/authorizenet.py
@@ -0,0 +1,153 @@
+from xml.dom.minidom import Document, parseString
+import httplib
+import urlparse
+
+
+class AuthorizeNet:
+ """
+ Basic client for Authorize.net's Automated Recurring Billing (ARB) service
+ """
+
+ def __init__(self):
+ from django.conf import settings
+ f = open("authorize.settings.prod") if not settings.DEBUG else open("authorize.settings.debug")
+ for line in f:
+ line = line.strip()
+ if len(line) > 0 and not line.startswith('#'):
+ parts = line.split('=',1)
+ var = parts[0].strip()
+ val = parts[1].strip()
+ if var in ['host_url','api_login_id','transaction_key']:
+ cmd = 'self.%s = %s' % (var,val)
+ exec(cmd)
+
+ def subscription_create(self, refId, name, length, unit, startDate, totalOccurrences, trialOccurrences,
+ amount, trialAmount, cardNumber, expirationDate, firstName, lastName, company,
+ address, city, state, zip, country):
+ doc,root = self._new_doc("ARBCreateSubscriptionRequest")
+ self._add_text_node(doc, root, 'refId', refId)
+ subscription = self._add_node(doc, root, 'subscription')
+ self._add_text_node(doc, subscription, 'name', name)
+ paymentSchedule = self._add_node(doc, subscription, 'paymentSchedule')
+ interval = self._add_node(doc, paymentSchedule, 'interval')
+ self._add_text_node(doc, interval, 'length', length)
+ self._add_text_node(doc, interval, 'unit', unit)
+ self._add_text_node(doc, paymentSchedule, 'startDate', startDate)
+ self._add_text_node(doc, paymentSchedule, 'totalOccurrences', totalOccurrences)
+ self._add_text_node(doc, paymentSchedule, 'trialOccurrences', trialOccurrences)
+ self._add_text_node(doc, subscription, 'amount', amount)
+ self._add_text_node(doc, subscription, 'trialAmount', trialAmount)
+ payment = self._add_node(doc, subscription, 'payment')
+ creditcard = self._add_node(doc, payment, 'creditCard')
+ self._add_text_node(doc, creditcard, 'cardNumber', cardNumber)
+ self._add_text_node(doc, creditcard, 'expirationDate', expirationDate)
+ billto = self._add_node(doc, subscription, 'billTo')
+ self._add_text_node(doc, billto, 'firstName', firstName)
+ self._add_text_node(doc, billto, 'lastName', lastName)
+ self._add_text_node(doc, billto, 'company', company)
+ self._add_text_node(doc, billto, 'address', address)
+ self._add_text_node(doc, billto, 'city', city)
+ self._add_text_node(doc, billto, 'state', state)
+ self._add_text_node(doc, billto, 'zip', zip)
+ self._add_text_node(doc, billto, 'country', country)
+ res = self._send_xml(doc.toxml())
+ subscriptionId = res.getElementsByTagName('subscriptionId')[0].childNodes[0].nodeValue
+ return subscriptionId
+
+
+ def subscription_update(self, refId, subscriptionId, name, startDate, totalOccurrences, trialOccurrences,
+ amount, trialAmount, cardNumber, expirationDate, firstName, lastName, company,
+ address, city, state, zip, country):
+ doc,root = self._new_doc("ARBUpdateSubscriptionRequest")
+ self._add_text_node(doc, root, 'refId', refId)
+ self._add_text_node(doc, root, 'subscriptionId', subscriptionId)
+ subscription = self._add_node(doc, root, 'subscription')
+ if name:
+ self._add_text_node(doc, subscription, 'name', name)
+ if startDate or totalOccurrences or trialOccurrences:
+ paymentSchedule = self._add_node(doc, subscription, 'paymentSchedule')
+ if startDate:
+ self._add_text_node(doc, paymentSchedule, 'startDate', startDate)
+ if totalOccurrences:
+ self._add_text_node(doc, paymentSchedule, 'totalOccurrences', totalOccurrences)
+ if trialOccurrences:
+ self._add_text_node(doc, paymentSchedule, 'trialOccurrences', trialOccurrences)
+ if amount:
+ self._add_text_node(doc, subscription, 'amount', amount)
+ if trialAmount:
+ self._add_text_node(doc, subscription, 'trialAmount', trialAmount)
+ if cardNumber and expirationDate:
+ payment = self._add_node(doc, subscription, 'payment')
+ creditcard = self._add_node(doc, payment, 'creditCard')
+ self._add_text_node(doc, creditcard, 'cardNumber', cardNumber)
+ self._add_text_node(doc, creditcard, 'expirationDate', expirationDate)
+ if firstName and lastName:
+ billto = self._add_node(doc, subscription, 'billTo')
+ self._add_text_node(doc, billto, 'firstName', firstName)
+ self._add_text_node(doc, billto, 'lastName', lastName)
+ if company:
+ self._add_text_node(doc, billto, 'company', company)
+ if address and city and state and zip and country:
+ self._add_text_node(doc, billto, 'address', address)
+ self._add_text_node(doc, billto, 'city', city)
+ self._add_text_node(doc, billto, 'state', state)
+ self._add_text_node(doc, billto, 'zip', zip)
+ self._add_text_node(doc, billto, 'country', country)
+ self._send_xml(doc.toxml())
+
+
+ def subscription_cancel(self, refId, subscriptionId):
+ doc,root = self._new_doc("ARBCancelSubscriptionRequest")
+ self._add_text_node(doc, root, 'refId', refId)
+ self._add_text_node(doc, root, 'subscriptionId', subscriptionId)
+ self._send_xml(doc.toxml())
+
+
+ def _add_node(self, doc, node, name):
+ elem = doc.createElement(name)
+ node.appendChild(elem)
+ return elem
+
+ def _add_text_node(self, doc, node, name, text):
+ elem = self._add_node(doc, node, name)
+ text_node = doc.createTextNode(text)
+ elem.appendChild(text_node)
+ return elem
+
+ def _new_doc(self, operation):
+ doc = Document()
+ root = doc.createElement(operation)
+ root.setAttribute('xmlns','AnetApi/xml/v1/schema/AnetApiSchema.xsd')
+ doc.appendChild(root)
+ auth = self._add_node(doc, root, 'merchantAuthentication')
+ self._add_text_node(doc, auth, 'name', self.api_login_id)
+ self._add_text_node(doc, auth, 'transactionKey', self.transaction_key)
+ return doc, root
+
+ def _send_xml(self, xml):
+ splits = urlparse.urlsplit(self.host_url)
+ print "connection.request('POST', "+self.host_url+", xml, {'Content-Type':'text/xml'})"
+ print "xml: "+xml
+ connection = httplib.HTTPSConnection(splits.hostname)
+ connection.request('POST', self.host_url, xml, {'Content-Type':'text/xml'})
+ response = connection.getresponse()
+ response.body = response.read()
+ connection.close()
+ print "resp: "+response.body
+ res = parseString(response.body)
+ ok = res.getElementsByTagName('resultCode')[0].childNodes[0].nodeValue == "Ok"
+ if not ok:
+ code = res.getElementsByTagName('message')[0].childNodes[0].childNodes[0].nodeValue
+ msg = res.getElementsByTagName('message')[0].childNodes[1].childNodes[0].nodeValue + " (%s)"%code
+ raise BillingException(msg,code)
+ return res
+
+
+class BillingException(Exception):
+ def __init__(self, msg, code):
+ self.msg = msg
+ self.code = code
+ def __str__(self):
+ return repr(self.msg)
+
+
diff --git a/api/lib/encoder.py b/api/lib/encoder.py
new file mode 100644
index 0000000..f6bb4dd
--- /dev/null
+++ b/api/lib/encoder.py
@@ -0,0 +1,74 @@
+# Short URL Generator
+
+#DEFAULT_ALPHABET = 'JedR8LNFY2j6MrhkBSADUyfP5amuH9xQCX4VqbgpsGtnW7vc3TwKE'
+DEFAULT_ALPHABET = 'ed82j6rhkyf5amu9x4qbgpstn7vc3w1ioz'
+DEFAULT_BLOCK_SIZE = 22
+
+class Encoder(object):
+ def __init__(self, alphabet=DEFAULT_ALPHABET, block_size=DEFAULT_BLOCK_SIZE):
+ self.alphabet = alphabet
+ self.block_size = block_size
+ self.mask = (1 << block_size) - 1
+ self.mapping = range(block_size)
+ self.mapping.reverse()
+ def encode_url(self, n, min_length=0):
+ return self.enbase(self.encode(n), min_length)
+ def decode_url(self, n):
+ return self.decode(self.debase(n))
+ def encode(self, n):
+ return (n & ~self.mask) | self._encode(n & self.mask)
+ def _encode(self, n):
+ result = 0
+ for i, b in enumerate(self.mapping):
+ if n & (1 << i):
+ result |= (1 << b)
+ return result
+ def decode(self, n):
+ return (n & ~self.mask) | self._decode(n & self.mask)
+ def _decode(self, n):
+ result = 0
+ for i, b in enumerate(self.mapping):
+ if n & (1 << b):
+ result |= (1 << i)
+ return result
+ def enbase(self, x, min_length=0):
+ result = self._enbase(x)
+ padding = self.alphabet[0] * (min_length - len(result))
+ return '%s%s' % (padding, result)
+ def _enbase(self, x):
+ n = len(self.alphabet)
+ if x < n:
+ return self.alphabet[x]
+ return self.enbase(x/n) + self.alphabet[x%n]
+ def debase(self, x):
+ n = len(self.alphabet)
+ result = 0
+ for i, c in enumerate(reversed(x)):
+ result += self.alphabet.index(c) * (n**i)
+ return result
+
+DEFAULT_ENCODER = Encoder()
+
+def encode(n):
+ return DEFAULT_ENCODER.encode(n)
+
+def decode(n):
+ return DEFAULT_ENCODER.decode(n)
+
+def enbase(n, min_length=0):
+ return DEFAULT_ENCODER.enbase(n, min_length)
+
+def debase(n):
+ return DEFAULT_ENCODER.debase(n)
+
+def encode_url(n, min_length=0):
+ return DEFAULT_ENCODER.encode_url(n, min_length)
+
+def decode_url(n):
+ return DEFAULT_ENCODER.decode_url(n)
+
+def to_key(n):
+ return enbase(encode(n))
+
+def from_key(n):
+ return decode(debase(n))
diff --git a/api/lib/error_logging.py b/api/lib/error_logging.py
new file mode 100644
index 0000000..dbf927b
--- /dev/null
+++ b/api/lib/error_logging.py
@@ -0,0 +1,13 @@
+import traceback
+from lib import flaptor_logging
+from django.http import HttpResponse
+
+logger = flaptor_logging.get_logger('error_logging')
+
+class ViewErrorLoggingMiddleware:
+
+ def process_view(self, request, view_func, view_args, view_kwargs):
+ self.view_name = view_func.__name__
+ def process_exception(self, request, exception):
+ logger.error('UNEXPECTED EXCEPTION in view "%s". Exception is: %s', self.view_name, repr(traceback.print_exc()))
+ return HttpResponse('{"status":"ERROR", "message":"Unexpected error."}')
diff --git a/api/lib/exceptions.py b/api/lib/exceptions.py
new file mode 100644
index 0000000..c6ff0a7
--- /dev/null
+++ b/api/lib/exceptions.py
@@ -0,0 +1,8 @@
+
+
+class CloudException(Exception):
+ pass
+
+class NoIndexerException(CloudException):
+ pass
+
diff --git a/api/lib/flaptor_logging.py b/api/lib/flaptor_logging.py
new file mode 100644
index 0000000..1af893c
--- /dev/null
+++ b/api/lib/flaptor_logging.py
@@ -0,0 +1,100 @@
+import logging as pylogging
+from logging import config
+import os
+
+usingNativeLogger = True
+
+__loggers = {}
+
+
+
+def get_logger(name, force_new=False):
+ '''Get the Logger instance for a given name'''
+ global __loggers
+ if __loggers is None:
+ __loggers = {}
+ if force_new:
+ return pylogging.getLogger(name)
+ if not __loggers.has_key(name):
+ __loggers[name] = pylogging.getLogger(name)
+ return __loggers[name]
+
+class SpecialFormatter(pylogging.Formatter):
+ BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
+ RESET_SEQ = "\033[0m"
+ COLOR_SEQ = "\033[37;4%dm"
+ PIDCOLOR_SEQ = "\033[1;3%dm"
+ BOLD_SEQ = "\033[1m"
+ COLORS = {
+ 'WARN': YELLOW,
+ 'INFO': GREEN,
+ 'DEBU': BLUE,
+ 'CRIT': RED,
+ 'ERRO': RED
+ }
+
+ def __init__(self, *args, **kwargs):
+ pylogging.Formatter.__init__(self, *args, **kwargs)
+ def format(self, record):
+ if not hasattr(record, 'prefix'): record.prefix = ''
+ if not hasattr(record, 'suffix'): record.suffix = ''
+ if not hasattr(record, 'compname'): record.compname = ''
+ record.pid = os.getpid()
+
+ record.levelname = record.levelname[:4]
+
+ r = pylogging.Formatter.format(self, record)
+ if record.levelname in SpecialFormatter.COLORS:
+ levelcolor = SpecialFormatter.COLOR_SEQ % (SpecialFormatter.COLORS[record.levelname])
+ r = r.replace('$LEVELCOLOR', levelcolor)
+ r = r.replace('$RESET', SpecialFormatter.RESET_SEQ)
+ else:
+ r = r.replace('$COLOR', '')
+ r = r.replace('$RESET', '')
+ pidcolor = SpecialFormatter.COLOR_SEQ % (1 + (record.pid % 5))
+ r = r.replace('$PIDCOLOR', pidcolor)
+ r = r.replace('$BOLD', SpecialFormatter.BOLD_SEQ)
+ return r
+
+pylogging.SpecialFormatter = SpecialFormatter
+
+if usingNativeLogger:
+ try:
+ config.fileConfig('logging.conf')
+ except Exception, e:
+ print e
+
+#class NativePythonLogger:
+# def __init__(self, name):
+# '''Creates a new Logger for the given name.
+# Do not call this method directly, instead use
+# get_logger(name) to get the appropriate instance'''
+# self.name = name
+# self.__logger = pylogging.getLogger(name)
+# #self.updateLevel(5)
+#
+# def updateLevel(self, level):
+# self.__level = level
+# if level == 1:
+# self.__logger.setLevel(pylogging.CRITICAL)
+# elif level == 2:
+# self.__logger.setLevel(pylogging.INFO)
+# elif level == 3:
+# self.__logger.setLevel(pylogging.WARNING)
+# elif level == 4:
+# self.__logger.setLevel(pylogging.INFO)
+# elif level == 5:
+# self.__logger.setLevel(pylogging.DEBUG)
+#
+# def debug(self, format_str, *values):
+# self.__logger.debug(format_str, *values)
+# def info(self, format_str, *values):
+# self.__logger.info(format_str, *values)
+# def warn(self, format_str, *values):
+# self.__logger.warn(format_str, *values)
+# def error(self, format_str, *values):
+# self.__logger.error(format_str, *values)
+# def exception(self, format_str, *values):
+# self.__logger.exception(format_str, *values)
+# def fatal(self, format_str, *values):
+# self.__logger.critical(format_str, *values)
diff --git a/api/lib/mail.py b/api/lib/mail.py
new file mode 100644
index 0000000..b1d3a08
--- /dev/null
+++ b/api/lib/mail.py
@@ -0,0 +1,101 @@
+from django.core.mail import send_mail
+
+try:
+ ENV=open('/data/env.name').readline().strip()
+except Exception:
+ ENV='PROD+EXC'
+
+def _no_fail(method, *args, **kwargs):
+ def decorated(*args, **kwargs):
+ try:
+ return method(*args, **kwargs)
+ except Exception, e:
+ print e
+ return
+ return decorated
+
+
+
+@_no_fail
+def report_payment_data(account):
+ activity_report = 'An Account has entered payment data\n'
+ activity_report += '---------------------------\n'
+ activity_report += 'Plan: ' + account.package.name + '\n'
+ activity_report += 'Email: ' + account.user.email + '\n'
+ activity_report += 'API KEY: ' + account.apikey + ''
+
+ report_activity('Payment Data for ' + account.package.name + ' Account (' + account.user.email + ')', activity_report)
+
+@_no_fail
+def report_new_account(account):
+ activity_report = 'A new Account was created\n'
+ activity_report += '---------------------------\n'
+ activity_report += 'Plan: ' + account.package.name + '\n'
+ activity_report += 'Email: ' + account.user.email + '\n'
+ activity_report += 'API KEY: ' + account.apikey + ''
+
+ report_activity('New ' + account.package.name + ' Account (' + account.user.email + ')', activity_report)
+
+@_no_fail
+def report_new_index(index):
+ activity_report = 'A new Index was created\n'
+ activity_report += '---------------------------\n'
+ activity_report += 'Plan: ' + index.account.package.name + '\n'
+ activity_report += 'User Email: ' + index.account.user.email + '\n'
+ activity_report += 'Index Name: ' + index.name + ''
+
+ report_activity('Index activity (' + index.code + ')', activity_report, 'l')
+
+@_no_fail
+def report_new_deploy(deploy):
+ activity_report = 'A new Deploy is now controllable\n'
+ activity_report += '---------------------------\n'
+ activity_report += 'Plan: ' + deploy.index.account.package.name + '\n'
+ activity_report += 'User Email: ' + deploy.index.account.user.email + '\n'
+ activity_report += 'Index Name: ' + deploy.index.name + '\n'
+ activity_report += 'Worker: #' + str(deploy.worker.id) + '\n'
+ activity_report += ('Deploy: %r' % deploy) + '\n'
+ activity_report += ('Container Index: %r' % deploy.index) + '\n'
+
+ report_activity('Index activity (' + deploy.index.code + ')', activity_report, 'l')
+
+@_no_fail
+def report_delete_index(index):
+ activity_report = 'An Index has been deleted\n'
+ activity_report += '---------------------------\n'
+ activity_report += 'Plan: ' + index.account.package.name + '\n'
+ activity_report += 'User Email: ' + index.account.user.email + '\n'
+ activity_report += 'Index Name: ' + index.name + '\n'
+
+ report_activity('Index activity (' + index.code + ')', activity_report, 'l')
+
+@_no_fail
+def report_new_worker(worker):
+ activity_report = 'A new Worker was created\n'
+ activity_report += '---------------------------\n'
+ activity_report += repr(worker)
+
+ report_activity('New Worker (%d)' % (worker.pk), activity_report, 't')
+
+@_no_fail
+def report_automatic_redeploy(deploy, initial_xmx, new_xmx):
+ activity_report = 'Automatic redeploy.\n'
+ activity_report += '---------------------------\n'
+ activity_report += 'initial xmx value: %d\n' % (initial_xmx)
+ activity_report += 'new xmx value: %d\n' % (new_xmx)
+ activity_report += repr(deploy)
+
+ report_activity('Automatic redeploy', activity_report, 't')
+
+@_no_fail
+def report_activity(subject, body, type='b'):
+ if type == 'b':
+ mail_to = 'activity@indextank.com'
+ elif type == 't':
+ mail_to = 'activitytech@indextank.com'
+ elif type == 'l':
+ mail_to = 'lowactivity@indextank.com'
+ else:
+ raise Exception('Wrong report type')
+
+ send_mail(ENV + ' - ' + subject, body, 'IndexTank Activity ', [mail_to], fail_silently=False)
diff --git a/api/lib/monitor.py b/api/lib/monitor.py
new file mode 100644
index 0000000..3fe9818
--- /dev/null
+++ b/api/lib/monitor.py
@@ -0,0 +1,148 @@
+
+from threading import Thread
+from traceback import format_tb
+import time, datetime
+import sys
+import shelve
+
+from django.core.mail import send_mail
+
+from lib import flaptor_logging
+
+try:
+ ENV=open('/data/env.name').readline().strip()
+except Exception:
+ ENV='PROD+EXC'
+
+#helper functions
+def is_prod():
+ return ENV == 'PROD' or ENV == 'QoS_Monitor'
+
+def env_name():
+ if ENV == 'PROD':
+ return 'PRODUCTION'
+ elif ENV == 'QoS_Monitor':
+ return 'QoS_Monitor'
+ else:
+ return ENV
+
+class Monitor(Thread):
+ def __init__(self, pagerduty_email='api-monitor@flaptor.pagerduty.com'):
+ super(Monitor, self).__init__()
+ self.name = self.__class__.__name__
+ self.statuses = shelve.open('/data/monitor-%s.shelf' % self.name)
+ self.logger = flaptor_logging.get_logger(self.name)
+ self.failure_threshold = 1
+ self.fatal_failure_threshold = 0
+ self.severity = 'WARNING'
+ self.title_template = '%s::%s: [%s] %s'
+ self.pagerduty_email = pagerduty_email
+
+ def iterable(self):
+ return [None]
+
+ def run(self):
+ self.step = 1
+ while True:
+ starttime = int(time.time())
+ try:
+ self.logger.info("running cycle %d", self.step)
+ for object in self.iterable():
+ self._monitor(object)
+ self.report_ok("unexpected error in monitor cycle")
+ self.clean()
+ except Exception:
+ self.logger.exception("Unexpected error while executing cycle")
+ self.report_bad("unexpected error in monitor cycle", 1, 0, 'UNEXPECTED ERROR IN THE CYCLE OF %s\n\n%s' % (self.name, self.describe_error()))
+ self.step += 1
+ self.statuses.sync()
+ time.sleep(max(0, self.period - (int(time.time()) - starttime)))
+
+ def clean(self):
+ for title, status in self.statuses.items():
+ if not status['working']:
+ if status['last_update'] != self.step:
+ self.report_ok(title)
+ else:
+ del self.statuses[title]
+
+
+ def _monitor(self, object):
+ try:
+ if self.monitor(object):
+ self.report_ok(str(self.alert_title(object)))
+ else:
+ self.report_bad(str(self.alert_title(object)), self.failure_threshold, self.fatal_failure_threshold, self.alert_msg(object))
+ self.report_ok("unexpected error in monitor")
+ except Exception, e:
+ self.logger.exception("Unexpected error while executing monitor. Exception is: %s" % (e))
+ message = 'UNEXPECTED ERROR IN THE MONITORING OF %s FOR TITLE: %s\n\n%s' % (self.name, self.alert_title(object), self.describe_error())
+ self.report_bad("unexpected error in monitor", 1, 'WARNING', message)
+
+ def describe_error(self):
+ exc_type, exc_value, exc_traceback = sys.exc_info()
+ return 'EXCEPTION: %s : %s\ntraceback:\n%s' % (exc_type, exc_value, ''.join(format_tb(exc_traceback)))
+
+ def update_status(self, key, **kwargs):
+ self.statuses[key] = kwargs
+
+ def send_alert(self, title, message, severity):
+ try:
+ if is_prod():
+ if severity == 'FATAL':
+ name = 'FATAL ALERT (%s)' % env_name()
+ else:
+ name = 'ALERT (%s)' % env_name()
+ else:
+ name = '%s test alert' % ENV
+
+ title = self.title_template % (ENV, self.name, severity, title)
+ message += '\n\n--------SENT AT ' + str(datetime.datetime.now())
+ to = ['alerts@indextank.com']
+ if severity == 'FATAL' and is_prod():
+ to.append('alerts+fatal@indextank.com')
+ to.append(self.pagerduty_email)
+ send_mail(title, message, '"%s" ' % name, to, fail_silently=False)
+ self.logger.info('Sending alert for title: %s\n============\n%s', title, message)
+ except Exception, e:
+ self.logger.exception("Unexpected error while sending alerts. Exception is: %s" % (e))
+
+ def report_ok(self, title):
+ if title in self.statuses and not self.statuses[title]['working'] and (self.statuses[title]['alerted'] or self.statuses[title]['alerted_fatal']):
+ # it has just been resolved
+ self.send_alert(title, 'The problem is no longer reported. The last message was:\n %s' % (self.statuses[title]['message']), self.severity)
+ if title in self.statuses:
+ del self.statuses[title]
+
+ def report_bad(self, title, threshold, fatal_threshold, message):
+ if title in self.statuses and not self.statuses[title]['working']:
+ # this object had already failed, let's grab the first step in which it failed
+ first_failure = self.statuses[title]['first_failure']
+ has_alerted = self.statuses[title]['alerted']
+ has_alerted_fatal = self.statuses[title]['alerted_fatal']
+ else:
+ # this object was fine, first failure is now
+ first_failure = self.step
+ has_alerted = False
+ has_alerted_fatal = False
+
+
+ should_alert = self.step - first_failure + 1 >= threshold
+ should_alert_fatal = fatal_threshold > 0 and self.step - first_failure + 1 >= fatal_threshold
+
+ if should_alert_fatal:
+ if not has_alerted_fatal:
+ has_alerted_fatal = True
+ if is_prod():
+ self.send_alert(title, 'A new problem on IndexTank has been detected:\n %s' % (message), 'FATAL')
+ else:
+ self.logger.info('Fatal error was found but alert has already been sent')
+ elif should_alert:
+ if not has_alerted:
+ has_alerted = True
+ self.send_alert(title, 'A new problem on IndexTank has been detected:\n %s' % (message), self.severity)
+ else:
+ self.logger.info('Error was found but alert has already been sent')
+
+ # save current state of the object (is_failed, message, first_failure, last_update)
+ self.update_status(title, working=False, last_update=self.step, message=message, first_failure=first_failure, alerted=has_alerted, alerted_fatal=has_alerted_fatal)
diff --git a/api/logging.conf b/api/logging.conf
new file mode 100644
index 0000000..838ebbb
--- /dev/null
+++ b/api/logging.conf
@@ -0,0 +1,50 @@
+[loggers]
+keys=root,rpc,boto,errors
+
+[handlers]
+keys=consoleHandler,errorHandler
+
+[formatters]
+keys=simpleFormatter,errorFormatter
+
+[logger_root]
+level=DEBUG
+handlers=consoleHandler
+
+[logger_rpc]
+level=INFO
+handlers=consoleHandler
+propagate=0
+qualname=RPC
+
+[logger_boto]
+level=INFO
+handlers=consoleHandler
+propagate=0
+qualname=boto
+
+[logger_errors]
+level=INFO
+handlers=errorHandler
+propagate=0
+qualname=Errors
+
+[handler_consoleHandler]
+class=StreamHandler
+formatter=simpleFormatter
+args=(sys.stdout,)
+
+[handler_errorHandler]
+class=StreamHandler
+formatter=errorFormatter
+args=(sys.stderr,)
+
+[formatter_simpleFormatter]
+format=%(pid)+5s %(asctime)s %(name)+8.8s:%(levelname)s%(prefix)s %(message)-90s %(suffix)s@%(filename)s:%(lineno)s
+datefmt=%d/%m-%H.%M.%S
+class=logging.SpecialFormatter
+
+[formatter_errorFormatter]
+format=%(pid)+5s %(asctime)s %(compname)+8.8s:%(levelname)s%(prefix)s %(message)-90s %(suffix)s@%(filename)s:%(lineno)s
+datefmt=%d/%m-%H.%M.%S
+class=logging.SpecialFormatter
diff --git a/api/manage.py b/api/manage.py
new file mode 100644
index 0000000..db37c60
--- /dev/null
+++ b/api/manage.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+from django.core.management import execute_manager
+
+from os import environ
+from sys import argv
+
+environ['DJANGO_LOCAL'] = ''
+
+if argv[1] == 'runserver':
+ environ['DJANGO_LOCAL'] = '1'
+if argv[1].startswith('local'):
+ environ['DJANGO_LOCAL'] = '1'
+ argv[1] = argv[1][5:]
+
+try:
+ import settings # Assumed to be in the same directory.
+except ImportError:
+ import sys
+ sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+ sys.exit(1)
+
+if __name__ == "__main__":
+ execute_manager(settings)
diff --git a/api/models.py b/api/models.py
new file mode 100644
index 0000000..0908f60
--- /dev/null
+++ b/api/models.py
@@ -0,0 +1,938 @@
+import hashlib
+import random
+import binascii
+
+from lib.indextank.client import ApiClient, IndexAlreadyExists
+from lib.authorizenet import AuthorizeNet, BillingException
+
+from django.db import models
+from django.contrib.auth.models import User
+from django.utils import simplejson as json
+from django.db import IntegrityError
+from django.db.models.aggregates import Sum, Count
+
+from lib import encoder, flaptor_logging
+
+from django.conf import settings
+from datetime import datetime
+
+logger = flaptor_logging.get_logger('Models')
+
+# idea taken from https://www.grc.com/passwords.htm
+def generate_apikey(id):
+ key = "2A1A8AE7CAEFAC47D6F74920CE4B0CE46430CDA6CF03D254C1C29402D727E570"
+ while True:
+ hash = hashlib.md5()
+ hash.update('%d' % id)
+ hash.update(key)
+ hash.update('%d' % random.randint(0,1000000))
+ random_part = binascii.b2a_base64(hash.digest())[:14]
+ if not '/' in random_part:
+ break
+
+ unique_part = encoder.to_key(id)
+
+ return unique_part + '-' + random_part
+
+def generate_onetimepass(id):
+ key = "CAEFAC47D6F7D727E57024920CE4B0CE46430CDA6CF03D254C1C29402A1A8AE7"
+ while True:
+ hash = hashlib.md5()
+ hash.update('%d' % id)
+ hash.update(key)
+ hash.update('%d' % random.randint(0,1000000))
+ random_part = binascii.b2a_base64(hash.digest())[:5]
+ if not '/' in random_part:
+ break
+
+ unique_part = encoder.to_key(id)
+
+ return unique_part + random_part
+
+def generate_forgotpass(id):
+ key = "E57024920CE4B0CE4643CAEFAC47D6F7D7270CDA6CF03D254C1C29402A1A8AE7"
+ while True:
+ hash = hashlib.md5()
+ hash.update('%d' % id)
+ hash.update(key)
+ hash.update('%d' % random.randint(0,1000000))
+ random_part = binascii.b2a_base64(hash.digest())[:6]
+ if not '/' in random_part:
+ break
+
+ unique_part = encoder.to_key(id)
+
+ return random_part + unique_part
+
+
+# StoreFront models
+class Account(models.Model):
+ apikey = models.CharField(max_length=22, unique=True)
+ creation_time = models.DateTimeField()
+ package = models.ForeignKey('Package', null=True)
+ status = models.CharField(max_length=30, null=False)
+ provisioner = models.ForeignKey('Provisioner', null=True)
+
+ configuration = models.ForeignKey('IndexConfiguration', null=True)
+ default_analyzer = models.ForeignKey('Analyzer', null=True, related_name="accounts")
+
+ class Statuses:
+ operational = 'OPERATIONAL'
+ creating = 'CREATING'
+ closed = 'CLOSED'
+
+ def __repr__(self):
+ return 'Account (%s):\n\tuser_email: %s\n\tapikey: %s\n\tcreation_time: %s\n\tstatus: %s\n\tpackage: %s\n\tconfiguration: %s\n' % (self.id, PFUser.objects.filter(account=self)[0].email, str(self.apikey), str(self.creation_time), str(self.status), self.package.name, self.configuration.description)
+
+ def __str__(self):
+ return '(apikey: %s; creation_time: %s; status: %s)' % (str(self.apikey), str(self.creation_time), str(self.status))
+
+ def count_indexes(self):
+ return self.indexes.aggregate(cnt=Count('id'))['cnt']
+
+ def count_documents(self):
+ return self.indexes.aggregate(cnt=Sum('current_docs_number'))['cnt']
+
+ def is_operational(self):
+ return self.status == Account.Statuses.operational
+
+ def is_heroku(self):
+ # HACK UNTIL HEROKU IS A PROVISIONER
+ return self.package.code.startswith('HEROKU_')
+ #return self.provisioner and self.provisioner.name == 'heroku'
+
+ @classmethod
+ def create_account(cls, dt, email=None, password=None):
+ account = Account()
+
+ account.creation_time = datetime.now()
+ account.status = Account.Statuses.creating
+ account.save()
+
+ account.apikey = generate_apikey(account.id)
+ account.save()
+
+ unique_part, random_part = account.apikey.split('-', 1)
+ if email is None:
+ email = '%s@indextank.com' % unique_part
+
+ if password is None:
+ password = random_part
+
+ try:
+ user = User.objects.create_user(email, '', password)
+ except IntegrityError, e:
+ account.delete()
+ raise e
+
+ try:
+ pfu = PFUser()
+ pfu.account = account
+ pfu.user = user
+ pfu.email = email
+
+ pfu.save()
+ except IntegrityError, e:
+ account.delete()
+ user.delete()
+ raise e
+
+ return account, pfu
+
+ def create_index(self, index_name, public_search=None):
+ index = Index()
+
+ # basic index data
+ index.populate_for_account(self)
+ index.name = index_name
+ index.creation_time = datetime.now()
+ index.language_code = 'en'
+ index.status = Index.States.new
+ if not public_search is None:
+ index.public_api = public_search
+
+ # test for name uniqueness
+ # raises IntegrityError if the index name already exists
+ index.save()
+
+ # define the default function
+ function = ScoreFunction()
+ function.index = index
+ function.name = '0'
+ function.definition = '-age'
+ function.save()
+
+ # deduce code from id
+ index.code = encoder.to_key(index.id)
+ index.save()
+
+ return index
+
+ def create_demo_index(self):
+ try:
+ dataset = DataSet.objects.get(code='DEMO')
+ except DataSet.DoesNotExist:
+ logger.exception('DemoIndex dataset not present in database. Aborting demo index creation')
+ return
+
+ index = self.create_index('DemoIndex')
+
+ index.public_api = True
+ index.save()
+
+ population = IndexPopulation()
+ population.index = index
+ population.status = IndexPopulation.Statuses.created
+ population.dataset = dataset
+ population.time = datetime.now()
+ population.populated_size = 0
+
+ population.save()
+
+ def close(self):
+ # Dropping an account implies:
+
+ # - removing the payment information from the account
+ # - removing the subscriptions from authorize.net
+ for info in self.payment_informations.all():
+ auth = AuthorizeNet()
+ for subscription in info.subscriptions.all():
+ auth.subscription_cancel(subscription.reference_id, subscription.subscription_id)
+ subscription.delete()
+ info.delete()
+
+
+ # - changing the status to CLOSED
+ self.status = Account.Statuses.closed
+
+ # - removing and stopping the indexes for the account
+ for index in self.indexes.all():
+ self.drop_index(index)
+
+ # - notify
+ # send_notification(//close account)
+
+ # - FIXME: handle authorize net errors!
+
+
+ self.save()
+
+ def drop_index(self, index):
+ client = ApiClient(self.get_private_apiurl())
+ client.delete_index(index.name)
+
+ def apply_package(self, package):
+ self.package = package
+
+ self.configuration = package.configuration
+
+ def update_apikey(self):
+ self.apikey = generate_apikey(self.id)
+
+ def get_private_apikey(self):
+ return self.apikey.split('-', 1)[1]
+
+ def get_public_apikey(self):
+ return self.apikey.split('-', 1)[0]
+
+ def get_private_apiurl(self):
+ return 'http://:%s@%s.api.indextank.com' % (self.get_private_apikey(), self.get_public_apikey())
+
+ def get_public_apiurl(self):
+ return 'http://%s.api.indextank.com' % self.get_public_apikey()
+
+ class Meta:
+ db_table = 'storefront_account'
+
+class AccountPayingInformation(models.Model):
+ account = models.ForeignKey('Account', related_name='payment_informations')
+
+ first_name = models.CharField(max_length=50, null=True)
+ last_name = models.CharField(max_length=50, null=True)
+ address = models.CharField(max_length=60, null=True)
+ city = models.CharField(max_length=60, null=True)
+ state = models.CharField(max_length=2, null=True)
+ zip_code = models.CharField(max_length=60, null=True)
+ country = models.CharField(max_length=2, null=True)
+
+ company = models.CharField(max_length=50, null=True)
+
+ credit_card_last_digits = models.CharField(max_length=4, null=True)
+ contact_email = models.EmailField(max_length=255, null=True)
+
+ #custom subscription
+ monthly_amount = models.DecimalField(max_digits=8, decimal_places=2, null=True)
+ subscription_status = models.CharField(max_length=30, null=True)
+ subscription_type = models.CharField(max_length=30, null=True)
+
+
+ class Meta:
+ db_table = 'storefront_accountpayinginformation'
+
+
+class PaymentSubscription(models.Model):
+ account = models.ForeignKey('AccountPayingInformation', related_name='subscriptions')
+
+ # authorizenet id
+ subscription_id = models.CharField(max_length=20, null=False, blank=False)
+ # indextank id
+ reference_id = models.CharField(max_length=13, null=False, blank=False)
+
+ amount = models.DecimalField(max_digits=8, decimal_places=2, null=False)
+
+ # Frequency
+ start_date = models.DateTimeField()
+ frequency_length = models.IntegerField(null=False)
+ frequency_unit = models.CharField(max_length=10, null=False, blank=False)
+
+ class Meta:
+ db_table = 'storefront_paymentsubscription'
+
+
+class EffectivePayment(models.Model):
+ account = models.ForeignKey('AccountPayingInformation', related_name='payments')
+
+ transaction_date = models.DateTimeField()
+
+ # authorizenet data
+ transaction_id = models.CharField(max_length=12, null=False, blank=False)
+ customer_id = models.CharField(max_length=8, null=False, blank=False)
+ transaction_message = models.CharField(max_length=300, null=True)
+ subscription_id = models.CharField(max_length=20, null=False, blank=False)
+ subscription_payment_number = models.IntegerField(null=False)
+ first_name = models.CharField(max_length=50, null=False, blank=False)
+ last_name = models.CharField(max_length=50, null=False, blank=False)
+ address = models.CharField(max_length=60, null=True)
+ city = models.CharField(max_length=60, null=True)
+ state = models.CharField(max_length=2, null=True)
+ zip_code = models.CharField(max_length=60, null=True)
+ country = models.CharField(max_length=2, null=True)
+ company = models.CharField(max_length=50, null=True)
+
+ # Inherited data (from account information
+ credit_card_last_digits = models.CharField(max_length=4, null=False, blank=False)
+ contact_email = models.EmailField(max_length=255)
+
+ amount = models.DecimalField(max_digits=8, decimal_places=2, null=False)
+
+ class Meta:
+ db_table = 'storefront_effectivepayment'
+
+class DataSet(models.Model):
+ name = models.CharField(null=True, max_length=50, unique=True)
+ code = models.CharField(null=True, max_length=15, unique=True)
+ filename = models.CharField(null=True, max_length=100, unique=True)
+ size = models.IntegerField(default=0)
+
+ class Meta:
+ db_table = 'storefront_dataset'
+
+class IndexPopulation(models.Model):
+ index = models.ForeignKey('Index', related_name='datasets')
+ dataset = models.ForeignKey('DataSet', related_name='indexes')
+ time = models.DateTimeField()
+ populated_size = models.IntegerField(default=0)
+
+ status = models.CharField(max_length=50,null=True)
+
+ class Statuses:
+ created = 'CREATED'
+ populating = 'POPULATING'
+ finished = 'FINISHED'
+
+ class Meta:
+ db_table = 'storefront_indexpopulation'
+
+
+class Index(models.Model):
+ account = models.ForeignKey('Account', related_name='indexes')
+ code = models.CharField(null=True, max_length=22, unique=True)
+ name = models.CharField(max_length=50)
+ language_code = models.CharField(max_length=2)
+ creation_time = models.DateTimeField()
+
+ analyzer_config = models.TextField(null=True)
+ configuration = models.ForeignKey('IndexConfiguration', null=True)
+ public_api = models.BooleanField(default=False, null=False)
+
+ status = models.CharField(max_length=50)
+
+ deleted = models.BooleanField(default=False, null=False)
+
+ class States:
+ new = 'NEW'
+ live = 'LIVE'
+ hibernate_requested = 'HIBERNATE_REQUESTED'
+ hibernated = 'HIBERNATED'
+ waking_up = 'WAKING_UP'
+
+ def get_json_for_analyzer(self):
+ if self.analyzer_config is None:
+ return None
+ configuration = json.loads(self.analyzer_config)
+ final_configuration = {}
+
+ if configuration.has_key('per_field'):
+ per_field_final = {}
+ per_field = configuration.get('per_field')
+ for field in per_field.keys():
+ per_field_final[field] = Index.get_analyzer(per_field[field])
+ final_configuration['perField'] = per_field_final
+ final_configuration['default'] = Index.get_analyzer(per_field.get('default'))
+ else:
+ final_configuration = Index.get_analyzer(configuration)
+
+ return final_configuration
+
+ @classmethod
+ def get_analyzer(cls, configuration):
+ analyzer_map = {}
+ code = configuration.get('code')
+ if code is None:
+ raise ValueError('Analyzer configuration has no "code" key')
+
+ try:
+ analyzer = AnalyzerComponent.objects.get(code=code)
+ except AnalyzerComponent.DoesNotExist:
+ raise ValueError('Analyzer configuration "code" key doesn\'t match any analyzers')
+
+ analyzer_map['factory'] = analyzer.factory
+ analyzer_map['configuration'] = json.loads(analyzer.config)
+
+ if configuration.has_key('filters'):
+ filters_list = []
+ for filter in configuration.get('filters'):
+ filters_list.append(Index.get_analyzer(filter))
+ analyzer_map['configuration']['filters'] = filters_list
+
+ return analyzer_map
+
+# allows_adds = models.BooleanField(null=False,default=True)
+# allows_queries = models.BooleanField(null=False,default=True)
+
+ # index creation data
+# allows_snippets = models.BooleanField()
+#
+# allows_autocomplete = models.BooleanField(default=True)
+# autocomplete_type = models.models.CharField(max_length=10, null=True) # NEW
+#
+# allows_faceting = models.BooleanField()
+# facets_bits = models.IntegerField(null=True) # NEW
+#
+# max_variables = models.IntegerField(null=False) # NEW
+#
+# max_memory_mb = models.IntegerField(null=False) # NEW
+# rti_documents_number = models.IntegerField(null=False) # NEW
+
+ # statistics
+ current_size = models.FloatField(default=0)
+ current_docs_number = models.IntegerField(default=0)
+ queries_per_day = models.FloatField(default=0)
+
+ #demo
+ base_port = models.IntegerField(null=True)
+
+ def __repr__(self):
+ return 'Index (%s):\n\tname: %s\n\tcode: %s\n\tcreation_time: %s\n\tconfiguration: %s\n\taccount\'s package: %s\ncurrent deploys: %r' % (self.id, self.name, self.code, self.creation_time, self.configuration.description, self.account.package.name, self.deploys.all())
+
+ def is_populating(self):
+ for population in self.datasets.all():
+ if not population.status == IndexPopulation.Statuses.finished:
+ return True
+ return False
+
+ def is_demo(self):
+ return self.name == 'DemoIndex' and self.datasets.count() > 0
+
+
+ def is_ready(self):
+ '''
+ Returns True if the end-user can use the index.
+ (this means for read and write, and it's meant to
+ be shown in the storefront page). Internally, this
+ means that at least one deployment for this index
+ is readable, and at least one is writable.
+ '''
+ return self.is_writable() and self.is_readable()
+
+ def is_hibernated(self):
+ return self.status in (Index.States.hibernated, Index.States.waking_up)
+
+ def is_writable(self):
+ '''
+ Returns true if there's at least one index that can be written.
+ '''
+ for deploy in self.deploys.all():
+ if deploy.is_writable():
+ return True
+
+ def is_readable(self):
+ '''
+ Returns true if there's at least one index that can be read.
+ '''
+ for deploy in self.deploys.all():
+ if deploy.is_readable():
+ return True
+
+ def populate_for_account(self, account):
+ self.account = account
+ self.configuration = account.configuration
+ if account.default_analyzer is not None:
+ self.analyzer_config = account.default_analyzer.configuration
+
+ def searchable_deploy(self):
+ '''Returns a single deploy that can be used to search. If no deploy is searcheable
+ it returns None. Note that if more than one deploy is searcheable, there are no warranties
+ of wich one will be returned.'''
+ # TODO : should change once deploy roles are implemented
+ ds = self.deploys.all()
+ ds = [d for d in ds if d.is_readable()]
+ return ds[0] if ds else None
+
+ def indexable_deploys(self):
+ '''Returns the list of all deploys that should be updated (adds/updates/deletes/etc)'''
+ # TODO : should change once deploy roles are implemented
+ ds = self.deploys.all()
+ ds = [d for d in ds if d.is_writable()]
+ return ds
+
+ def get_functions_dict(self):
+ return dict((str(f.name), f.definition) for f in self.scorefunctions.all())
+
+ def get_debug_info(self):
+ info = 'Index: %s [%s]\n' % (self.name, self.code) +\
+ 'Account: %s\n' % self.account.user.email +\
+ 'Deploys:\n'
+ for d in self.deploys.all():
+ info += ' [deploy:%d] %s on [worker:%s] %s:%s' % (d.id, d.status, d.worker.id, d.worker.wan_dns, d.base_port)
+ return info
+
+ def update_status(self, new_status):
+ print 'updating status %s for %r' % (new_status, self)
+ logger.debug('Updating status to %s for idnex %r', new_status, self)
+ Index.objects.filter(id=self.id).update(status=new_status)
+
+ def mark_deleted(self):
+ Index.objects.filter(id=self.id).update(deleted=True)
+
+ class AutocompleTypes:
+ created = 'DOCUMENTS'
+ initializing = 'QUERIES'
+
+ class Meta:
+ unique_together = (('code','account'),('name','account'))
+ db_table = 'storefront_index'
+
+class Insight(models.Model):
+ index = models.ForeignKey(Index, related_name='insights')
+ code = models.CharField(max_length=30, null=False)
+ data = models.TextField(null=False)
+ last_update = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ unique_together = ('index', 'code')
+ db_table = 'storefront_insight'
+
+class IndexConfiguration(models.Model):
+ description = models.TextField(null=False)
+ creation_date = models.DateField()
+ json_configuration = models.TextField(null=False)
+
+ def __repr__(self):
+ j_map = json.loads(self.json_configuration)
+ mapStr = '{\n'
+ for m in j_map:
+ mapStr += '\t\t%s -> %s\n' % (m, j_map[m])
+ mapStr += '\t}\n'
+ return 'IndexConfiguration (%s):\n\tdescription: %s\n\tcreation_date: %s\n\tjson_configuration: %s\n' % (self.id, self.description, str(self.creation_date), mapStr)
+
+ def __str__(self):
+ return '(description: %s; creation_date: %s; json_configuration: %s)' % (self.description, str(self.creation_date), self.json_configuration)
+
+ def get_data(self):
+ map = json.loads(self.json_configuration)
+ data = {}
+ for k,v in map.items():
+ data[str(k)] = v
+ data['ram'] = data.get('xmx',0) + data.get('bdb_cache',0)
+ return data
+ def set_data(self, data):
+ self.json_configuration = json.dumps(data)
+
+ class Meta:
+ db_table = 'storefront_indexconfiguration'
+
+class Analyzer(models.Model):
+ account = models.ForeignKey('Account', related_name='analyzers')
+ code = models.CharField(max_length=64)
+ configuration = models.TextField()
+
+ class Meta:
+ db_table = 'storefront_analyzer'
+
+class AnalyzerComponent(models.Model):
+ code = models.CharField(max_length=15, unique=True)
+ name = models.CharField(max_length=200)
+ description = models.CharField(max_length=1000)
+ config = models.TextField(null=False,blank=False)
+ factory = models.CharField(max_length=200)
+ type = models.CharField(max_length=20)
+ enabled = models.BooleanField()
+
+ class Types:
+ tokenizer = 'TOKENIZER'
+ filter = 'FILTER'
+
+ class Meta:
+ db_table = 'storefront_analyzercomponent'
+
+def create_analyzer(code, name, config, factory, type, enabled):
+ analyzer = None
+ try:
+ analyzer = AnalyzerComponent.objects.get(code=code)
+
+ analyzer.name = name
+ analyzer.config = config
+ analyzer.factory = factory
+ analyzer.type = type
+ analyzer.enabled = enabled
+
+ analyzer.save()
+ except AnalyzerComponent.DoesNotExist:
+ analyzer = AnalyzerComponent(code=code, name=name, config=config, type=type, enabled=enabled)
+ analyzer.save()
+
+class Package(models.Model):
+ '''
+ Packages define what a user have the right to when creating an Account and how does the indexes in that Account
+ behave.
+ There are two sections for what the Package configures. A fixed section with the control and limits information
+ that is used by nebu, storefront or api (base_price, index_max_size, searches_per_day, max_indexes). A dynamic
+ section that is handled by the IndexConfiguration object. The information of that section is passed to the IndexEngine
+ as it is and handled by it.
+ '''
+ name = models.CharField(max_length=50)
+ code = models.CharField(max_length=30)
+ base_price = models.FloatField()
+ index_max_size = models.IntegerField()
+ searches_per_day = models.IntegerField()
+ max_indexes = models.IntegerField()
+
+ configuration = models.ForeignKey('IndexConfiguration', null=True)
+
+ def __repr__(self):
+ return 'Package (%s):\n\tname: %s\n\tcode: %s\n\tbase_price: %.2f\n\tindex_max_size: %i\n\tsearches_per_day: %i\n\tmax_indexes: %i\n' % (self.id, self.name, self.code, self.base_price, self.index_max_size, self.searches_per_day, self.max_indexes)
+
+ def __str__(self):
+ return '(name: %s; code: %s; base_price: %.2f; index_max_size: %i; searches_per_day: %i; max_indexes: %i)' % (self.name, self.code, self.base_price, self.index_max_size, self.searches_per_day, self.max_indexes)
+
+ def max_size_mb(self):
+ return self.index_max_size * settings.INDEX_SIZE_RATIO
+ class Meta:
+ db_table = 'storefront_package'
+
+class ScoreFunction(models.Model):
+ index = models.ForeignKey(Index, related_name='scorefunctions')
+ name = models.IntegerField(null=False) # TODO the java API expects an int. But a String may be nicer for name.
+ definition = models.CharField(max_length=255, blank=False, null=True)
+
+ class Meta:
+ db_table = 'storefront_scorefunction'
+ unique_together = (('index','name'))
+
+
+def create_configuration(description, data, creation_date=None):
+ configuration = IndexConfiguration()
+ configuration.description = description
+ configuration.creation_date = creation_date or datetime.now()
+ configuration.json_configuration = json.dumps(data)
+
+ configuration.save()
+ return configuration
+
+def create_package(code, name, base_price, index_max_size, searches_per_day, max_indexes, configuration_map):
+# The configuration_map will only be considered if the package if new or if it didn't already have a configuration
+
+ package = None
+ try:
+ package = Package.objects.get(code=code)
+
+ package.name = name
+ package.base_price = base_price
+ package.index_max_size = index_max_size
+ package.searches_per_day = searches_per_day
+ package.max_indexes = max_indexes
+
+ if not package.configuration:
+ package.configuration = create_configuration('package:' + code, configuration_map)
+
+ package.save()
+ except Package.DoesNotExist:
+ configuration = create_configuration('package:' + code, configuration_map)
+ package = Package(code=code, base_price=base_price, index_max_size=index_max_size, searches_per_day=searches_per_day, max_indexes=max_indexes, configuration=configuration)
+ package.save()
+
+def create_provisioner(name, token, email, plans):
+ provisioner = None
+ try:
+ provisioner = Provisioner.objects.get(name=name)
+ except Provisioner.DoesNotExist:
+ provisioner = Provisioner()
+ provisioner.name = name
+ provisioner.token = token
+ provisioner.email = email
+ provisioner.save()
+
+ provisioner.plans.all().delete()
+ for plan, code in plans.items():
+ pp = ProvisionerPlan()
+ pp.plan = plan
+ pp.provisioner = provisioner
+ pp.package = Package.objects.get(code=code)
+ pp.save()
+
+
+class AccountMovement(models.Model):
+ account = models.ForeignKey('Account', related_name='movements')
+ class Meta:
+ db_table = 'storefront_accountmovement'
+
+class ActionLog(models.Model):
+ account = models.ForeignKey('Account', related_name='actions')
+ class Meta:
+ db_table = 'storefront_actionlog'
+
+class PFUser(models.Model):
+ user = models.ForeignKey(User, unique=True)
+ account = models.OneToOneField('Account', related_name='user')
+ email = models.EmailField(unique=True, max_length=255)
+ change_password = models.BooleanField(default=False, null=False)
+ class Meta:
+ db_table = 'storefront_pfuser'
+
+
+
+MAX_USABLE_RAM_PERCENTAGE = 0.9
+# Nebulyzer stuff
+class Worker(models.Model):
+ '''
+ Describes an amazon ec2 instance.
+ '''
+ instance_name = models.CharField(max_length=50,null=False,blank=False)
+ lan_dns = models.CharField(max_length=100,null=False,blank=False)
+ wan_dns = models.CharField(max_length=100,null=False,blank=False)
+ status = models.CharField(max_length=30)
+ timestamp = models.DateTimeField(auto_now=True,auto_now_add=True)
+ #physical memory in MegaBytes
+ ram = models.IntegerField()
+
+ class States:
+ created = 'CREATED'
+ initializing = 'INITIALIZING'
+ updating = 'UPDATING'
+ controllable = 'CONTROLLABLE'
+ decommissioning = 'DECOMMISSIONING'
+ dying = 'DYING'
+ dead = 'DEAD'
+
+ class Meta:
+ db_table = 'storefront_worker'
+
+ def get_usable_ram(self):
+ '''Return the amount of ram that can be used in this machine for
+ indexengines. It's calculated as a fixed percentage of the physical
+ ram. Value returned in MegaBytes'''
+ return MAX_USABLE_RAM_PERCENTAGE * self.ram
+
+ def get_used_ram(self):
+ xmx = self.deploys.aggregate(xmx=Sum('effective_xmx'))['xmx']
+ bdb = self.deploys.aggregate(bdb=Sum('effective_bdb'))['bdb']
+ if xmx == None:
+ xmx = 0
+ if bdb == None:
+ bdb = 0
+ return xmx + bdb
+
+ def is_assignable(self):
+ return self.status != Worker.States.decommissioning
+
+ def is_ready(self):
+ return self.status in [Worker.States.controllable, Worker.States.decommissioning]
+
+ def __repr__(self):
+ return 'Worker (%s):\n\tinstance_name: %s\n\tlan_dns: %s\n\twan_dns: %s\n\tstatus: %s\n\ttimestamp: %s\n\tram: %s\n' %(self.pk, self.instance_name, self.lan_dns, self.wan_dns, self.status, self.timestamp, self.ram)
+
+class Service(models.Model):
+ name = models.CharField(max_length=50,null=False,blank=False)
+ type = models.CharField(max_length=50,null=True,blank=True )
+ host = models.CharField(max_length=100,null=False,blank=False)
+ port = models.IntegerField()
+ timestamp = models.DateTimeField(auto_now=True,auto_now_add=True)
+
+ class Meta:
+ db_table = 'storefront_service'
+
+ def __repr__(self):
+ return 'Service (%s):\n\tname: %s\n\ttype: %s\n\thost: %s\n\tport: %s\n\ttimestamp: %s\n' % (self.pk, self.name, self.type, self.host, self.port, self.timestamp)
+
+
+# CPU Stats
+class WorkerMountInfo(models.Model):
+ worker = models.ForeignKey(Worker, related_name="disk_infos")
+ timestamp = models.DateTimeField()
+
+ mount = models.CharField(max_length=100,null=False,blank=False)
+ available = models.IntegerField()
+ used = models.IntegerField()
+
+ class Meta:
+ db_table = 'storefront_workermountinfo'
+
+
+class WorkerLoadInfo(models.Model):
+ worker = models.ForeignKey(Worker, related_name="load_infos")
+ timestamp = models.DateTimeField()
+
+ load_average = models.FloatField()
+
+ class Meta:
+ db_table = 'storefront_workerloadinfo'
+
+class WorkerIndexInfo(models.Model):
+ worker = models.ForeignKey(Worker, related_name="indexes_infos")
+ timestamp = models.DateTimeField()
+
+ deploy = models.ForeignKey('Deploy', related_name="index_infos")
+ used_disk = models.IntegerField()
+ used_mem = models.IntegerField()
+
+ class Meta:
+ db_table = 'storefront_workerindexinfo'
+
+
+class Deploy(models.Model):
+ '''
+ Describes a deploy of an index on a worker, and it's status.
+ The idea is that an index can be moving from one worker to another,
+ so queries and indexing requests have to be mapped to one or more
+ index engines.
+ '''
+ index = models.ForeignKey(Index, related_name="deploys")
+ worker = models.ForeignKey(Worker, related_name="deploys")
+ base_port = models.IntegerField()
+ status = models.CharField(max_length=30)
+ timestamp = models.DateTimeField(auto_now=True,auto_now_add=True) # Last time we updated this deploy.
+ parent = models.ForeignKey('self', related_name='children', null=True) # For moving deploys.
+ effective_xmx = models.IntegerField()
+ effective_bdb = models.IntegerField()
+ dying = models.BooleanField(default=False, null=False)
+
+ # TODO add role fields
+ #searching_role = models.BooleanField()
+ #indexing_role = models.BooleanField()
+
+ def __repr__(self):
+ return 'Deploy (%s):\n\tparent deploy: %s\n\tindex code: %s\n\tstatus: %s\n\tworker ip: %s\n\tport: %d\n\teffective_xmx: %d\n\teffective_bdb: %d\n' % (self.id, self.parent_id, self.index.code, self.status, self.worker.lan_dns, self.base_port, self.effective_xmx, self.effective_bdb)
+
+ def __unicode__(self):
+ return "Deploy: %s on %s:%d" % (self.status, self.worker.lan_dns, self.base_port)
+
+ def is_readable(self):
+ '''Returns true if a search can be performed on this deployment, and
+ the returned data is up to date'''
+ return self.status == Deploy.States.controllable or \
+ self.status == Deploy.States.move_requested or \
+ self.status == Deploy.States.moving or \
+ (self.status == Deploy.States.recovering and not self.parent)
+
+ def is_writable(self):
+ '''Returns True if new data has to be written to this deployment.'''
+ return self.status == Deploy.States.controllable or \
+ self.status == Deploy.States.recovering or \
+ self.status == Deploy.States.move_requested or \
+ self.status == Deploy.States.moving
+
+ def total_ram(self):
+ return self.effective_xmx + self.effective_bdb
+
+ def update_status(self, new_status):
+ print 'updating status %s for %r' % (new_status, self)
+ logger.debug('Updating status to %s for deploy %r', new_status, self)
+ Deploy.objects.filter(id=self.id).update(status=new_status, timestamp=datetime.now())
+
+ def update_parent(self, new_parent):
+ logger.debug('Updating parent to %s for deploy %r', new_parent, self)
+ Deploy.objects.filter(id=self.id).update(parent=new_parent)
+
+ class States:
+ created = 'CREATED'
+ initializing = 'INITIALIZING'
+ recovering = 'RECOVERING'
+ resurrecting = 'RESURRECTING'
+ controllable = 'CONTROLLABLE'
+ move_requested = 'MOVE_REQUESTED'
+ moving = 'MOVING'
+ decommissioning = 'DECOMMISSIONING'
+
+ class Meta:
+ db_table = 'storefront_deploy'
+
+class BetaTestRequest(models.Model):
+ email = models.EmailField(unique=True, max_length=255)
+ site_url = models.CharField(max_length=200,null=False,blank=False)
+ summary = models.TextField(null=False,blank=False)
+
+ request_date = models.DateTimeField(default=datetime.now)
+ status = models.CharField(max_length=50,null=True)
+
+ class Meta:
+ db_table = 'storefront_betatestrequest'
+
+class BetaInvitation(models.Model):
+ password = models.CharField(max_length=20, null=True)
+ account = models.ForeignKey('Account', null=True)
+ assigned_customer = models.CharField(max_length=50, null=True)
+ beta_requester = models.ForeignKey('BetaTestRequest', null=True, related_name="invitation")
+
+ invitation_date = models.DateTimeField(default=datetime.now)
+ forced_package = models.ForeignKey('Package', null=False)
+
+ class Meta:
+ db_table = 'storefront_signupotp'
+
+class ContactInfo(models.Model):
+ name = models.CharField(max_length=64)
+ email = models.EmailField(unique=True, max_length=255)
+ request_date = models.DateTimeField(default=datetime.now)
+ source = models.CharField(max_length=64, null=True)
+
+ class Meta:
+ db_table = 'storefront_contactinfo'
+
+
+
+class Provisioner(models.Model):
+ name = models.CharField(max_length=64)
+ token = models.CharField(max_length=64, null=False, blank=False)
+ email = models.EmailField(max_length=255) # contact info for the provisioner
+
+ class Meta:
+ db_table = "storefront_provisioner"
+
+class ProvisionerPlan(models.Model):
+ plan = models.CharField(max_length=50)
+ provisioner = models.ForeignKey('Provisioner', related_name='plans')
+ package = models.ForeignKey('Package')
+
+ class Meta:
+ db_table = "storefront_provisionerplan"
+
+class BlogPostInfo(models.Model):
+ title = models.CharField(max_length=200)
+ url = models.CharField(max_length=1024)
+ date = models.DateTimeField()
+ author = models.CharField(max_length=64)
+
+ class Meta:
+ db_table = 'storefront_blogpost'
+
diff --git a/api/restapi.py b/api/restapi.py
new file mode 100644
index 0000000..b6025c9
--- /dev/null
+++ b/api/restapi.py
@@ -0,0 +1,1647 @@
+import datetime
+
+from django.contrib.auth.management.commands.createsuperuser import is_valid_email
+from django.http import HttpResponse, Http404, HttpResponseRedirect
+from django.db import IntegrityError
+
+from models import Account, ScoreFunction, Package, Service
+
+from flaptor.indextank.rpc import ttypes
+from views import mixpanel_event_track
+
+from restresource import ProvisionerResource, Resource, JsonResponse, non_empty_argument, int_argument, required_data, optional_data, int_querystring, json_querystring, querystring_argument, required_querystring_argument, check_public_api, get_index_param, get_index_param_or404, wakeup_if_hibernated, authorized_method
+
+from lib import encoder
+from lib import mail
+from lib.indextank.client import ApiClient
+
+import rpc
+import re
+from api import models
+import time
+import storage
+
+LOG_STORAGE_ENABLED = True
+
+""" Data validation and parsing functions """
+def _encode_utf8(s):
+ try:
+ return s.encode('utf-8')
+ except:
+ try:
+ str(s).decode('utf-8')
+ return str(s)
+ except:
+ try:
+ return str(s).encode('utf-8')
+ except:
+ return None
+
+def __validate_boolean(field_name):
+ def dovalidate(arg):
+ if type(arg) != bool:
+ return HttpResponse('"Invalid \\"%s\\" argument, it should be a json boolean"' % field_name, status=400)
+ return dovalidate
+
+def __validate_docid(docid):
+ """
+ Validates that a document id is a string, a unicode, or an int (for backwards compatibility).
+ It can't be empty, nor longer than 1024 bytes.
+ Valid inputs
+ >>> __validate_docid("a")
+ >>> __validate_docid("\xc3\xb1")
+ >>> __validate_docid(u"\xc3\xb1")
+ >>> # for backwards compatibility
+ >>> __validate_docid(123)
+ >>> __validate_docid(0)
+ >>> __validate_docid(-1)
+
+ Validate length
+ >>> __validate_docid("a"*1024)
+ >>> __validate_docid(u"a"*1024)
+ >>> # 512 2-byte chars are ok
+ >>> __validate_docid("\xc3\xb1"*512)
+ >>> e = __validate_docid("a"*1025)
+ >>> isinstance(e, HttpResponse)
+ True
+ >>> e = __validate_docid(u"\xc3"*1025)
+ >>> isinstance(e, HttpResponse)
+ True
+ >>> # 512 2-byte chars are not ok
+ >>> e = __validate_docid("\xc3\xb1"*513)
+ >>> isinstance(e, HttpResponse)
+ True
+
+ Validate emptyness
+ >>> e = __validate_docid(" ")
+ >>> isinstance(e, HttpResponse)
+ True
+ >>> e = __validate_docid("")
+ >>> isinstance(e, HttpResponse)
+ True
+ >>> e = __validate_docid(" "*80)
+ >>> isinstance(e, HttpResponse)
+ True
+
+ Validate not supported types
+ >>> e = __validate_docid(80.0)
+ >>> isinstance(e, HttpResponse)
+ True
+ >>> e = __validate_docid([1,2,3])
+ >>> isinstance(e, HttpResponse)
+ True
+ >>> e = __validate_docid({"a":"b"})
+ >>> isinstance(e, HttpResponse)
+ True
+
+ Validate None
+ >>> e = __validate_docid(None)
+ >>> isinstance(e, HttpResponse)
+ True
+
+
+ """
+ if type(docid) in [int, long]:
+ docid = str(docid)
+
+ if not type(docid) in [str,unicode]:
+ return HttpResponse('"Invalid docid, it should be a String."', status=400)
+
+ if docid.strip() == '':
+ return HttpResponse('"Invalid docid, it shouldnt be empty."', status=400)
+
+ udocid = _encode_utf8(docid)
+ if len(udocid) > 1024:
+ return HttpResponse('"Invalid docid, it shouldnt be longer than 1024 bytes. It was %d"'%len(udocid), status=400)
+
+
+def __parse_docid(docid):
+ if not type(docid) in [str,unicode]:
+ docid = str(docid)
+ return docid
+
+def __str_is_integer(val):
+ try:
+ i = int(val)
+ return True
+ except:
+ return False
+
+def __validate_fields(fields):
+ """
+ Validates that a document fields is a dictionary with string (or unicode) keys and string (or unicode) values.
+ The only exception is 'timestamp', that can be an int or a string representation of an int.
+
+ The sum of the sizes of all the field values can not be bigger than 100kb.
+ Returns nothing, unless a validation error was found. In that case, it returns an HttpResponse with the error as body.
+
+ Validate documents without errors
+ >>> __validate_fields({'field1':'value1', 'field2':'value2'})
+ >>> __validate_fields({'text':'', 'title':u''})
+ >>> __validate_fields({'text':u'just one field'})
+ >>> __validate_fields({'field1':'value1 and value2 and value 3 or value4', 'field2':'value2', 'field3':'123'})
+
+ Validate documents with errors on field values (int, float, array, dict and None)
+ As the input for this method comes from json, there can't be objects as values
+ >>> __validate_fields({'text': 123})
+ >>> __validate_fields({'text': 42.8})
+
+ >>> e = __validate_fields({'text': ['123']})
+ >>> isinstance(e,HttpResponse)
+ True
+ >>> e = __validate_fields({'text': None})
+ >>> isinstance(e,HttpResponse)
+ True
+ >>> e = __validate_fields({'text': {'k':'v'}})
+ >>> isinstance(e,HttpResponse)
+ True
+
+ Validate documents with errors on field names (int, float and None)
+ As the input for this method comes from json, there can't be objects as keys
+ >>> e = __validate_fields({None: '123'})
+ >>> isinstance(e,HttpResponse)
+ True
+ >>> e = __validate_fields({123: 'text'})
+ >>> isinstance(e,HttpResponse)
+ True
+ >>> e = __validate_fields({42.5: 'value'})
+ >>> isinstance(e,HttpResponse)
+ True
+
+ Validate timestamps
+ >>> __validate_fields({'timestamp': 123, 'field1':'value1'})
+ >>> __validate_fields({'timestamp': '123', 'fieldN':'valueN'})
+ >>> __validate_fields({'timestamp': -123, 'field1':'value1'})
+ >>> __validate_fields({'timestamp': '-123', 'fieldN':'valueN'})
+ >>> e = __validate_fields({'timestamp': 'something', 'fieldN': 'valueN'})
+ >>> isinstance(e,HttpResponse)
+ True
+
+ Validate document size
+ >>> __validate_fields({'text': 'a'*1024})
+ >>> __validate_fields({'text': 'a'*1024, 'title':'b'*1024})
+ >>> # this is the boundary case for 1 field
+ >>> __validate_fields({'text': 'a'*1024*100})
+ >>> # a boundary case for 2 fields .. 1 * 9 * 1024 + 10 * 9 * 1024
+ >>> __validate_fields({'text': 'a'*9*1024, 'title': 'a b c d e '*9*1024})
+ >>> # a boundary case for 2-byte chars on fields
+ >>> __validate_fields({'text': '\xc3\xb1'*100*512})
+ >>> # this is an error case for 1 field
+ >>> e = __validate_fields({'text': 'a'*1024*101})
+ >>> isinstance(e,HttpResponse)
+ True
+ >>> # this is an error case for 2 fields
+ >>> e = __validate_fields({'text': 'a'*50*1024, 'title': 'a b c d e '*9*1024})
+ >>> isinstance(e,HttpResponse)
+ True
+ >>> # this is an error case for 10 fields .. 10 * ( 1024 * 11 )
+ >>> fields = {}
+ >>> fields.update([("text%d"%i,"123 456 789"*1024) for i in range(0,10)])
+ >>> e = __validate_fields(fields)
+ >>> isinstance(e,HttpResponse)
+ True
+ >>> # this is an error case for 2-byte chars on fields
+ >>> e = __validate_fields({"text":"\xc3\xb1"*100*513})
+ >>> isinstance(e,HttpResponse)
+ True
+ >>> # disallow no fields
+ >>> e = __validate_fields({})
+ >>> isinstance(e,HttpResponse)
+ True
+
+ >>> # disallow None, Arrays, Numbers and Strings
+ >>> e = __validate_fields(None)
+ >>> isinstance(e,HttpResponse)
+ True
+ >>> e = __validate_fields([1, 2, 3])
+ >>> isinstance(e,HttpResponse)
+ True
+ >>> e = __validate_fields(123)
+ >>> isinstance(e,HttpResponse)
+ True
+ >>> e = __validate_fields("this is some text")
+ >>> isinstance(e,HttpResponse)
+ True
+ >>> e = __validate_fields(True)
+ >>> isinstance(e,HttpResponse)
+ True
+
+ """
+
+ if not fields:
+ return HttpResponse('"At least one field is required. If you\'re trying to delete the document, you should use the delete api"', status=400)
+
+
+ if not isinstance(fields, dict):
+ return HttpResponse('"fields should be a JSON Object, e.g: {\'text\': \'something\'} "', status=400)
+
+
+ for k, v in fields.iteritems():
+ # timestamp gets special treatment, it should be an integer
+ if 'timestamp' == k:
+ if not type(v) == int and not (type(v) == str and __str_is_integer(v)):
+ return HttpResponse('"Invalid timestamp: %s. It should be an integer."' % fields['timestamp'], status=400)
+ continue
+
+ # any other key should be a string or unicode
+ if not isinstance(k,str) and not isinstance(k,unicode):
+ return HttpResponse('"Name for field %s is not a String"' % (k), status=400)
+
+
+ if isinstance(v,int) or isinstance(v,float):
+ v = str(v)
+ if not isinstance(v,str) and not isinstance(v,unicode):
+ return HttpResponse('"Value for field %s is not a String nor a number"' % (k), status=400)
+
+ ev = _encode_utf8(v)
+ ek = _encode_utf8(k)
+
+ if ek is None:
+ return HttpResponse('"Invalid name for field %s"' % (k), status=400)
+ if ev is None:
+ return HttpResponse('"Invalid content for field %s: %s"' % (k, v), status=400)
+
+ # verify document size
+ doc_size = sum(map(lambda (k,v) : len(v) if type(v) in [str, unicode] else 0, fields.iteritems()))
+ if doc_size > 1024 * 100:
+ return HttpResponse('"Invalid document size. It shouldn\'t be bigger than 100KB. Got %d bytes"' % (doc_size), status=400)
+
+
+def __parse_fields(fields):
+ if 'timestamp' not in fields:
+ fields['timestamp'] = str(int(time.time()))
+ for k, v in fields.iteritems():
+ fields[k] = _encode_utf8(v)
+ return fields
+
+def __validate_categories(categories):
+ """
+ Validates that a document categories is a dictionary with string (or unicode) keys and string (or unicode) values.
+ Returns nothing, unless a validation error was found. In that case, it returns an HttpResponse with the error as body
+
+ Validate categories without errors
+ >>> __validate_categories({'field1':'value1', 'field2':'value2'})
+ >>> __validate_categories({'text':'', 'title':u''})
+ >>> __validate_categories({'text':u'just one field'})
+ >>> __validate_categories({'field1':'value1 and value2 and value 3 or value4', 'field2':'value2', 'field3':'123'})
+
+ Validate documents with errors on category values (int, float, array, dict and None)
+ As the input for this method comes from json, there can't be objects as values
+ >>> e = __validate_categories({'text': 123})
+ >>> isinstance(e,HttpResponse)
+ True
+ >>> e = __validate_categories({'text': 42.8})
+ >>> isinstance(e,HttpResponse)
+ True
+ >>> e = __validate_categories({'text': ['123']})
+ >>> isinstance(e,HttpResponse)
+ True
+ >>> e = __validate_categories({'text': None})
+ >>> isinstance(e,HttpResponse)
+ True
+ >>> e = __validate_categories({'text': {'k':'v'}})
+ >>> isinstance(e,HttpResponse)
+ True
+
+ Validate documents with errors on category names (int, float and None)
+ As the input for this method comes from json, there can't be objects as keys
+ >>> e = __validate_categories({None: '123'})
+ >>> isinstance(e,HttpResponse)
+ True
+ >>> e = __validate_categories({123: 'text'})
+ >>> isinstance(e,HttpResponse)
+ True
+ >>> e = __validate_categories({42.5: 'value'})
+ >>> isinstance(e,HttpResponse)
+ True
+ """
+ for k, v in categories.iteritems():
+ ev = _encode_utf8(v)
+ ek = _encode_utf8(k)
+
+ if not isinstance(k,str) and not isinstance(k,unicode):
+ return HttpResponse('"Name for category %s is not a String"' % (k), status=400)
+
+ if not isinstance(v,str) and not isinstance(v,unicode):
+ return HttpResponse('"Value for category %s is not a String"' % (k), status=400)
+
+ if ek is None:
+ return HttpResponse('"Invalid name for category %s"' % (k), status=400)
+ if ev is None:
+ return HttpResponse('"Invalid content for category %s: %s"' % (k, v), status=400)
+
+def __parse_categories(categories):
+ parsed = {}
+ for k, v in categories.iteritems():
+ parsed[k] = _encode_utf8(v).strip()
+ return parsed
+
+
+def __validate_variables(variables):
+ """
+ Validates that variables for a document is a dict with string representations of positive ints as keys and floats as values
+
+ variable mappings without errors
+ >>> __validate_variables({"4":8, "5":2.5})
+ >>> __validate_variables({"1":2})
+ >>> __validate_variables({"10":2})
+ >>> # the next line is kinda valid. "2" can be parsed to float.
+ >>> __validate_variables({"1":"2"})
+
+
+ variable mappings with errors on keys
+ >>> e = __validate_variables({"-10":2})
+ >>> isinstance(e,HttpResponse)
+ True
+ >>> e = __validate_variables({"var1":2})
+ >>> isinstance(e,HttpResponse)
+ True
+ >>> e = __validate_variables({"10":2, "v3":4.5})
+ >>> isinstance(e,HttpResponse)
+ True
+
+ variable mappings with errors on values
+ >>> e = __validate_variables({"1":[2.5]})
+ >>> isinstance(e,HttpResponse)
+ True
+ >>> e = __validate_variables({"1":2, "2":{2.5:2}})
+ >>> isinstance(e,HttpResponse)
+ True
+
+ """
+ for k, v in variables.iteritems():
+ if not k.isdigit():
+ return HttpResponse('"Invalid variable index: %s. It should be integer."' % k, status=400)
+ try:
+ float(v)
+ except (ValueError, TypeError):
+ return HttpResponse('"Invalid variable value for index %s: %s. It should be a float"' % (k,v), status=400)
+
+
+def __parse_variables(variables):
+ parsed = {}
+ for k, v in variables.iteritems():
+ parsed[int(k)] = float(v)
+ return parsed
+
+def __validate_document(document):
+ if type(document) is not dict:
+ return HttpResponse('"Document should be a JSON object"', status=400)
+ if 'docid' not in document:
+ return HttpResponse('"Document should have a docid attribute"', status=400)
+ if 'fields' not in document:
+ return HttpResponse('"Document should have a fields attribute"', status=400)
+ response = None
+ response = response or __validate_docid(document['docid'])
+ response = response or __validate_fields(document['fields'])
+ if 'variables' in document:
+ response = response or __validate_variables(document['variables'])
+ if 'categories' in document:
+ response = response or __validate_categories(document['categories'])
+ if response:
+ return response
+
+def __parse_document(document):
+ document['docid'] = __parse_docid(document['docid'])
+ document['fields'] = __parse_fields(document['fields'])
+ if 'variables' in document:
+ document['variables'] = __parse_variables(document['variables'])
+ if 'categories' in document:
+ document['categories'] = __parse_categories(document['categories'])
+ return document
+
+def __validate_query(query):
+ """
+ Validates that a query is a string or a unicode.
+ It can't be empty.
+ Valid inputs
+ >>> __validate_query("a")
+ >>> __validate_query("\xc3\xb1")
+ >>> __validate_query(u"\xc3\xb1")
+
+ Validate emptyness
+ >>> e = __validate_query(" ")
+ >>> isinstance(e, HttpResponse)
+ True
+ >>> e = __validate_query("")
+ >>> isinstance(e, HttpResponse)
+ True
+ >>> e = __validate_query(" "*80)
+ >>> isinstance(e, HttpResponse)
+ True
+
+ Validate not supported types
+ >>> e = __validate_query(80.0)
+ >>> isinstance(e, HttpResponse)
+ True
+ >>> e = __validate_query([1,2,3])
+ >>> isinstance(e, HttpResponse)
+ True
+ >>> e = __validate_query({"a":"b"})
+ >>> isinstance(e, HttpResponse)
+ True
+
+ Validate None
+ >>> e = __validate_query(None)
+ >>> isinstance(e, HttpResponse)
+ True
+ """
+
+ if query is None:
+ return HttpResponse('"Invalid query. It cannot be NULL"', status=400)
+
+ if type(query) not in [str,unicode]:
+ return HttpResponse('"Invalid query. It MUST be a String"', status=400)
+
+ if query.strip() == '' :
+ return HttpResponse('"Invalid query. It cannot be a empty"', status=400)
+
+def __parse_query(query):
+ return _encode_utf8(query.lower())
+
+""" Argument validation decorators """
+required_index_name = non_empty_argument('index_name', 'Index name cannot be empty')
+
+""" Data validation decorators """
+required_categories_data = required_data('categories', __validate_categories, __parse_categories)
+required_variables_data = required_data('variables', __validate_variables, __parse_variables)
+required_fields_data = required_data('fields', __validate_fields, __parse_fields)
+required_docid_data = required_data('docid', __validate_docid, __parse_docid)
+optional_public_search_data = optional_data('public_search', __validate_boolean('public_search'))
+optional_docid_data = optional_data('docid', __validate_docid, __parse_docid)
+required_definition_data = required_data('definition')
+required_query_data = required_data('query', __validate_query, __parse_query)
+optional_variables_data = optional_data('variables', __validate_variables, __parse_variables)
+required_integer_function = int_argument('function', 'Function number should be a non-negative integer')
+
+def required_documents(func):
+ def decorated(self, request, data, **kwargs):
+ if type(data) is list:
+ if not data:
+ return HttpResponse('"Invalid batch insert. At least one document is required"', status=400)
+ for i in xrange(len(data)):
+ response = __validate_document(data[i])
+ if response:
+ response.content = '"Invalid batch insert, in document #%d of %d: %s"' % (i+1, len(data), response.content[1:-1])
+ return response
+ data[i] = __parse_document(data[i])
+ kwargs['batch_mode'] = True
+ kwargs['documents'] = data
+ else:
+ response = __validate_document(data)
+ if response:
+ return response
+ kwargs['batch_mode'] = False
+ kwargs['documents'] = [__parse_document(data)]
+ return func(self, request, data=data, **kwargs)
+ return decorated
+
+def required_docids(func):
+ def decorated(self, request, data, **kwargs):
+
+ if not data:
+ docids = request.GET.getlist('docid')
+ if docids:
+ if len(docids) > 1:
+ data = []
+ for docid in docids:
+ data.append({'docid':docid})
+ else:
+ data = {'docid':docids[0]}
+ else:
+ return HttpResponse('"If no body is given, you should include a docid argument in the querystring"', status=400)
+
+ if type(data) is list:
+ if not data:
+ return HttpResponse('"Invalid batch delete. At least one docid is required"', status=400)
+ for i in xrange(len(data)):
+ if not data[i].has_key('docid'):
+ return HttpResponse('"Invalid batch delete. Document #%d of %d doesn\'t have a docid parameter"' % (i+1, len(data)), status=400)
+
+ response = __validate_docid(data[i]['docid'])
+ if response:
+ response.content = '"Invalid batch delete, in docid #%d of %d: %s"' % (i+1, len(data), response.content[1:-1])
+ return response
+ data[i]['docid'] = __parse_docid(data[i]['docid'])
+ kwargs['bulk_mode'] = True
+ kwargs['documents'] = data
+ else:
+ if 'docid' in data:
+ response = __validate_docid(data['docid'])
+ if response:
+ response.content
+ return response
+ data['docid'] = __parse_docid(data['docid'])
+ kwargs['bulk_mode'] = False
+ kwargs['documents'] = [data]
+ else:
+ return HttpResponse('"Argument docid is required in the request body"', status=400)
+ return func(self, request, data=data, **kwargs)
+ return decorated
+
+""" Shared bulk delete code """
+def delete_docs_from_index(resource, index, documents):
+ """
+ resource: A Resource (from restresource.py). Needed just to LOG errors
+ index: The index to delete documents from. Every deploy for that index will be hit.
+ documents: list of documents to delete. **Need** 'docid' on each of them.
+ """
+ indexers = rpc.get_indexer_clients(index)
+
+ responses = []
+
+ for doc in documents:
+ ret = {'deleted': True}
+ try:
+ for indexer in indexers:
+ indexer.delDoc(doc['docid'])
+ except Exception:
+ resource.logger.exception('"Failed to delete %s on %s (%s)', doc['docid'], index.code, index.name)
+ resource.error_logger.exception('"Failed to delete %s on %s (%s)', doc['docid'], index.code, index.name)
+ ret['deleted'] = False
+ ret['error'] = '"Currently unable to delete the requested document"'
+
+ responses.append(ret)
+ return responses
+
+""" Util functions for request processing """
+def metadata_for_index(index):
+ return dict(code=index.code, creation_time=index.creation_time.isoformat(), started=index.is_ready(), size=index.current_docs_number, public_search=index.public_api, status=index.status)
+
+def build_logrecord_for_add(index, document):
+ return ttypes.LogRecord(index_code=index.code,
+ docid=document['docid'],
+ deleted=False,
+ fields=document['fields'],
+ variables=document.get('variables', {}),
+ categories=document.get('categories', {}))
+
+def build_logrecord_for_delete(index, document):
+ return ttypes.LogRecord(index_code=index.code,
+ docid=document['docid'],
+ deleted=True)
+log_writers = None
+log_addresses = None
+
+
+def send_log_storage_batch(resource, index, records):
+ global log_addresses
+ global log_writers
+ try:
+ storage_services = Service.objects.filter(name='storage')
+ if storage_services:
+ addresses = set((service.host, int(service.port), service.type) for service in storage_services)
+ if addresses != log_addresses:
+ log_writers = [(rpc.getReconnectingLogWriterClient(h,p), t == 'optional') for h,p,t in addresses]
+ log_addresses = addresses
+ for writer, optional in log_writers:
+ try:
+ writer.send_batch(ttypes.LogBatch(records=records))
+ except:
+ if optional:
+ resource.logger.exception('Optional storage failed to receive batch - IGNORING. %d records for index %s', len(records), index.code)
+ resource.error_logger.exception('Optional storage failed to receive batch - IGNORING. %d records for index %s', len(records), index.code)
+ else:
+ raise
+
+ return True
+ else:
+ resource.logger.error('No storage services found. %d records for index %s', len(records), index.code)
+ resource.error_logger.error('No storage services found. %d records for index %s', len(records), index.code)
+ return False
+ except:
+ resource.logger.exception('Error sending batch to log storage. %d records for index %s', len(records), index.code)
+ resource.error_logger.exception('Error sending batch to log storage. %d records for index %s', len(records), index.code)
+ return False
+
+
+"""
+ Version resource ======================================================
+"""
+class Version(Resource):
+ authenticated = False
+ def GET(self, request, version):
+ return HttpResponse('"API V %s : Documentation"' % version)
+
+"""
+ Indexes resource ======================================================
+"""
+class Indexes(Resource):
+ authenticated = True
+ def GET(self, request, version):
+ metadata = {}
+ for index in self.get_account().indexes.all():
+ metadata[index.name] = metadata_for_index(index)
+ # 200 OK : json of the indexes metadata
+ return JsonResponse(metadata)
+
+"""
+ Index resource ======================================================
+"""
+class Index(Resource):
+ authenticated = True
+
+ # gets index metadata
+ @required_index_name
+ @get_index_param_or404
+ def GET(self, request, version, index):
+ return JsonResponse(metadata_for_index(index))
+
+ # creates a new index for the given name
+ @required_index_name
+ @optional_public_search_data
+ def PUT(self, request, data, version, index_name, public_search=None):
+ account = self.get_account()
+
+ indexes = account.indexes.filter(name=index_name)
+ if indexes.count() > 1:
+ self.logger.exception('Inconsistent state: more than one index with name %s', index_name)
+ self.error_logger.exception('Inconsistent state: more than one index with name %s', index_name)
+
+ return HttpResponse('Unable to create/update your index. Please contact us.', status=500)
+ elif indexes.count() == 1:
+ if not public_search is None:
+ index = indexes[0]
+ index.public_api = public_search
+ index.save()
+
+ # 204 OK: index already exists
+ return HttpResponse(status=204)
+ else:
+ current_count = account.indexes.filter(deleted=False).count()
+ max_count = account.package.max_indexes
+
+ if not current_count < max_count:
+ msg = '"Unable to create. Too many indexes for the account (maximum: %d)"' % max_count
+ return HttpResponse(msg, status=409) #conflict
+
+ # basic index data
+ try:
+ index = account.create_index(index_name, public_search)
+ except IntegrityError:
+ return HttpResponse(status=204) # already exists
+
+ mail.report_new_index(index)
+ mixpanel_event_track('use', { 'plan' : account.package.code, 'use-type': 'index creation', 'distinct_id': account.user.email})
+
+ # 201 OK : index created
+ return JsonResponse(metadata_for_index(index), status=201)
+
+
+ @required_index_name
+ @get_index_param
+ def DELETE(self, request, data, version, index):
+ if not index:
+ return HttpResponse(status=204)
+ rpc.get_deploy_manager().delete_index(index.code)
+ mail.report_delete_index(index)
+ mixpanel_event_track('use', { 'plan' : index.account.package.code, 'use-type': 'index deletion', 'distinct_id': index.account.user.email})
+ return HttpResponse()
+
+
+"""
+ Document resource ======================================================
+"""
+class Document(Resource):
+ authenticated = True
+
+ @required_index_name
+ @required_querystring_argument('docid')
+ @get_index_param_or404
+ def GET(self, request, version, index, docid):
+ self.logger.debug('id=%s', docid)
+ doc = storage.storage_get(index.code, docid)
+ if doc is None:
+ raise Http404
+ self.set_message('Retrieved document %s' % docid)
+ return JsonResponse(doc)
+
+ def _insert_document(self, index, indexers, document):
+ tdoc = ttypes.Document(fields=document['fields'])
+ docid = document['docid']
+ variables = document.get('variables', {})
+ categories = document.get('categories', {})
+
+ success = { 'added': True }
+ try:
+ for indexer in indexers:
+ indexer.addDoc(docid, tdoc, int(tdoc.fields['timestamp']), variables)
+ if categories:
+ indexer.updateCategories(docid, categories)
+
+ except Exception, e:
+ self.logger.exception('Failed to index %s on %s (%s)', docid, index.code, index.name)
+ self.error_logger.exception('Failed to index %s on %s (%s)', docid, index.code, index.name)
+ success['added'] = False
+ success['error'] = '"Currently unable to index the requested document"'
+
+ return success
+
+ def _validate_variables(self, index, documents):
+ max_variables = index.configuration.get_data().get('max_variables')
+ for i in xrange(len(documents)):
+ if 'variables' in documents[i]:
+ for k in documents[i]['variables'].keys():
+ if k < 0 or k >= max_variables:
+ if len(documents) == 1:
+ return HttpResponse('"Invalid key in variables: \'%d\' (it should be in the range [0..%d]"' % (k, max_variables-1), status=400)
+ else:
+ return HttpResponse('"Invalid batch insert, in document #%d of %d: Invalid variable index %d. Valid keys are in range [0-%d]"' % (i+1, len(documents), k, max_variables-1), status=400)
+
+ @required_index_name
+ @required_documents
+ @get_index_param_or404
+ @wakeup_if_hibernated
+ def PUT(self, request, data, version, index, documents, batch_mode):
+ if batch_mode:
+ self.logger.debug('batch insert: %d docs', len(documents))
+ else:
+ self.logger.debug('id=%s fields=%s variables=%s categories=%s', documents[0].get('docid'), documents[0].get('fields'), documents[0].get('variables'), documents[0].get('categories'))
+
+ if not index.is_ready():
+ return HttpResponse('"Index is not ready."', status=409)
+
+ response = self._validate_variables(index, documents)
+ if response:
+ return response
+
+ rt = []
+
+ indexers = rpc.get_indexer_clients(index)
+
+ if LOG_STORAGE_ENABLED:
+ records = [build_logrecord_for_add(index, d) for d in documents]
+ if not send_log_storage_batch(self, index, records):
+ return HttpResponse('"Currently unable to insert the requested document(s)."', status=503)
+
+ for document in documents:
+ rt.append(self._insert_document(index, indexers, document))
+
+ if not batch_mode:
+ if rt[0]['added']:
+ return HttpResponse()
+ else:
+ return HttpResponse(rt[0]['error'], status=503)
+
+ return JsonResponse(rt)
+
+
+
+
+ @required_index_name
+ @required_docids
+ @get_index_param_or404
+ @wakeup_if_hibernated
+ def DELETE(self, request, data, version, index, documents, bulk_mode):
+ if bulk_mode:
+ self.logger.debug('bulk delete: %s docids', len(documents))
+ else:
+ self.logger.debug('id=%s qsid=%s', data['docid'], request.GET.get('docid'))
+
+ if not index.is_ready():
+ return HttpResponse('"Index is not ready"', status=409)
+
+ if LOG_STORAGE_ENABLED:
+ records = [build_logrecord_for_delete(index, d) for d in documents]
+
+ if not send_log_storage_batch(self, index, records):
+ return HttpResponse('"Currently unable to delete the requested document."', status=503)
+
+ indexers = rpc.get_indexer_clients(index)
+
+ responses = delete_docs_from_index(self, index, documents)
+
+ if not bulk_mode:
+ if responses[0]['deleted']:
+ return HttpResponse()
+ else:
+ return HttpResponse(responses[0]['error'], status=503)
+
+ return JsonResponse(responses)
+
+
+"""
+ Categories resource ======================================================
+"""
+class Categories(Resource):
+ authenticated = True
+
+ @required_index_name
+ @required_docid_data
+ @required_categories_data
+ @get_index_param_or404
+ @wakeup_if_hibernated
+ def PUT(self, request, data, version, categories, index, docid):
+ self.logger.debug('id=%s categories=%s', docid, categories)
+
+ if not index.is_writable():
+ return HttpResponse('"Index is not ready"', status=409)
+
+ if LOG_STORAGE_ENABLED:
+ records = [ttypes.LogRecord(index_code=index.code, docid=docid, deleted=False, categories=categories)]
+ if not send_log_storage_batch(self, index, records):
+ return HttpResponse('"Currently unable to update the requested document."', status=503)
+
+
+ indexers = rpc.get_indexer_clients(index)
+
+ failed = False
+ for indexer in indexers:
+ try:
+ indexer.updateCategories(docid, categories)
+ except Exception, e:
+ if isinstance(e, ttypes.IndextankException):
+ return HttpResponse(e.message, status=400)
+ else:
+ self.logger.exception('Failed to update variables for %s on %s (%s)', docid, index.code, index.name)
+ self.error_logger.exception('Failed to update variables for %s on %s (%s)', docid, index.code, index.name)
+ failed = True
+ break
+
+ if failed:
+ return HttpResponse('"Currently unable to update the categories for the requested document."', status=503)
+ else:
+ self.set_message('Updated categories for %s' % docid)
+ return HttpResponse()
+
+
+"""
+ Variables resource ======================================================
+"""
+class Variables(Resource):
+ authenticated = True
+
+ @required_index_name
+ @required_docid_data
+ @required_variables_data
+ @get_index_param_or404
+ @wakeup_if_hibernated
+ def PUT(self, request, data, version, variables, index, docid):
+ self.logger.debug('id=%s variables=%s', docid, variables)
+
+ if not index.is_writable():
+ return HttpResponse('"Index is not ready"', status=409)
+
+ if LOG_STORAGE_ENABLED:
+ records = [ttypes.LogRecord(index_code=index.code, docid=docid, deleted=False, variables=variables)]
+ if not send_log_storage_batch(self, index, records):
+ return HttpResponse('"Currently unable to update the requested document."', status=503)
+
+
+ indexers = rpc.get_indexer_clients(index)
+
+ failed = False
+ for indexer in indexers:
+ try:
+ indexer.updateBoost(docid, variables)
+ except Exception, e:
+ if isinstance(e, ttypes.IndextankException) and e.message.startswith('Invalid boost index'):
+ return HttpResponse(e.message.replace('boost', 'variable'), status=400)
+ else:
+ self.logger.exception('Failed to update variables for %s on %s (%s)', docid, index.code, index.name)
+ self.error_logger.exception('Failed to update variables for %s on %s (%s)', docid, index.code, index.name)
+ failed = True
+ break
+
+ if failed:
+ return HttpResponse('"Currently unable to index the requested document."', status=503)
+ else:
+ self.set_message('Updated variables for %s' % docid)
+ return HttpResponse()
+
+"""
+ Functions resource ======================================================
+"""
+class Functions(Resource):
+ # gets the functions metadata
+ @required_index_name
+ @get_index_param_or404
+ @wakeup_if_hibernated
+ def GET(self, request, version, index):
+ #asking for writability in a get sounds odd... but jorge and spike
+ #think it's ok for functions.
+ if not index.is_writable():
+ return HttpResponse('"Index not ready to list functions."', status=409)
+
+ indexer = rpc.get_indexer_clients(index)[0]
+ functions = indexer.listScoreFunctions()
+ return JsonResponse(functions)
+
+
+"""
+ Function resource ======================================================
+"""
+class Function(Resource):
+ authenticated = True
+
+
+ # TODO GET could return the function definition
+
+ # writes a function for the given number
+ @required_index_name
+ @required_integer_function
+ @required_definition_data
+ @get_index_param_or404
+ @wakeup_if_hibernated
+ def PUT(self, request, data, version, index, function, definition):
+ self.logger.debug('f=%d', function)
+
+ if (function < 0):
+ return HttpResponse('"Function index cannot be negative."', status=400)
+
+ if not index.is_writable():
+ return HttpResponse('"Index is not writable"', status=409)
+
+ indexers = rpc.get_indexer_clients(index)
+
+ failed = False
+ for indexer in indexers:
+ try:
+ indexer.addScoreFunction(function, definition)
+ except:
+ self.logger.warn('Failed to add function %s with definition: %s', function, data)
+ failed = True
+ break
+
+ if failed:
+ return HttpResponse('"Unable to add the requested function. Check your syntax."', status=400)
+ else:
+ sf, _ = ScoreFunction.objects.get_or_create(index=index, name=function)
+ sf.definition = definition
+ sf.save()
+
+ self.set_message('set to %s' % (definition))
+ return HttpResponse()
+
+ @required_index_name
+ @required_integer_function
+ @get_index_param_or404
+ @wakeup_if_hibernated
+ def DELETE(self, request, data, version, index, function):
+ self.logger.debug('f=%d', function)
+
+ if (function < 0):
+ return HttpResponse('"Function index cannot be negative."', status=400)
+
+ if not index.is_writable():
+ return HttpResponse('"Index is not writable"', status=409)
+
+ indexers = rpc.get_indexer_clients(index)
+
+ failed = False
+ for indexer in indexers:
+ try:
+ indexer.removeScoreFunction(function)
+ except:
+ self.logger.exception('Failed to remove function %s', function)
+ self.error_logger.exception('Failed to remove function %s', function)
+ failed = True
+ break
+
+ if failed:
+ return HttpResponse('"Failed to remove the requested function."', status=503)
+ else:
+ models.ScoreFunction.objects.filter(index=index, name=function).delete()
+ return HttpResponse()
+
+builtin_len = len
+"""
+ Search resource ======================================================
+"""
+class Search(Resource):
+ authenticated = False
+
+ @required_index_name
+ @required_querystring_argument('q')
+ @int_querystring('start')
+ @int_querystring('function')
+ @json_querystring('category_filters')
+ def DELETE(self, request, version, index_name, q, start=0, function=0, category_filters={}, data=None):
+ #kwarg 'data' is added in Resource.dispatch for 'DELETE' requests
+ self.logger.debug('f=%d pag=%d q=%s', function, start, q)
+ index = self.get_index_or_404(index_name)
+
+ authorize_response = self._check_authorize(request)
+ if authorize_response:
+ return authorize_response
+
+ if not index.is_readable():
+ return HttpResponse('"Index is not searchable"', status=409)
+
+ if not index.is_writable():
+ return HttpResponse('"Index is not writable"', status=409)
+ q = _encode_utf8(q)
+
+ if start > 5000:
+ return HttpResponse('"Invalid start parameters (start shouldn\'t exceed 5000)"', status=400)
+
+ query_vars = {}
+ for i in xrange(10):
+ k = 'var%d' % i
+ if k in request.GET:
+ try:
+ v = float(request.GET[k])
+ except ValueError:
+ return HttpResponse('"Invalid query variable, it should be a decimal number (var%d = %s)"', status=400)
+ query_vars[i] = v
+
+ # range facet_filters
+ function_filters = []
+ docvar_filters = []
+ docvar_prefix = ''
+ function_prefix = 'filter_function'
+
+ for key in request.GET.keys():
+ doc_match = re.match('filter_docvar(\\d+)',key)
+ func_match = re.match('filter_function(\\d+)',key)
+ if doc_match:
+ self.logger.info('got docvar filter: ' + request.GET[key])
+ extra_filters = self._get_range_filters(int(doc_match.group(1)), request.GET[key])
+ if extra_filters == None:
+ return HttpResponse('"Invalid document variable range filter (' + request.GET[key] + ')"', status=400)
+ docvar_filters.extend(extra_filters)
+ elif func_match:
+ extra_filters = self._get_range_filters(int(func_match.group(1)), request.GET[key])
+ if extra_filters == None:
+ return HttpResponse('"Invalid function range filter (' + request.GET[key] + ')"', status=400)
+ function_filters.extend(extra_filters)
+
+ facet_filters = []
+ for k,v in category_filters.items():
+ if type(v) is str or type(v) is unicode:
+ facet_filters.append(ttypes.CategoryFilter(k, v))
+ elif type(v) is list:
+ for element in v:
+ facet_filters.append(ttypes.CategoryFilter(k, element))
+ else:
+ return HttpResponse('"Invalid facets filter"', status=400)
+
+ extra_parameters = {}
+
+ searcher = rpc.get_searcher_client(index)
+ if searcher is None:
+ self.logger.warn('cannot find searcher for index %s (%s)', index.name, index.code)
+ self.error_logger.warn('cannot find searcher for index %s (%s)', index.name, index.code)
+ return HttpResponse('"Currently unable to perform the requested query"', status=503)
+
+ len = 1000
+ iterations = None
+ while True:
+ if iterations is not None:
+ iterations -= 1
+ if iterations < 0:
+ return HttpResponse('"Currently unable to perform the requested query - some or all documents may not have been deleted."', status=503)
+ try:
+ rs = searcher.search(q, start, len, function, query_vars, facet_filters, docvar_filters, function_filters, extra_parameters)
+ except ttypes.InvalidQueryException, iqe:
+ return HttpResponse('"Invalid query: %s"' % q, status=400)
+ except ttypes.MissingQueryVariableException, qve:
+ return HttpResponse('"%s"' % qve.message, status=400)
+ except ttypes.IndextankException, ite:
+ if ite.message == 'Invalid query':
+ return HttpResponse('"Invalid query: %s"' % q, status=400)
+ else:
+ self.logger.exception('Unexpected IndextankException while performing query')
+ self.error_logger.exception('Unexpected IndextankException while performing query')
+ if iterations is None:
+ return HttpResponse('"Currently unable to perform the requested query."', status=503)
+ else:
+ continue
+
+ if iterations is None:
+ iterations = ((rs.matches - start) / len) * 2
+
+ self.logger.debug('query delete: %s docids', rs.matches)
+
+ if LOG_STORAGE_ENABLED:
+ records = [build_logrecord_for_delete(index, d) for d in rs.docs]
+
+ if not send_log_storage_batch(self, index, records):
+ continue
+
+ delete_docs_from_index(self, index, rs.docs)
+ if (rs.matches - start) < len:
+ break
+
+ return HttpResponse()
+
+ @required_index_name
+ @required_querystring_argument('q')
+ @int_querystring('start')
+ @int_querystring('len')
+ @int_querystring('function')
+ @querystring_argument('fetch')
+ @querystring_argument('fetch_variables')
+ @querystring_argument('fetch_categories')
+ @querystring_argument('snippet')
+ @querystring_argument('snippet_type')
+ @json_querystring('category_filters')
+ @querystring_argument('callback')
+ @get_index_param_or404
+ @check_public_api
+ @wakeup_if_hibernated
+ def GET(self, request, version, index, q, start=0, len=10, function=0, fetch='', fetch_variables='', fetch_categories='', snippet='', snippet_type='', category_filters={}, callback=None):
+ self.logger.debug('f=%d pag=%d:%d q=%s snippet=%s fetch=%s fetch_variables=%s fetch_categories=%s ', function, start, start+len, q, snippet, fetch, fetch_variables, fetch_categories)
+
+ dymenabled = index.configuration.get_data().get('didyoumean')
+
+ if not index.is_readable():
+ return HttpResponse('"Index is not searchable"', status=409)
+
+ q = _encode_utf8(q)
+ q = self._sanitize_query(q)
+
+ if start + len > 5000:
+ return HttpResponse('"Invalid start and len parameters (start+len shouldn\'t exceed 5000)"', status=400)
+
+ query_vars = {}
+ for i in xrange(10):
+ k = 'var%d' % i
+ if k in request.GET:
+ try:
+ v = float(request.GET[k])
+ except ValueError:
+ return HttpResponse('"Invalid query variable, it should be a decimal number (var%d = %s)"', status=400)
+ query_vars[i] = v
+
+ # range facet_filters
+ function_filters = []
+ docvar_filters = []
+ docvar_prefix = ''
+ function_prefix = 'filter_function'
+
+ for key in request.GET.keys():
+ doc_match = re.match('filter_docvar(\\d+)',key)
+ func_match = re.match('filter_function(\\d+)',key)
+ if doc_match:
+ self.logger.info('got docvar filter: ' + request.GET[key])
+ extra_filters = self._get_range_filters(int(doc_match.group(1)), request.GET[key])
+ if extra_filters == None:
+ return HttpResponse('"Invalid document variable range filter (' + request.GET[key] + ')"', status=400)
+ docvar_filters.extend(extra_filters)
+ elif func_match:
+ extra_filters = self._get_range_filters(int(func_match.group(1)), request.GET[key])
+ if extra_filters == None:
+ return HttpResponse('"Invalid function range filter (' + request.GET[key] + ')"', status=400)
+ function_filters.extend(extra_filters)
+
+ facet_filters = []
+ for k,v in category_filters.items():
+ if type(v) is str or type(v) is unicode:
+ facet_filters.append(ttypes.CategoryFilter(k, v))
+ elif type(v) is list:
+ for element in v:
+ facet_filters.append(ttypes.CategoryFilter(k, element))
+ else:
+ return HttpResponse('"Invalid facets filter"', status=400)
+
+ extra_parameters = {}
+ if snippet:
+ extra_parameters['snippet_fields'] = snippet
+ if snippet_type:
+ extra_parameters['snippet_type'] = snippet_type
+ if fetch:
+ extra_parameters['fetch_fields'] = ','.join([f.strip() for f in fetch.split(',')])
+ if fetch_variables:
+ extra_parameters['fetch_variables'] = fetch_variables
+ if fetch_categories:
+ extra_parameters['fetch_categories'] = fetch_categories
+
+
+ searcher = rpc.get_searcher_client(index)
+ if searcher is None:
+ self.logger.warn('cannot find searcher for index %s (%s)', index.name, index.code)
+ self.error_logger.warn('cannot find searcher for index %s (%s)', index.name, index.code)
+ return HttpResponse('"Currently unable to perform the requested search"', status=503)
+
+ try:
+ t1 = time.time()
+ rs = searcher.search(q, start, len, function, query_vars, facet_filters, docvar_filters, function_filters, extra_parameters)
+ t2 = time.time()
+ except ttypes.InvalidQueryException, iqe:
+ return HttpResponse('"Invalid query: %s"' % q, status=400)
+ except ttypes.MissingQueryVariableException, qve:
+ return HttpResponse('"%s"' % qve.message, status=400)
+ except ttypes.IndextankException, ite:
+ if ite.message == 'Invalid query':
+ return HttpResponse('"Invalid query: %s"' % q, status=400)
+ else:
+ self.logger.exception('Unexpected IndextankException while performing query')
+ self.error_logger.exception('Unexpected IndextankException while performing query')
+ return HttpResponse('"Currently unable to perform the requested search"', status=503)
+
+
+ formatted_time = '%.3f' % (t2-t1)
+ for i in xrange(builtin_len(rs.docs)):
+ if rs.variables:
+ for k,v in rs.variables[i].iteritems():
+ rs.docs[i]['variable_%d' % k] = v
+ if rs.categories:
+ for k,v in rs.categories[i].iteritems():
+ rs.docs[i]['category_%s' % k] = v
+ if rs.scores:
+ rs.docs[i]['query_relevance_score'] = rs.scores[i]
+ rsp = dict(matches=rs.matches, results=rs.docs, search_time=formatted_time, facets=rs.facets, query=q)
+ #only add didyoumean to customers who have it enabled.
+ if dymenabled:
+ rsp['didyoumean'] = rs.didyoumean
+
+ return JsonResponse(rsp, callback=callback)
+
+ def _get_range_filters(self, id, filter_string):
+ filter_strings = filter_string.split(',')
+ range_filters = []
+ for filter in filter_strings:
+ parts = filter.split(':')
+ if (len(parts) != 2):
+ return None
+
+ if parts[0] == '*':
+ floor = 0
+ no_floor = True
+ else:
+ try:
+ floor = float(parts[0])
+ no_floor = False
+ except ValueError:
+ return None
+
+ if parts[1] == '*':
+ ceil = 0
+ no_ceil = True
+ else:
+ try:
+ ceil = float(parts[1])
+ no_ceil = False
+ except ValueError:
+ return None
+
+ range_filters.append(ttypes.RangeFilter(key=id, floor=floor, no_floor=no_floor, ceil=ceil, no_ceil=no_ceil))
+
+ return range_filters
+
+ def _sanitize_query(self, q):
+ s = self._try_to_sanitize_parentheses_and_quotes(q)
+ s = s.replace(r'{', r'\{')
+ s = s.replace(r'}', r'\}')
+ s = s.replace(r'[', r'\[')
+ s = s.replace(r']', r'\]')
+ s = s.replace(r'!', r'\!')
+ s = s.replace(r'?', r'\?')
+ s = s.replace(r'~', r'\~')
+ #s = s.replace(r'*', r'\*')
+ s = s.replace(r': ', r'\: ')
+ s = s.replace(r' :', r' \:')
+ s = s.replace(r'ssh:/', r'ssh\:/')
+ s = s.replace(r'ftp:/', r'ftp\:/')
+ s = s.replace(r'http:/', r'http\:/')
+ s = s.replace(r'https:/', r'https\:/')
+ if s != q:
+ self.logger.debug('query sanitized it was (%s) now (%s)', q, s)
+ return s
+
+ def _try_to_sanitize_parentheses_and_quotes(self, q):
+ # should solve most 'solveable' queries
+ lp = q.count('(')
+ rp = q.count(')')
+ while (lp > rp):
+ q = q.rstrip()
+ q += ')'
+ rp += 1
+ while (lp < rp):
+ q = '(' + q
+ lp += 1
+ while '()' in q:
+ q = q.replace(r'()',r'')
+ if (q.count('"') % 2):
+ q += '"'
+ return q
+
+"""
+ Promote resource ======================================================
+"""
+class Promote(Resource):
+ authenticated = True
+
+ @required_index_name
+ @required_docid_data
+ @required_query_data
+ @get_index_param_or404
+ @wakeup_if_hibernated
+ def PUT(self, request, data, version, index, docid, query):
+ self.logger.debug('id=%s q=%s', docid, query)
+
+ if not index.is_writable():
+ return HttpResponse('"Index is not writable"', status=409)
+
+ indexers = rpc.get_indexer_clients(index)
+
+ failed = False
+ for indexer in indexers:
+ try:
+ indexer.promoteResult(docid, query)
+ except:
+ self.logger.exception('"Failed to promote result %s for query %s"', docid, query)
+ self.error_logger.exception('"Failed to promote result %s for query %s"', docid, query)
+ failed = True
+ break
+
+ if failed:
+ return HttpResponse('"Currently unable to perform the requested promote."', status=503)
+ else:
+ return HttpResponse()
+
+"""
+ InstantLinks resource ======================================================
+"""
+class InstantLinks(Resource):
+ authenticated = False
+
+ @required_index_name
+ @required_querystring_argument('query', parse=_encode_utf8)
+ @querystring_argument('fetch', parse=_encode_utf8)
+ @querystring_argument('callback', parse=_encode_utf8)
+ @querystring_argument('field', parse=_encode_utf8)
+ @get_index_param_or404
+ @wakeup_if_hibernated
+ def GET(self, request, version, index, query, fetch='name', callback=None, field='name'):
+ self.logger.debug('query=%s callback=%s field=%s fetch=%s', query, callback, field, fetch)
+
+ if not index.is_readable():
+ return HttpResponse('"Index is not readable."', status=409)
+
+ searcher = rpc.get_searcher_client(index)
+ if searcher is None:
+ self.logger.warn('cannot find searcher for index %s (%s)', index.name, index.code)
+ return HttpResponse('"Currently unable to perform the requested search"', status=503)
+
+ suggestor = rpc.get_suggestor_client(index)
+ try:
+ suggestions = suggestor.complete(query, field)
+ if builtin_len(suggestions) == 0:
+ rsp = dict(matches=0, results=[],search_time='', facets={})
+ return JsonResponse(rsp, callback=callback)
+
+ # got suggestions .. build a query
+ print 'suggestions=%s' % suggestions
+ query = ' OR '.join( map( lambda x: "(%s)"%x, suggestions))
+ query = '%s:(%s)' % (field, query)
+ print 'query=%s' % query
+ extra_parameters = {}
+ extra_parameters['fetch_fields'] = fetch
+ try:
+ rs = searcher.search(query, 0, 4, 0, {}, {}, {}, {}, extra_parameters)
+ except ttypes.IndextankException, ite:
+ self.logger.exception('Unexpected IndextankException while performing query for instantlinks')
+ self.error_logger.exception('Unexpected IndextankException while performing query for instantlinks')
+ return HttpResponse('"Currently unable to perform the requested search"', status=503)
+ rsp = dict(matches=rs.matches, results=rs.docs, search_time='', facets=rs.facets)
+ return JsonResponse(rsp, callback=callback)
+ except:
+ self.logger.exception('Failed to provide instant links for "%s"', query)
+ self.error_logger.exception('Failed to provide instant links for "%s"', query)
+ return HttpResponse('"Currently unable to perform the requested instant links."', status=503)
+
+
+"""
+ AutoComplete resource ======================================================
+"""
+class AutoComplete(Resource):
+ authenticated = False
+
+ @required_index_name
+ @required_querystring_argument('query', parse=_encode_utf8)
+ @querystring_argument('callback', parse=_encode_utf8)
+ @querystring_argument('field', parse=_encode_utf8)
+ @get_index_param_or404
+ @wakeup_if_hibernated
+ def GET(self, request, version, index, query, callback=None, field='text'):
+ self.logger.debug('query=%s callback=%s', query, callback)
+
+ if not index.is_readable():
+ return HttpResponse('"Index is not readable."', status=409)
+
+ suggestor = rpc.get_suggestor_client(index)
+ try:
+ suggestions = suggestor.complete(query, field)
+ return JsonResponse({'suggestions': suggestions, 'query': query}, callback=callback)
+ except:
+ self.logger.exception('Failed to provide autocomplete for "%s"', query)
+ self.error_logger.exception('Failed to provide autocomplete for "%s"', query)
+ return HttpResponse('"Currently unable to perform the requested autocomplete."', status=503)
+
+
+
+
+"""
+ Provisioner for CloudFoundry. This for the transition until we adapt CF to conform to our default provisioner.
+"""
+class TransitionCloudFoundryProvisioner(ProvisionerResource):
+
+ @authorized_method
+ @required_querystring_argument("code")
+ def GET(self, request, code, **kwargs):
+ # fetch the account we want info about
+ account = Account.objects.get(apikey__startswith=code+"-")
+
+ # make sure the provisioner for the account is the same requesting its deletion
+ if self.provisioner != account.provisioner:
+ return HttpResponse('You are not the provisioner for this account', status=403)
+
+ # create the creds
+ creds = {
+ 'INDEXTANK_PRIVATE_API_URL' : account.get_private_apiurl(),
+ 'INDEXTANK_PUBLIC_API_URL' : account.get_public_apiurl(),
+ 'INDEXTANK_INDEX_NAMES' : [i.name for i in account.indexes.all()]
+ }
+
+ return JsonResponse({"code": account.get_public_apikey(), "config": creds})
+
+ @authorized_method
+ @required_data("plan")
+ def PUT(self, request, data, **kwargs):
+ # only default package right now
+ if data['plan'].upper() != "FREE":
+ return HttpResponse("Only FREE plan allowed", status=400)
+
+ account, _ = Account.create_account(datetime.datetime.now())
+ account.status = Account.Statuses.operational
+ account.provisioner = self.provisioner
+
+ # apply package. see validation above
+ account.apply_package(Package.objects.get(name=data['plan'].upper()))
+
+ account.save()
+
+ # create an index, using ApiClient
+ client = ApiClient(account.get_private_apiurl())
+ i = client.create_index("idx")
+
+ # ok, write credentials on response
+ creds = {
+ 'INDEXTANK_PRIVATE_API_URL' : account.get_private_apiurl(),
+ 'INDEXTANK_PUBLIC_API_URL' : account.get_public_apiurl(),
+ 'INDEXTANK_INDEX_NAMES': ["idx"]
+ }
+
+ return JsonResponse({"code": account.get_public_apikey(), "config": creds})
+
+ @authorized_method
+ @required_data("code")
+ def DELETE(self, request, data, **kwargs):
+ # fetch the account that should be deleted
+ account = Account.objects.get(apikey__startswith=data['code']+"-")
+
+ # make sure the provisioner for the account is the same requesting its deletion
+ if self.provisioner != account.provisioner:
+ return HttpResponse('You are not the provisioner for this account', status=403)
+
+ # ok, lets do it
+ account.close()
+
+ return HttpResponse(status=204)
+
+
+class BaseProvisioner(ProvisionerResource):
+ def _is_valid_provisioner(self, provisioner):
+ return True
+
+ def _validate(self, request, data, plan, **kwargs):
+ if not self._is_valid_provisioner(self.provisioner):
+ return HttpResponse('"Invalid provisioner"', status=400)
+ if self.provisioner.plans.filter(plan=plan.upper()).count() == 0:
+ return HttpResponse('"Plan %s NOT allowed"' % plan, status=400)
+
+ def _get_email(self, request, data, plan, **kwargs):
+ return None
+
+ def _get_id(self, account):
+ return account.get_public_apikey()
+
+ @authorized_method
+ @required_data("plan")
+ def POST(self, request, data, plan=None, **kwargs):
+ response = self._validate(request, data, plan, **kwargs)
+ if response:
+ return response
+
+ package = self.provisioner.plans.get(plan=plan.upper()).package
+ account, _ = Account.create_account(datetime.datetime.now(), email=self._get_email(request, data, plan, **kwargs))
+
+ account.status = Account.Statuses.operational
+ account.provisioner = self.provisioner
+ account.apply_package(package)
+ account.save()
+
+ mixpanel_event_track('signup', { 'plan' : package.code, 'signup_place': self.provisioner.name, 'distinct_id': account.user.email})
+
+ # create an index, using ApiClient
+ client = ApiClient(account.get_private_apiurl())
+ i = client.create_index("idx", options={ 'public_search':True })
+
+ # ok, write credentials on response
+ creds = {
+ 'INDEXTANK_API_URL' : account.get_private_apiurl(),
+ 'INDEXTANK_PRIVATE_API_URL' : account.get_private_apiurl(),
+ 'INDEXTANK_PUBLIC_API_URL' : account.get_public_apiurl(),
+ }
+
+ return JsonResponse({"id": self._get_id(account), "config": creds})
+
+ @authorized_method
+ def PUT(self, request, data, id=None):
+ return HttpResponse('{ "message": "Sorry, we do not support plan upgrades" }', status=503)
+
+
+class EmailProvisionerMixin(object):
+ def _validate(self, request, data, plan, **kwargs):
+ r = super(EmailProvisionerMixin, self)._validate(request, data, plan, **kwargs)
+ return r or self._validate_email(data.get('email', None))
+
+ def _validate_email(self, email):
+ if email is None:
+ return HttpResponse('"An email is required"', status=400)
+ try:
+ is_valid_email(email)
+ except:
+ return HttpResponse('"Invalid email!"', status=400)
+
+ def _get_email(self, request, data, plan, **kwargs):
+ return data['email']
+
+class SSOProvisionerMixin(object):
+ def GET(self, request, id=None):
+ timestamp = int(request.GET.get('timestamp',0))
+ token = request.GET.get('token','')
+ navdata = request.GET.get('nav-data','')
+ return HttpResponseRedirect("http://indextank.com/provider/resources/%s?token=%s×tamp=%s&nav-data=%s"%(id,token,timestamp,navdata)) # TODO fix this
+
+class DeleteProvisionerMixin(object):
+ def _get_account(self, id):
+ return Account.objects.get(apikey__startswith=("%s-" % id))
+
+ @authorized_method
+ def DELETE(self, request, data, id=None):
+ # fetch the account that should be deleted
+ account = self._get_account(id)
+
+ # make sure the provisioner for the account is the same requesting its deletion
+ if self.provisioner != account.provisioner:
+ return HttpResponse('You are not the provisioner for this account', status=403)
+
+ # ok, lets do it
+ account.close()
+
+ return HttpResponse()
+
+class FetchCredentialsProvisionerMixin(object):
+ @authorized_method
+ @required_querystring_argument("id")
+ def GET(self, request, code, **kwargs):
+ # fetch the account we want info about
+ account = self._get_account(id)
+
+ # make sure the provisioner for the account is the same requesting its deletion
+ if self.provisioner != account.provisioner:
+ return HttpResponse('You are not the provisioner for this account', status=403)
+
+ # create the creds
+ creds = {
+ 'INDEXTANK_PRIVATE_API_URL' : account.get_private_apiurl(),
+ 'INDEXTANK_PUBLIC_API_URL' : account.get_public_apiurl(),
+ 'INDEXTANK_INDEX_NAMES' : [i.name for i in account.indexes.all()]
+ }
+
+ return JsonResponse({"id": account.get_public_apikey(), "config": creds})
+
+
+class PublicProvisioner(EmailProvisionerMixin, BaseProvisioner):
+ pass
+
+class DisabledProvisioner(BaseProvisioner):
+ @authorized_method
+ @required_data("plan")
+ def POST(self, request, data, plan=None, **kwargs):
+ return HttpResponse('"Provisioning is currently disabled"', status=503)
+
+class CloudFoundryProvisioner(DeleteProvisionerMixin, FetchCredentialsProvisionerMixin, DisabledProvisioner):
+ def _is_valid_provisioner(self, provisioner):
+ return provisioner.name == 'cloudfoundry'
+
+class AppHarborProvisioner(DeleteProvisionerMixin, SSOProvisionerMixin, DisabledProvisioner):
+ def _is_valid_provisioner(self, provisioner):
+ return provisioner.name == 'appharbor'
+
+class HerokuProvisioner(DeleteProvisionerMixin, SSOProvisionerMixin, BaseProvisioner):
+ def _is_valid_provisioner(self, provisioner):
+ return provisioner.name == 'heroku'
+ def _get_id(self, account):
+ return account.id
+ def _get_account(self, id):
+ return Account.objects.get(id=id)
+
+def default(request):
+ return HttpResponse('"IndexTank API. Please refer to the documentation: http://indextank.com/documentation/api"')
+
+
+
diff --git a/api/restresource.py b/api/restresource.py
new file mode 100644
index 0000000..1cdbd36
--- /dev/null
+++ b/api/restresource.py
@@ -0,0 +1,544 @@
+import re
+import time
+import base64
+
+from django.http import Http404, HttpResponse, HttpResponseNotAllowed, HttpResponseServerError, HttpResponseNotFound
+from django.utils import simplejson as json
+from django.shortcuts import get_object_or_404
+
+from models import Account, Index, Provisioner
+from lib import encoder
+from lib.flaptor_logging import get_logger
+import logging
+
+class HttpMethodNotAllowed(Exception):
+ """
+ Signals that request.method was not part of
+ the list of permitted methods.
+ """
+
+class AccountFilter(logging.Filter):
+ def __init__(self, resource):
+ logging.Filter.__init__(self)
+ self.resource = resource
+
+ def filter(self, record):
+ record.prefix = ' $PIDCOLOR%s$RESET' % (self.resource.get_code())
+ if self.resource.executing:
+ # indent internal log lines
+ record.msg = ' ' + record.msg
+ else:
+ record.prefix += '$BOLD'
+ record.suffix = '$RESET'
+ return True
+
+class FakeLogger():
+ '''
+ Logger wrapper that adds:
+ - pid
+ - coloring
+ '''
+ def __init__(self, logger, resource, compname=None):
+ self.logger = logger
+ self.resource = resource
+ self.compname = compname or logger.name
+
+ def _add_extra(self, msg, kwargs):
+ prefix = ' $PIDCOLOR%s$RESET' % (self.resource.get_code())
+ suffix = ''
+ if self.resource.executing:
+ # indent internal log lines
+ msg = ' ' + msg
+ else:
+ prefix += '$BOLD'
+ suffix += '$RESET'
+ if 'extra' in kwargs:
+ kwargs['extra']['prefix'] = prefix
+ kwargs['extra']['suffix'] = suffix
+ kwargs['extra']['compname'] = self.compname
+ else:
+ kwargs['extra'] = dict(prefix=prefix, suffix=suffix, compname=self.compname)
+ return msg
+
+ def debug(self, msg, *args, **kwargs):
+ msg = self._add_extra(msg, kwargs)
+ self.logger.debug(msg, *args, **kwargs)
+
+ def info(self, msg, *args, **kwargs):
+ msg = self._add_extra(msg, kwargs)
+ self.logger.info(msg, *args, **kwargs)
+
+ def warn(self, msg, *args, **kwargs):
+ msg = self._add_extra(msg, kwargs)
+ self.logger.warn(msg, *args, **kwargs)
+
+ def error(self, msg, *args, **kwargs):
+ msg = self._add_extra(msg, kwargs)
+ self.logger.error(msg, *args, **kwargs)
+
+ def fatal(self, msg, *args, **kwargs):
+ msg = self._add_extra(msg, kwargs)
+ self.logger.fatal(msg, *args, **kwargs)
+
+ def exception(self, msg, *args, **kwargs):
+ msg = self._add_extra(msg, kwargs)
+ kwargs['exc_info'] = 1
+ self.logger.error(msg, *args, **kwargs)
+
+class Resource(object):
+ authenticated = False
+ mimetype = None
+ permitted_methods = 'GET'
+
+ def __init__(self):
+ self.__name__ = self.__class__.__name__
+ self._account = None
+ self.permitted_methods = []
+ if hasattr(self, 'GET'):
+ self.permitted_methods.append('GET')
+ if hasattr(self, 'POST'):
+ self.permitted_methods.append('POST')
+ if hasattr(self, 'PUT'):
+ self.permitted_methods.append('PUT')
+ if not hasattr(self, 'POST'):
+ self.permitted_methods.append('POST')
+ self.POST = self.PUT
+ if hasattr(self, 'DELETE'):
+ self.permitted_methods.append('DELETE')
+ self.logger = FakeLogger(get_logger(self.__name__), self)
+ self.error_logger = FakeLogger(get_logger('Errors'), self, compname=self.__name__)
+ self.executing = True
+
+ def dispatch(self, request, target, **kwargs):
+ '''
+ Delegates to the appropriate method (PUT, GET, DELETE)
+ if the derived class defines the handler
+ Returns and HttpMethodNotAllowed if not.
+ For all methods but get, it also parses the body as json, and
+ passes it to the handler method. Returns http 400 if it's
+ unparsable.
+ '''
+ request_method = request.method.upper()
+ if request_method not in self.permitted_methods:
+ raise HttpMethodNotAllowed
+
+ if request_method == 'GET':
+ return target.GET(request, **kwargs)
+ elif request_method == 'POST' or request_method == 'PUT' or request_method == 'DELETE':
+ data = request._get_raw_post_data()
+ if data:
+ try:
+ data = json.loads(data)
+ except ValueError, e:
+ return HttpResponse('"Invalid JSON input: %s"' % e, status=400)
+ else:
+ data = {}
+ if request_method == 'DELETE':
+ return target.DELETE(request, data=data, **kwargs)
+ if request_method == 'POST':
+ return target.POST(request, data=data, **kwargs)
+ else:
+ return target.PUT(request, data=data, **kwargs)
+ else:
+ raise Http404
+
+ def get_code(self):
+ '''
+ Returns the account code (the public part of
+ the api-key
+ '''
+ return self._code
+
+ def get_account(self):
+ '''
+ Returns an Account object for this call.
+ If account cannot be validated, it returns 404.
+ '''
+ if self._account_id and not self._account:
+ self._account = get_object_or_404(Account, pk=self._account_id)
+ return self._account
+
+ def get_index(self, index_name):
+ '''
+ Returns an Index Object for this call.
+ If and index with index_name cannot be found for this account,
+ it returns None.
+ '''
+ idx = Index.objects.filter(account=self._account_id, name=index_name)
+ if len(idx):
+ return idx[0]
+ else:
+ return None
+
+ def get_index_or_404(self, index_name):
+ return get_object_or_404(Index, account=self._account_id, name=index_name)
+
+ def __parse_account(self, request):
+ host = request.get_host()
+ code = re.search(r'([^\.]+)\.api\..*', host)
+ self._code = None
+ self._account_id = None
+ self._account = None
+ if code:
+ self._code = code.group(1)
+ self._account_id = encoder.from_key(self._code)
+
+ def _authorize(self, request, force=False):
+ auth = request.META.get('HTTP_AUTHORIZATION')
+ if self._account_id and auth:
+ auth = auth.split()
+ if auth and len(auth) > 1 and auth[0].lower() == "basic":
+ parts = base64.b64decode(auth[1]).split(':')
+ if len(parts) == 2:
+ key = '%s-%s' % (self._code, parts[1])
+ #self.logger.debug('AUTH: comparing %s vs %s', key, self.get_account().apikey)
+ if self.get_account().apikey == key:
+ return True
+ return False
+
+ def __call__(self, request, **kwargs):
+ self._start_time = time.time()
+ self.__parse_account(request)
+
+ self.message = None
+
+ response = HttpResponseServerError('Unreachable response')
+
+
+ authorize_response = self.validate_request(request)
+
+ if authorize_response:
+ response = authorize_response
+ else:
+ try:
+ response = self.dispatch(request, self, **kwargs)
+ except HttpMethodNotAllowed:
+ response = HttpResponseNotAllowed(self.permitted_methods)
+ response.mimetype = self.mimetype
+ except Http404:
+ response = HttpResponseNotFound()
+ except Exception:
+ self.logger.exception('Unexpected error while processing request')
+ self.error_logger.exception('Unexpected error while processing request')
+ response = HttpResponseServerError('Unexpected error')
+ elapsed_time = time.time() - self._start_time
+
+ if self.message is None:
+ self.message = response.content[:120]
+
+ self.executing = False
+ self.logger.info('[%d] in %.3fs for [%s %s] : %s', response.status_code, elapsed_time, request.method, request.path, self.message)
+ self.executing = True
+ return response
+
+ def _check_authorize(self, request):
+ """
+ Checks authorization. Returns None if ok and the proper response
+ if it does not pass.
+ """
+ response = None
+ if not self._authorize(request):
+ response = HttpResponse('"Authorization Required"', mimetype=self.mimetype, status=401)
+ response['WWW-Authenticate'] = 'Basic realm=""'
+
+ return response
+
+ def validate_request(self, request):
+ response = None
+ if self.authenticated:
+ response = self._check_authorize(request)
+ if response is None and self.get_account().status != Account.Statuses.operational:
+ response = HttpResponse('"Account not active"', mimetype=self.mimetype, status=409)
+ return response
+
+ def set_message(self, message):
+ self.message = message
+
+ @classmethod
+ def as_view(cls, **initkwargs):
+ def view(request, *args, **kwargs):
+ instance = cls(**initkwargs)
+ return instance(request, *args, **kwargs)
+ return view
+
+
+class JsonResponse(HttpResponse):
+ def __init__(self, json_object, *args, **kwargs):
+ body = json.dumps(json_object)
+ if not ('mimetype' in kwargs and kwargs['mimetype']):
+ if 'callback' in kwargs and kwargs['callback']:
+ kwargs['mimetype'] = 'application/javascript'
+ else:
+ kwargs['mimetype'] = 'application/json'
+
+ if 'callback' in kwargs:
+ callback = kwargs.pop('callback')
+ if callback:
+ body = '%s(%s)' % (callback, body)
+ super(JsonResponse, self).__init__(body, *args, **kwargs)
+
+class ProvisionerResource(Resource):
+ provisioner = None
+
+ def get_code(self):
+ '''
+ Returs a human readable string identifying the provider.
+ '''
+ return self.provisioner.name if self.provisioner else '?????'
+
+ def _authorize(self, request):
+ auth = request.META.get('HTTP_AUTHORIZATION')
+ if auth:
+ auth = auth.split()
+ if auth and len(auth) > 1 and auth[0].lower() == "basic":
+ parts = base64.b64decode(auth[1]).split(':')
+ if len(parts) == 2:
+ token = parts[1]
+ provisioner = Provisioner.objects.filter(token=token)
+ if provisioner:
+ self.provisioner = provisioner[0]
+ return True
+ return False
+
+ def validate_request(self, request):
+ return None
+
+
+"""
+ Standard validators and parsers
+"""
+
+def int_validator(message, status=400):
+ '''
+ Returns a validation function that, on error,
+ return an HttpResponse with the given status code
+ and message.
+
+ >>> validator = int_validator('foo', status=401)
+ >>> validator(1)
+ >>> response = validator('bar')
+ >>> response.code
+ 401
+
+ '''
+ def validate_int(string):
+ if not string.isdigit():
+ return HttpResponse(message, status=status)
+ return validate_int
+
+def json_validator(message, status=400):
+ '''
+ Return a validation function for json, that on error
+ returns an HttpResponse with the given status code and
+ message
+
+ >>> validator = json_validator('foo', status=401)
+ >>> validator('{"menu": {"foo": "bar", "bar": "foo"}}')
+ >>> response = validator("{'menu': {'foo': 'bar', 'bar': 'foo'}}")
+ >>> response.code
+ 401
+
+ '''
+ def validate_json(string):
+ try:
+ data = json.loads(string)
+ except ValueError, e:
+ return HttpResponse(message, status=status)
+ return validate_json
+
+ALWAYS_VALID = lambda x: None
+IDENTITY = lambda x: x
+
+"""
+ Data extraction and validation for PUT requests decorators
+"""
+def optional_data(name, validate=ALWAYS_VALID, parse=IDENTITY):
+ return __check_data(name, validate, parse, False)
+def required_data(name, validate=ALWAYS_VALID, parse=IDENTITY):
+ return __check_data(name, validate, parse, True)
+
+def __check_data(name, validate, parse, required):
+ def decorate(func):
+ def decorated(self, request, data, **kwargs):
+ if name in data:
+ response = validate(data[name])
+ if response:
+ return response
+ kwargs[name] = parse(data[name])
+ elif required:
+ return HttpResponse('"Argument %s is required in the request body"' % name, status=400)
+ return func(self, request, data=data, **kwargs)
+ return decorated
+ return decorate
+
+"""
+ Keyword argument validation decorators
+"""
+def validate_argument(name, validate, parse):
+ '''
+ Decorator that validates a request arguments, and returns a
+ parsed version.
+ Arguments:
+ name -- The name of the parameter to validate.
+ validate -- a validation function that returns None for sucess
+ or an HttpResponse for failure
+ parse -- The parsing function.
+ '''
+ def decorate(func):
+ def decorated(self, request, **kwargs):
+ if name in kwargs:
+ response = validate(kwargs[name])
+ if response:
+ return response
+ kwargs[name] = parse(kwargs[name])
+ return func(self, request, **kwargs)
+ return decorated
+ return decorate
+
+def utf8_argument(name, message=None, status=400):
+ '''
+ Decorator that validates and parses utf-8 arguments into str.
+ Arguments:
+ message -- ignored
+ status -- ignored
+ '''
+ def validate_utf8(string):
+ try:
+ string.encode('utf-8')
+ except:
+ return HttpResponse('"%s should be valid utf-8. Was: %s"' % (name, string))
+ def encode_utf8(string):
+ return string.encode('utf-8')
+ return validate_argument(name, validate_utf8, encode_utf8)
+
+def int_argument(name, message=None, status=400):
+ return validate_argument(name, int_validator(message, status), int)
+
+def non_empty_argument(name, message, status=400):
+ def validate_non_empty(string):
+ if string.strip() == '':
+ return HttpResponse(message, status=status)
+ return validate_argument(name, validate_non_empty, IDENTITY)
+
+"""
+ GET querystring argument validation decorators
+"""
+def required_querystring_argument(name, validate=ALWAYS_VALID, parse=IDENTITY):
+ def decorate(func):
+ def decorated(self, request, **kwargs):
+ if name in request.GET:
+ response = validate(request.GET[name])
+ if response:
+ return response
+ kwargs[name] = parse(request.GET[name])
+ else:
+ return HttpResponse('"Argument %s is required in the QueryString"' % (name), status=400)
+ return func(self, request, **kwargs)
+ return decorated
+ return decorate
+
+def querystring_argument(name, validate=ALWAYS_VALID, parse=IDENTITY):
+ def decorate(func):
+ def decorated(self, request, **kwargs):
+ if name in request.GET:
+ response = validate(request.GET[name])
+ if response:
+ return response
+ kwargs[name] = parse(request.GET[name])
+ return func(self, request, **kwargs)
+ return decorated
+ return decorate
+
+def int_querystring(name, message=None, status=400):
+ if not message:
+ message = 'Query string argument %s should be a non negative integer.' % name
+ return querystring_argument(name, int_validator(message, status), int)
+
+def json_querystring(name, message=None, status=400):
+ if not message:
+ message = 'Query string argument %s should be a valid json.' % name
+ return querystring_argument(name, json_validator(message, status), json.loads)
+
+
+def authorized_method(func):
+ '''
+ Decorator that forces request to be authorized.
+ Using it on a method ensures that in the case that the
+ user isn't authorized, the body of the method won't be run.
+ '''
+ def decorated(self, request, **kwargs):
+ return self._check_authorize(request) or func(self, request, **kwargs)
+ return decorated
+
+def check_public_api(func):
+ '''
+ Checks that the defined index has public access enabled OR
+ that the request is (privately) authorized.
+
+ Depends on the "index" param so get_index_param_or404 should
+ be called first.
+
+ In case that the index doesn't have public access enabled and
+ the request is not authorized, it return 404.
+ '''
+ def decorated(self, request, **kwargs):
+ if 'index' in kwargs:
+ index = kwargs['index']
+ if not index.public_api:
+ authorize_response = self._check_authorize(request)
+ if authorize_response:
+ return authorize_response
+ else:
+ return HttpResponse('"Index name cannot be empty"', status=400)
+ return func(self, request, **kwargs)
+ return decorated
+
+def get_index_param(func):
+ '''
+ Decorator that validates the existence of an index_name
+ param, and parses it into the "index" variable.
+ If no index with that name exist, it parses "None" into
+ the index param, but the decorated function does run.
+ '''
+ def decorated(self, request, **kwargs):
+ if 'index_name' in kwargs:
+ index = self.get_index(kwargs['index_name'])
+ kwargs['index'] = index
+ del kwargs['index_name']
+ return func(self, request, **kwargs)
+ return decorated
+
+def get_index_param_or404(func):
+ '''
+ Decorator that validates the existence of an index_name
+ param, and parses it into the "index" variable.
+ In case of error, an 404 HttpResponse is returned and
+ the decorated function doesn't run.
+ '''
+ def decorated(self, request, **kwargs):
+ if 'index_name' in kwargs:
+ index = self.get_index_or_404(kwargs['index_name'])
+ kwargs['index'] = index
+ del kwargs['index_name']
+ return func(self, request, **kwargs)
+ return decorated
+
+def wakeup_if_hibernated(func):
+ '''
+ Decorator that makes sure an index is not hibernated.
+ It takes the "index" argument from the request, and checks
+ the index's state. If it was hibernated, it triggers wake up
+ and returns a 503 response.
+ '''
+ def decorated(self, request, **kwargs):
+ if 'index' in kwargs:
+ index = kwargs['index']
+ if index.status in (Index.States.hibernated, Index.States.waking_up):
+ if index.status == Index.States.hibernated:
+ index.update_status(Index.States.waking_up)
+ return HttpResponse('"Wellcome back! Your index has been hibernating and is now waking up. Please retry in a few seconds"', status=503)
+ return func(self, request, **kwargs)
+ else:
+ return HttpResponse('"Index name cannot be empty"', status=400)
+ return decorated
+
diff --git a/api/rpc.py b/api/rpc.py
new file mode 100644
index 0000000..2fd7649
--- /dev/null
+++ b/api/rpc.py
@@ -0,0 +1,169 @@
+from flaptor.indextank.rpc import Indexer, Searcher, Suggestor, Storage, LogWriter, WorkerManager,\
+ DeployManager, Controller, FrontendManager
+
+from flaptor.indextank.rpc.ttypes import NebuException, IndextankException
+
+''' ===========================
+ THRIFT STUFF
+ =========================== '''
+from thrift.transport import TSocket, TTransport
+from thrift.protocol import TBinaryProtocol
+from lib import flaptor_logging, exceptions
+from thrift.transport.TTransport import TTransportException
+from socket import socket
+from socket import error as SocketError
+
+
+logger = flaptor_logging.get_logger('RPC')
+
+# Missing a way to close transport
+def getThriftControllerClient(host, timeout_ms=None):
+ protocol, transport = __getThriftProtocolTransport(host,19010, timeout_ms)
+ client = Controller.Client(protocol)
+ transport.open()
+ return client
+
+# Missing a way to close transport
+def getThriftIndexerClient(host, base_port, timeout_ms=None):
+ protocol, transport = __getThriftProtocolTransport(host, base_port + 1, timeout_ms)
+ client = Indexer.Client(protocol)
+ transport.open()
+ return client
+
+def getThriftSearcherClient(host, base_port, timeout_ms=None):
+ protocol, transport = __getThriftProtocolTransport(host, base_port + 2, timeout_ms)
+ client = Searcher.Client(protocol)
+ transport.open()
+ return client
+
+def getThriftSuggestorClient(host, base_port):
+ protocol, transport = __getThriftProtocolTransport(host, base_port + 3)
+ client = Suggestor.Client(protocol)
+ transport.open()
+ return client
+
+storage_port = 10000
+def getThriftStorageClient():
+ protocol, transport = __getThriftProtocolTransport('storage',storage_port)
+ client = Storage.Client(protocol)
+ transport.open()
+ return client
+
+def getThriftLogWriterClient(host, port, timeout_ms=500):
+ protocol, transport = __getThriftProtocolTransport(host,port,timeout_ms)
+ client = LogWriter.Client(protocol)
+ transport.open()
+ return client
+
+def getThriftLogReaderClient(host, port, timeout_ms=None):
+ protocol, transport = __getThriftProtocolTransport(host,port,timeout_ms)
+ client = LogWriter.Client(protocol)
+ transport.open()
+ return client
+
+class ReconnectingClient:
+ def __init__(self, factory):
+ self.factory = factory
+ self.delegate = None #factory()
+
+ def __getattr__(self, name):
+ import types
+ if self.delegate is None:
+ self.delegate = self.factory()
+ att = getattr(self.delegate, name)
+ if type(att) is types.MethodType:
+ def wrap(*args, **kwargs):
+ try:
+ return att(*args, **kwargs)
+ except (NebuException, IndextankException):
+ logger.warn('raising catcheable exception')
+ raise
+ except (TTransportException, IOError, SocketError):
+ logger.warn('failed to run %s, reconnecting once', name)
+ self.delegate = self.factory()
+ att2 = getattr(self.delegate, name)
+ return att2(*args, **kwargs)
+ except Exception:
+ logger.exception('Unexpected failure to run %s, reconnecting once', name)
+ self.delegate = self.factory()
+ att2 = getattr(self.delegate, name)
+ return att2(*args, **kwargs)
+
+ return wrap
+ else:
+ return att
+
+def getReconnectingStorageClient():
+ return ReconnectingClient(getThriftStorageClient)
+
+def getReconnectingLogWriterClient(host, port):
+ return ReconnectingClient(lambda: getThriftLogWriterClient(host, port))
+
+worker_manager_port = 8799
+def getThriftWorkerManagerClient(host):
+ protocol, transport = __getThriftProtocolTransport(host,worker_manager_port)
+ client = WorkerManager.Client(protocol)
+ transport.open()
+ return client
+
+deploymanager_port = 8899
+def get_deploy_manager():
+ protocol, transport = __getThriftProtocolTransport('deploymanager',deploymanager_port)
+ client = DeployManager.Client(protocol)
+ transport.open()
+ return client
+
+
+def __getThriftProtocolTransport(host, port=0, timeout_ms=None):
+ ''' returns protocol,transport'''
+ # Make socket
+ transport = TSocket.TSocket(host, port)
+
+ if timeout_ms is not None:
+ transport.setTimeout(timeout_ms)
+
+ # Buffering is critical. Raw sockets are very slow
+ transport = TTransport.TBufferedTransport(transport)
+
+ # Wrap in a protocol
+ protocol = TBinaryProtocol.TBinaryProtocol(transport)
+ return protocol, transport
+
+
+def get_searcher_client(index, timeout_ms=None):
+ '''
+ This method returns a single searcherclient, or None
+ '''
+ deploy = index.searchable_deploy()
+ if deploy:
+ return getThriftSearcherClient(deploy.worker.lan_dns, int(deploy.base_port), timeout_ms)
+ else:
+ return None
+
+def get_worker_controller(worker, timeout_ms=None):
+ return getThriftControllerClient(worker. lan_dns)
+
+def get_suggestor_client(index):
+ '''
+ This method returns a single suggestorclient, or None
+ '''
+ deploy = index.searchable_deploy()
+ if deploy:
+ return getThriftSuggestorClient(deploy.worker.lan_dns, int(deploy.base_port))
+ else:
+ return None
+
+def get_indexer_clients(index, timeout_ms=1000):
+ '''
+ This method returns the list of all indexerclients that should be updated
+ on add,delete,update, and category updates.
+ @raise exceptions.NoIndexerException if this index has no writable deploy.
+ '''
+ deploys = index.indexable_deploys()
+ retval = []
+ for d in deploys:
+ retval.append(getThriftIndexerClient(d.worker.lan_dns, int(d.base_port), timeout_ms))
+ if retval:
+ return retval
+ else:
+ raise exceptions.NoIndexerException()
diff --git a/api/safe_reload.sh b/api/safe_reload.sh
new file mode 100755
index 0000000..d5a7368
--- /dev/null
+++ b/api/safe_reload.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+# kill all api workers (those that match the pattern but are not `cat pid`)
+# the master should then take care of respawning workers
+echo "slowly killing workers and allowing the master to respawn them"
+for pid in `pgrep -f 'api-uwsgi' | grep -v \`cat pid\``; do
+ count=`pgrep -f 'api-uwsgi' | grep -v \`cat pid\` | wc -l`
+ echo "killing $pid - live workers: $count"
+ kill $pid
+ sleep 1
+done
\ No newline at end of file
diff --git a/api/settings.py b/api/settings.py
new file mode 100644
index 0000000..15ef978
--- /dev/null
+++ b/api/settings.py
@@ -0,0 +1,80 @@
+# Django settings for burbio project.
+
+from os import environ
+
+LOCAL = (environ.get('DJANGO_LOCAL') == '1')
+
+DEBUG = False
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+ # ('Your Name', 'your_email@domain.com'),
+)
+
+MANAGERS = ADMINS
+
+DATABASE_ENGINE = 'mysql' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'ado_mssql'.
+DATABASE_NAME = 'indextank' # Or path to database file if using sqlite3.
+DATABASE_USER = '****' # Not used with sqlite3.
+DATABASE_PASSWORD = '****' # Not used with sqlite3.
+DATABASE_HOST = 'database' # Set to empty string for localhost. Not used with sqlite3.
+DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
+
+# Local time zone for this installation. Choices can be found here:
+# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
+# although not all choices may be available on all operating systems.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = 'Etc/GMT+0'
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'en-us'
+
+SITE_ID = 1
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+ 'django.template.loaders.filesystem.load_template_source',
+ 'django.template.loaders.app_directories.load_template_source',
+# 'django.template.loaders.eggs.load_template_source',
+)
+
+MIDDLEWARE_CLASSES = (
+ 'api.lib.error_logging.ViewErrorLoggingMiddleware',
+ 'django.middleware.gzip.GZipMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.locale.LocaleMiddleware',
+)
+
+ROOT_URLCONF = 'api.urls'
+
+TEMPLATE_DIRS = (
+ 'templates'
+ # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
+ # Always use forward slashes, even on Windows.
+ # Don't forget to use absolute paths, not relative paths.
+)
+
+INSTALLED_APPS = (
+ 'api',
+ 'django.contrib.contenttypes',
+ 'django.contrib.auth'
+)
+
+STORAGE_ENV = 'PROD'
+
+STATIC_URLS = [ '/_static' ]
+ALLOWED_INCLUDE_ROOTS = ('static')
+
+USER_COOKIE_NAME = "pf_user"
+COMMON_DOMAIN = 'localhost'
+#SESSION_COOKIE_DOMAIN = COMMON_DOMAIN
+FORCE_SCRIPT_NAME = ''
+USE_MULTITHREADED_SERVER = True
+LOGGER_CONFIG_FILE='logging.conf'
+
+EMAIL_HOST='localhost'
+EMAIL_PORT=25
+EMAIL_HOST_USER='email%localhost'
+EMAIL_HOST_PASSWORD='****'
diff --git a/api/start_webapp.sh b/api/start_webapp.sh
new file mode 100755
index 0000000..9243e21
--- /dev/null
+++ b/api/start_webapp.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+./dostartapi.sh
+rm -f DONTSTART
diff --git a/api/static/README.v1 b/api/static/README.v1
new file mode 100644
index 0000000..33c8e1e
--- /dev/null
+++ b/api/static/README.v1
@@ -0,0 +1,104 @@
+Indextank API Version 1.0
+-------------------------
+
+Each IndexTank account has its own domain which can be found in the account's dashboard.
+This domain looks like this:
+ http://.api.indextank.com/
+We'll call this PUBLIC_DOMAIN
+
+This public domain can be authenticated via Basic auth:
+ http://:@.api.indextank.com/
+We'll call this AUTHENTICATED_DOMAIN
+
+Calls to the AUTHENTICATED_DOMAIN will return 401 if the authentication fails.
+
+Available methods
+-----------------
+
+GET http://api.indextank.com/v1
+ Returns this README file
+
+GET /v1/indexes
+ Returns a JSON map from index_name to their metadata
+
+PUT /v1/indexes/
+ Creates an index for the given name.
+ 201: It has been created and returns a JSON string with the new index code
+ 204: There already existed and index for that name
+ 409: The max number of indexes has been reached for the account
+
+GET /v1/indexes/
+ Returns JSON metadata for that index
+ 404: if it doesn't exist
+
+DELETE /v1/indexes/
+ Deletes the index for the given name
+ 204: if it didn't exist
+
+PUT /v1/indexes//docs
+ Expects the following arguments in the JSON body
+ - docid: a string with the document identifier
+ - fields: map from field names to their content
+ - variables: map from variable index to their float value
+ Indexes the given document in using the given docid and fields
+
+DELETE /v1/indexes//docs
+ Expects the following arguments in the JSON body
+ - docid: a string with the document identifier
+ Removes the document from the index.
+
+PUT /v1/indexes//docs/variables
+ Expects the following parameters in the JSON body
+ - docid: a string with the document identifier
+ - variables: map from variable index to their float value
+ Update the variables of the document.
+
+GET /v1/indexes//search
+ Searches the given query in that index
+ Required parameters in querystring:
+ - q: the query to perform
+ Optional parameters in querystring:
+ - start: integer with the first result to return (default is 0)
+ - len: integer with the number of results to return (default is 10)
+ - function: index of the relevance function to be used
+ - snippet: comma separated list of field names for which to return snippets (requires snipettinng feature)
+ - fetch: comma separated list of field names for which to return the full content (requires snipettinng feature)
+ 400: if the query is invalid
+
+PUT /v1/indexes//promote
+ Expects the following parameters in the JSON body
+ - docid: a string with the document identifier
+ - query: the query for which to promote the document
+ Promotes the document to the top of the given
+
+GET /v1/indexes//search//functions
+ Returns a list of the relevance function indexes and their definitions
+
+PUT /v1/indexes//search//functions/
+ Expects a JSON string with the function definition in the body
+ Updates the function to the given definition
+ 400: if the definition is invalid
+
+DELETE /v1/indexes//search//functions/
+ Removes the definition of the function
+
+GET /v1/indexes//stats
+ Returns usage stats for the given index
+
+GET /v1/indexes//query_log
+ Returns a portion of the query log for the given index
+
+GET /v1/indexes//autocomplete
+ Required parameters in querystring:
+ - fragment: the fragment for which to provide autocomplete suggestions
+ Optional parameters in querystring:
+ - callback: the name of the callback function. This causes the output to
+ be JSONP
+ Returns a JSON with the suggested queries for the given fragment
+ Intended to be used with AJAX.
+ NOTE: this request in unauthenticated since it needs to be made public
+ to function. if an authenticated request is made it will be
+ rejected to prevent private key exposition. If you suspect your
+ key has been expose please regenerate it in your dashboard and
+ update your clients accordingly.
+
\ No newline at end of file
diff --git a/api/storage.py b/api/storage.py
new file mode 100644
index 0000000..2e8614d
--- /dev/null
+++ b/api/storage.py
@@ -0,0 +1,107 @@
+
+''' =========================
+ Interaction with SimpleDB
+ ========================= '''
+
+import boto
+import hashlib
+import zlib
+import time
+import sys
+import traceback
+import datetime
+
+from amazon_credential import AMAZON_USER, AMAZON_PASSWORD
+
+from django.conf import settings
+
+def get_connection():
+ return boto.connect_sdb(AMAZON_USER, AMAZON_PASSWORD)
+
+def get_ids(index_code, doc_id):
+ md5 = hashlib.md5()
+ md5.update(index_code);
+ md5.update(doc_id);
+ domain_num = zlib.crc32(md5.digest()) % 100
+ domain_id = str(domain_num).rjust(3,'0')
+ item_id = index_code+'|'+doc_id
+ return domain_id, item_id
+
+VALUE_MAX_LENGTH = 1024
+def storage_add(index_code, doc_id, content):
+ domain_id, item_id = get_ids(index_code, doc_id)
+ try:
+ sdb = get_connection()
+ domain = sdb.get_domain(domain_id)
+ item = domain.new_item(item_id)
+ item['timestamp'] = time.time()
+ limit = VALUE_MAX_LENGTH-2
+ for key, txt in content.iteritems():
+ n = 0
+ i = 1
+ while len(txt) > n:
+ part = '['+txt[n:n+limit]+']'
+ item['_'+str(i)+'_'+key] = part
+ n += limit
+ i += 1
+ item.save()
+ return ""
+ except:
+ return sys.exc_info()
+
+def storage_get(index_code, doc_id):
+ domain_id, item_id = get_ids(index_code, doc_id)
+ try:
+ sdb = get_connection()
+ domain = sdb.get_domain(domain_id)
+ item = domain.get_item('|%s|DOC|%s' % (settings.STORAGE_ENV, item_id))
+ return item_to_doc(item) if item is not None else None
+ except:
+ traceback.print_exc()
+ return {}, sys.exc_info()
+
+def storage_del(index_code, doc_id):
+ domain_id, item_id = get_ids(index_code, doc_id)
+ try:
+ sdb = get_connection()
+ domain = sdb.get_domain(domain_id)
+ domain.delete_attributes('DOC|'+item_id)
+ return ""
+ except:
+ return sys.exc_info()
+
+
+def item_to_doc(item):
+ doc = {}
+ vars = {}
+ cats = {}
+ item = dict(item)
+
+ fields = item['item_fields'].split(',')
+
+ for k, v in item.iteritems():
+ if k.startswith('user_boost_'):
+ vars[k[11:]] = v
+ if k.startswith('user_category_'):
+ cats[k[14:]] = v
+
+ for f in fields:
+ v = ''
+ p = 1
+ while True:
+ part = item.get('_%s_%d' % (f, p))
+ if part:
+ v += part
+ p += 1
+ else:
+ break
+ if f == 'timestamp':
+ doc[f] = '%s (%s)' % (v, datetime.datetime.fromtimestamp(int(v)))
+ else:
+ doc[f] = v
+
+ return { 'fields': doc, 'variables': vars, 'categories': cats }
+
+
+
+
diff --git a/api/testapi.py b/api/testapi.py
new file mode 100644
index 0000000..da815ce
--- /dev/null
+++ b/api/testapi.py
@@ -0,0 +1,44 @@
+from lib.indextank.client import ApiClient
+import sys
+import time
+
+if len(sys.argv) != 2:
+ print 'Usage: testapi.py '
+
+api = ApiClient(sys.argv[1])
+index = api.get_index('testapi.py')
+
+if index.exists():
+ print 'deleting previous index'
+ index.delete_index()
+
+print 'creating index'
+index.create_index()
+
+print 'waiting to start...'
+while not index.has_started():
+ time.sleep(1)
+
+print 'adding docs'
+index.add_document(1, {'text': 'a b c1'}, variables={0:1, 1:2, 2:3})
+index.add_document(2, {'text': 'a b c2'}, variables={0:2, 1:2, 2:2})
+index.add_document(3, {'text': 'a b c3'}, variables={0:3, 1:2, 2:1})
+
+print 'adding functions'
+index.add_function(1, 'd[0]')
+index.add_function(2, 'd[2]')
+
+print 'checking functions'
+assert index.search('a', scoring_function=1)['results'][0]['docid'] == '3'
+assert index.search('a', scoring_function=2)['results'][0]['docid'] == '1'
+
+print 'checking fetch'
+assert index.search('a', scoring_function=2, fetch_fields=['text'])['results'][0]['text'] == 'a b c1'
+
+print 'checking delete'
+index.delete_document(1)
+
+assert index.search('a', scoring_function=2)['results'][0]['docid'] == '2'
+
+print 'success'
+
diff --git a/api/tests.py b/api/tests.py
new file mode 100644
index 0000000..e788a90
--- /dev/null
+++ b/api/tests.py
@@ -0,0 +1,8 @@
+import unittest
+import doctest
+from api import restapi
+
+def suite():
+ suite = unittest.TestSuite()
+ suite.addTest(doctest.DocTestSuite(restapi))
+ return suite
diff --git a/api/thrift/TSCons.py b/api/thrift/TSCons.py
new file mode 100644
index 0000000..2404625
--- /dev/null
+++ b/api/thrift/TSCons.py
@@ -0,0 +1,33 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+from os import path
+from SCons.Builder import Builder
+
+def scons_env(env, add=''):
+ opath = path.dirname(path.abspath('$TARGET'))
+ lstr = 'thrift --gen cpp -o ' + opath + ' ' + add + ' $SOURCE'
+ cppbuild = Builder(action = lstr)
+ env.Append(BUILDERS = {'ThriftCpp' : cppbuild})
+
+def gen_cpp(env, dir, file):
+ scons_env(env)
+ suffixes = ['_types.h', '_types.cpp']
+ targets = map(lambda s: 'gen-cpp/' + file + s, suffixes)
+ return env.ThriftCpp(targets, dir+file+'.thrift')
diff --git a/api/thrift/TSerialization.py b/api/thrift/TSerialization.py
new file mode 100644
index 0000000..b19f98a
--- /dev/null
+++ b/api/thrift/TSerialization.py
@@ -0,0 +1,34 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+from protocol import TBinaryProtocol
+from transport import TTransport
+
+def serialize(thrift_object, protocol_factory = TBinaryProtocol.TBinaryProtocolFactory()):
+ transport = TTransport.TMemoryBuffer()
+ protocol = protocol_factory.getProtocol(transport)
+ thrift_object.write(protocol)
+ return transport.getvalue()
+
+def deserialize(base, buf, protocol_factory = TBinaryProtocol.TBinaryProtocolFactory()):
+ transport = TTransport.TMemoryBuffer(buf)
+ protocol = protocol_factory.getProtocol(transport)
+ base.read(protocol)
+ return base
+
diff --git a/api/thrift/Thrift.py b/api/thrift/Thrift.py
new file mode 100644
index 0000000..91728a7
--- /dev/null
+++ b/api/thrift/Thrift.py
@@ -0,0 +1,133 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+import sys
+
+class TType:
+ STOP = 0
+ VOID = 1
+ BOOL = 2
+ BYTE = 3
+ I08 = 3
+ DOUBLE = 4
+ I16 = 6
+ I32 = 8
+ I64 = 10
+ STRING = 11
+ UTF7 = 11
+ STRUCT = 12
+ MAP = 13
+ SET = 14
+ LIST = 15
+ UTF8 = 16
+ UTF16 = 17
+
+class TMessageType:
+ CALL = 1
+ REPLY = 2
+ EXCEPTION = 3
+ ONEWAY = 4
+
+class TProcessor:
+
+ """Base class for procsessor, which works on two streams."""
+
+ def process(iprot, oprot):
+ pass
+
+class TException(Exception):
+
+ """Base class for all thrift exceptions."""
+
+ # BaseException.message is deprecated in Python v[2.6,3.0)
+ if (2,6,0) <= sys.version_info < (3,0):
+ def _get_message(self):
+ return self._message
+ def _set_message(self, message):
+ self._message = message
+ message = property(_get_message, _set_message)
+
+ def __init__(self, message=None):
+ Exception.__init__(self, message)
+ self.message = message
+
+class TApplicationException(TException):
+
+ """Application level thrift exceptions."""
+
+ UNKNOWN = 0
+ UNKNOWN_METHOD = 1
+ INVALID_MESSAGE_TYPE = 2
+ WRONG_METHOD_NAME = 3
+ BAD_SEQUENCE_ID = 4
+ MISSING_RESULT = 5
+
+ def __init__(self, type=UNKNOWN, message=None):
+ TException.__init__(self, message)
+ self.type = type
+
+ def __str__(self):
+ if self.message:
+ return self.message
+ elif self.type == self.UNKNOWN_METHOD:
+ return 'Unknown method'
+ elif self.type == self.INVALID_MESSAGE_TYPE:
+ return 'Invalid message type'
+ elif self.type == self.WRONG_METHOD_NAME:
+ return 'Wrong method name'
+ elif self.type == self.BAD_SEQUENCE_ID:
+ return 'Bad sequence ID'
+ elif self.type == self.MISSING_RESULT:
+ return 'Missing result'
+ else:
+ return 'Default (unknown) TApplicationException'
+
+ def read(self, iprot):
+ iprot.readStructBegin()
+ while True:
+ (fname, ftype, fid) = iprot.readFieldBegin()
+ if ftype == TType.STOP:
+ break
+ if fid == 1:
+ if ftype == TType.STRING:
+ self.message = iprot.readString();
+ else:
+ iprot.skip(ftype)
+ elif fid == 2:
+ if ftype == TType.I32:
+ self.type = iprot.readI32();
+ else:
+ iprot.skip(ftype)
+ else:
+ iprot.skip(ftype)
+ iprot.readFieldEnd()
+ iprot.readStructEnd()
+
+ def write(self, oprot):
+ oprot.writeStructBegin('TApplicationException')
+ if self.message != None:
+ oprot.writeFieldBegin('message', TType.STRING, 1)
+ oprot.writeString(self.message)
+ oprot.writeFieldEnd()
+ if self.type != None:
+ oprot.writeFieldBegin('type', TType.I32, 2)
+ oprot.writeI32(self.type)
+ oprot.writeFieldEnd()
+ oprot.writeFieldStop()
+ oprot.writeStructEnd()
diff --git a/api/thrift/__init__.py b/api/thrift/__init__.py
new file mode 100644
index 0000000..48d659c
--- /dev/null
+++ b/api/thrift/__init__.py
@@ -0,0 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+__all__ = ['Thrift', 'TSCons']
diff --git a/api/thrift/protocol/TBinaryProtocol.py b/api/thrift/protocol/TBinaryProtocol.py
new file mode 100644
index 0000000..50c6aa8
--- /dev/null
+++ b/api/thrift/protocol/TBinaryProtocol.py
@@ -0,0 +1,259 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+from TProtocol import *
+from struct import pack, unpack
+
+class TBinaryProtocol(TProtocolBase):
+
+ """Binary implementation of the Thrift protocol driver."""
+
+ # NastyHaxx. Python 2.4+ on 32-bit machines forces hex constants to be
+ # positive, converting this into a long. If we hardcode the int value
+ # instead it'll stay in 32 bit-land.
+
+ # VERSION_MASK = 0xffff0000
+ VERSION_MASK = -65536
+
+ # VERSION_1 = 0x80010000
+ VERSION_1 = -2147418112
+
+ TYPE_MASK = 0x000000ff
+
+ def __init__(self, trans, strictRead=False, strictWrite=True):
+ TProtocolBase.__init__(self, trans)
+ self.strictRead = strictRead
+ self.strictWrite = strictWrite
+
+ def writeMessageBegin(self, name, type, seqid):
+ if self.strictWrite:
+ self.writeI32(TBinaryProtocol.VERSION_1 | type)
+ self.writeString(name)
+ self.writeI32(seqid)
+ else:
+ self.writeString(name)
+ self.writeByte(type)
+ self.writeI32(seqid)
+
+ def writeMessageEnd(self):
+ pass
+
+ def writeStructBegin(self, name):
+ pass
+
+ def writeStructEnd(self):
+ pass
+
+ def writeFieldBegin(self, name, type, id):
+ self.writeByte(type)
+ self.writeI16(id)
+
+ def writeFieldEnd(self):
+ pass
+
+ def writeFieldStop(self):
+ self.writeByte(TType.STOP);
+
+ def writeMapBegin(self, ktype, vtype, size):
+ self.writeByte(ktype)
+ self.writeByte(vtype)
+ self.writeI32(size)
+
+ def writeMapEnd(self):
+ pass
+
+ def writeListBegin(self, etype, size):
+ self.writeByte(etype)
+ self.writeI32(size)
+
+ def writeListEnd(self):
+ pass
+
+ def writeSetBegin(self, etype, size):
+ self.writeByte(etype)
+ self.writeI32(size)
+
+ def writeSetEnd(self):
+ pass
+
+ def writeBool(self, bool):
+ if bool:
+ self.writeByte(1)
+ else:
+ self.writeByte(0)
+
+ def writeByte(self, byte):
+ buff = pack("!b", byte)
+ self.trans.write(buff)
+
+ def writeI16(self, i16):
+ buff = pack("!h", i16)
+ self.trans.write(buff)
+
+ def writeI32(self, i32):
+ buff = pack("!i", i32)
+ self.trans.write(buff)
+
+ def writeI64(self, i64):
+ buff = pack("!q", i64)
+ self.trans.write(buff)
+
+ def writeDouble(self, dub):
+ buff = pack("!d", dub)
+ self.trans.write(buff)
+
+ def writeString(self, str):
+ self.writeI32(len(str))
+ self.trans.write(str)
+
+ def readMessageBegin(self):
+ sz = self.readI32()
+ if sz < 0:
+ version = sz & TBinaryProtocol.VERSION_MASK
+ if version != TBinaryProtocol.VERSION_1:
+ raise TProtocolException(type=TProtocolException.BAD_VERSION, message='Bad version in readMessageBegin: %d' % (sz))
+ type = sz & TBinaryProtocol.TYPE_MASK
+ name = self.readString()
+ seqid = self.readI32()
+ else:
+ if self.strictRead:
+ raise TProtocolException(type=TProtocolException.BAD_VERSION, message='No protocol version header')
+ name = self.trans.readAll(sz)
+ type = self.readByte()
+ seqid = self.readI32()
+ return (name, type, seqid)
+
+ def readMessageEnd(self):
+ pass
+
+ def readStructBegin(self):
+ pass
+
+ def readStructEnd(self):
+ pass
+
+ def readFieldBegin(self):
+ type = self.readByte()
+ if type == TType.STOP:
+ return (None, type, 0)
+ id = self.readI16()
+ return (None, type, id)
+
+ def readFieldEnd(self):
+ pass
+
+ def readMapBegin(self):
+ ktype = self.readByte()
+ vtype = self.readByte()
+ size = self.readI32()
+ return (ktype, vtype, size)
+
+ def readMapEnd(self):
+ pass
+
+ def readListBegin(self):
+ etype = self.readByte()
+ size = self.readI32()
+ return (etype, size)
+
+ def readListEnd(self):
+ pass
+
+ def readSetBegin(self):
+ etype = self.readByte()
+ size = self.readI32()
+ return (etype, size)
+
+ def readSetEnd(self):
+ pass
+
+ def readBool(self):
+ byte = self.readByte()
+ if byte == 0:
+ return False
+ return True
+
+ def readByte(self):
+ buff = self.trans.readAll(1)
+ val, = unpack('!b', buff)
+ return val
+
+ def readI16(self):
+ buff = self.trans.readAll(2)
+ val, = unpack('!h', buff)
+ return val
+
+ def readI32(self):
+ buff = self.trans.readAll(4)
+ val, = unpack('!i', buff)
+ return val
+
+ def readI64(self):
+ buff = self.trans.readAll(8)
+ val, = unpack('!q', buff)
+ return val
+
+ def readDouble(self):
+ buff = self.trans.readAll(8)
+ val, = unpack('!d', buff)
+ return val
+
+ def readString(self):
+ len = self.readI32()
+ str = self.trans.readAll(len)
+ return str
+
+
+class TBinaryProtocolFactory:
+ def __init__(self, strictRead=False, strictWrite=True):
+ self.strictRead = strictRead
+ self.strictWrite = strictWrite
+
+ def getProtocol(self, trans):
+ prot = TBinaryProtocol(trans, self.strictRead, self.strictWrite)
+ return prot
+
+
+class TBinaryProtocolAccelerated(TBinaryProtocol):
+
+ """C-Accelerated version of TBinaryProtocol.
+
+ This class does not override any of TBinaryProtocol's methods,
+ but the generated code recognizes it directly and will call into
+ our C module to do the encoding, bypassing this object entirely.
+ We inherit from TBinaryProtocol so that the normal TBinaryProtocol
+ encoding can happen if the fastbinary module doesn't work for some
+ reason. (TODO(dreiss): Make this happen sanely in more cases.)
+
+ In order to take advantage of the C module, just use
+ TBinaryProtocolAccelerated instead of TBinaryProtocol.
+
+ NOTE: This code was contributed by an external developer.
+ The internal Thrift team has reviewed and tested it,
+ but we cannot guarantee that it is production-ready.
+ Please feel free to report bugs and/or success stories
+ to the public mailing list.
+ """
+
+ pass
+
+
+class TBinaryProtocolAcceleratedFactory:
+ def getProtocol(self, trans):
+ return TBinaryProtocolAccelerated(trans)
diff --git a/api/thrift/protocol/TCompactProtocol.py b/api/thrift/protocol/TCompactProtocol.py
new file mode 100644
index 0000000..fbc156a
--- /dev/null
+++ b/api/thrift/protocol/TCompactProtocol.py
@@ -0,0 +1,368 @@
+from TProtocol import *
+from struct import pack, unpack
+
+__all__ = ['TCompactProtocol', 'TCompactProtocolFactory']
+
+CLEAR = 0
+FIELD_WRITE = 1
+VALUE_WRITE = 2
+CONTAINER_WRITE = 3
+BOOL_WRITE = 4
+FIELD_READ = 5
+CONTAINER_READ = 6
+VALUE_READ = 7
+BOOL_READ = 8
+
+def make_helper(v_from, container):
+ def helper(func):
+ def nested(self, *args, **kwargs):
+ assert self.state in (v_from, container), (self.state, v_from, container)
+ return func(self, *args, **kwargs)
+ return nested
+ return helper
+writer = make_helper(VALUE_WRITE, CONTAINER_WRITE)
+reader = make_helper(VALUE_READ, CONTAINER_READ)
+
+def makeZigZag(n, bits):
+ return (n << 1) ^ (n >> (bits - 1))
+
+def fromZigZag(n):
+ return (n >> 1) ^ -(n & 1)
+
+def writeVarint(trans, n):
+ out = []
+ while True:
+ if n & ~0x7f == 0:
+ out.append(n)
+ break
+ else:
+ out.append((n & 0xff) | 0x80)
+ n = n >> 7
+ trans.write(''.join(map(chr, out)))
+
+def readVarint(trans):
+ result = 0
+ shift = 0
+ while True:
+ x = trans.readAll(1)
+ byte = ord(x)
+ result |= (byte & 0x7f) << shift
+ if byte >> 7 == 0:
+ return result
+ shift += 7
+
+class CompactType:
+ TRUE = 1
+ FALSE = 2
+ BYTE = 0x03
+ I16 = 0x04
+ I32 = 0x05
+ I64 = 0x06
+ DOUBLE = 0x07
+ BINARY = 0x08
+ LIST = 0x09
+ SET = 0x0A
+ MAP = 0x0B
+ STRUCT = 0x0C
+
+CTYPES = {TType.BOOL: CompactType.TRUE, # used for collection
+ TType.BYTE: CompactType.BYTE,
+ TType.I16: CompactType.I16,
+ TType.I32: CompactType.I32,
+ TType.I64: CompactType.I64,
+ TType.DOUBLE: CompactType.DOUBLE,
+ TType.STRING: CompactType.BINARY,
+ TType.STRUCT: CompactType.STRUCT,
+ TType.LIST: CompactType.LIST,
+ TType.SET: CompactType.SET,
+ TType.MAP: CompactType.MAP,
+ }
+
+TTYPES = {}
+for k, v in CTYPES.items():
+ TTYPES[v] = k
+TTYPES[CompactType.FALSE] = TType.BOOL
+del k
+del v
+
+class TCompactProtocol(TProtocolBase):
+ "Compact implementation of the Thrift protocol driver."
+
+ PROTOCOL_ID = 0x82
+ VERSION = 1
+ VERSION_MASK = 0x1f
+ TYPE_MASK = 0xe0
+ TYPE_SHIFT_AMOUNT = 5
+
+ def __init__(self, trans):
+ TProtocolBase.__init__(self, trans)
+ self.state = CLEAR
+ self.__last_fid = 0
+ self.__bool_fid = None
+ self.__bool_value = None
+ self.__structs = []
+ self.__containers = []
+
+ def __writeVarint(self, n):
+ writeVarint(self.trans, n)
+
+ def writeMessageBegin(self, name, type, seqid):
+ assert self.state == CLEAR
+ self.__writeUByte(self.PROTOCOL_ID)
+ self.__writeUByte(self.VERSION | (type << self.TYPE_SHIFT_AMOUNT))
+ self.__writeVarint(seqid)
+ self.__writeString(name)
+ self.state = VALUE_WRITE
+
+ def writeMessageEnd(self):
+ assert self.state == VALUE_WRITE
+ self.state = CLEAR
+
+ def writeStructBegin(self, name):
+ assert self.state in (CLEAR, CONTAINER_WRITE, VALUE_WRITE), self.state
+ self.__structs.append((self.state, self.__last_fid))
+ self.state = FIELD_WRITE
+ self.__last_fid = 0
+
+ def writeStructEnd(self):
+ assert self.state == FIELD_WRITE
+ self.state, self.__last_fid = self.__structs.pop()
+
+ def writeFieldStop(self):
+ self.__writeByte(0)
+
+ def __writeFieldHeader(self, type, fid):
+ delta = fid - self.__last_fid
+ if 0 < delta <= 15:
+ self.__writeUByte(delta << 4 | type)
+ else:
+ self.__writeByte(type)
+ self.__writeI16(fid)
+ self.__last_fid = fid
+
+ def writeFieldBegin(self, name, type, fid):
+ assert self.state == FIELD_WRITE, self.state
+ if type == TType.BOOL:
+ self.state = BOOL_WRITE
+ self.__bool_fid = fid
+ else:
+ self.state = VALUE_WRITE
+ self.__writeFieldHeader(CTYPES[type], fid)
+
+ def writeFieldEnd(self):
+ assert self.state in (VALUE_WRITE, BOOL_WRITE), self.state
+ self.state = FIELD_WRITE
+
+ def __writeUByte(self, byte):
+ self.trans.write(pack('!B', byte))
+
+ def __writeByte(self, byte):
+ self.trans.write(pack('!b', byte))
+
+ def __writeI16(self, i16):
+ self.__writeVarint(makeZigZag(i16, 16))
+
+ def __writeSize(self, i32):
+ self.__writeVarint(i32)
+
+ def writeCollectionBegin(self, etype, size):
+ assert self.state in (VALUE_WRITE, CONTAINER_WRITE), self.state
+ if size <= 14:
+ self.__writeUByte(size << 4 | CTYPES[etype])
+ else:
+ self.__writeUByte(0xf0 | CTYPES[etype])
+ self.__writeSize(size)
+ self.__containers.append(self.state)
+ self.state = CONTAINER_WRITE
+ writeSetBegin = writeCollectionBegin
+ writeListBegin = writeCollectionBegin
+
+ def writeMapBegin(self, ktype, vtype, size):
+ assert self.state in (VALUE_WRITE, CONTAINER_WRITE), self.state
+ if size == 0:
+ self.__writeByte(0)
+ else:
+ self.__writeSize(size)
+ self.__writeUByte(CTYPES[ktype] << 4 | CTYPES[vtype])
+ self.__containers.append(self.state)
+ self.state = CONTAINER_WRITE
+
+ def writeCollectionEnd(self):
+ assert self.state == CONTAINER_WRITE, self.state
+ self.state = self.__containers.pop()
+ writeMapEnd = writeCollectionEnd
+ writeSetEnd = writeCollectionEnd
+ writeListEnd = writeCollectionEnd
+
+ def writeBool(self, bool):
+ if self.state == BOOL_WRITE:
+ self.__writeFieldHeader(types[bool], self.__bool_fid)
+ elif self.state == CONTAINER_WRITE:
+ self.__writeByte(int(bool))
+ else:
+ raise AssertetionError, "Invalid state in compact protocol"
+
+ writeByte = writer(__writeByte)
+ writeI16 = writer(__writeI16)
+
+ @writer
+ def writeI32(self, i32):
+ self.__writeVarint(makeZigZag(i32, 32))
+
+ @writer
+ def writeI64(self, i64):
+ self.__writeVarint(makeZigZag(i64, 64))
+
+ @writer
+ def writeDouble(self, dub):
+ self.trans.write(pack('!d', dub))
+
+ def __writeString(self, s):
+ self.__writeSize(len(s))
+ self.trans.write(s)
+ writeString = writer(__writeString)
+
+ def readFieldBegin(self):
+ assert self.state == FIELD_READ, self.state
+ type = self.__readUByte()
+ if type & 0x0f == TType.STOP:
+ return (None, 0, 0)
+ delta = type >> 4
+ if delta == 0:
+ fid = self.__readI16()
+ else:
+ fid = self.__last_fid + delta
+ self.__last_fid = fid
+ type = type & 0x0f
+ if type == CompactType.TRUE:
+ self.state = BOOL_READ
+ self.__bool_value = True
+ elif type == CompactType.FALSE:
+ self.state = BOOL_READ
+ self.__bool_value = False
+ else:
+ self.state = VALUE_READ
+ return (None, self.__getTType(type), fid)
+
+ def readFieldEnd(self):
+ assert self.state in (VALUE_READ, BOOL_READ), self.state
+ self.state = FIELD_READ
+
+ def __readUByte(self):
+ result, = unpack('!B', self.trans.readAll(1))
+ return result
+
+ def __readByte(self):
+ result, = unpack('!b', self.trans.readAll(1))
+ return result
+
+ def __readVarint(self):
+ return readVarint(self.trans)
+
+ def __readZigZag(self):
+ return fromZigZag(self.__readVarint())
+
+ def __readSize(self):
+ result = self.__readVarint()
+ if result < 0:
+ raise TException("Length < 0")
+ return result
+
+ def readMessageBegin(self):
+ assert self.state == CLEAR
+ proto_id = self.__readUByte()
+ if proto_id != self.PROTOCOL_ID:
+ raise TProtocolException(TProtocolException.BAD_VERSION,
+ 'Bad protocol id in the message: %d' % proto_id)
+ ver_type = self.__readUByte()
+ type = (ver_type & self.TYPE_MASK) >> self.TYPE_SHIFT_AMOUNT
+ version = ver_type & self.VERSION_MASK
+ if version != self.VERSION:
+ raise TProtocolException(TProtocolException.BAD_VERSION,
+ 'Bad version: %d (expect %d)' % (version, self.VERSION))
+ seqid = self.__readVarint()
+ name = self.__readString()
+ return (name, type, seqid)
+
+ def readMessageEnd(self):
+ assert self.state == VALUE_READ
+ assert len(self.__structs) == 0
+ self.state = CLEAR
+
+ def readStructBegin(self):
+ assert self.state in (CLEAR, CONTAINER_READ, VALUE_READ), self.state
+ self.__structs.append((self.state, self.__last_fid))
+ self.state = FIELD_READ
+ self.__last_fid = 0
+
+ def readStructEnd(self):
+ assert self.state == FIELD_READ
+ self.state, self.__last_fid = self.__structs.pop()
+
+ def readCollectionBegin(self):
+ assert self.state in (VALUE_READ, CONTAINER_READ), self.state
+ size_type = self.__readUByte()
+ size = size_type >> 4
+ type = self.__getTType(size_type)
+ if size == 15:
+ size = self.__readSize()
+ self.__containers.append(self.state)
+ self.state = CONTAINER_READ
+ return type, size
+ readSetBegin = readCollectionBegin
+ readListBegin = readCollectionBegin
+
+ def readMapBegin(self):
+ assert self.state in (VALUE_READ, CONTAINER_READ), self.state
+ size = self.__readSize()
+ types = 0
+ if size > 0:
+ types = self.__readUByte()
+ vtype = self.__getTType(types)
+ ktype = self.__getTType(types >> 4)
+ self.__containers.append(self.state)
+ self.state = CONTAINER_READ
+ return (ktype, vtype, size)
+
+ def readCollectionEnd(self):
+ assert self.state == CONTAINER_READ, self.state
+ self.state = self.__containers.pop()
+ readSetEnd = readCollectionEnd
+ readListEnd = readCollectionEnd
+ readMapEnd = readCollectionEnd
+
+ def readBool(self):
+ if self.state == BOOL_READ:
+ return self.__bool_value
+ elif self.state == CONTAINER_READ:
+ return bool(self.__readByte())
+ else:
+ raise AssertionError, "Invalid state in compact protocol: %d" % self.state
+
+ readByte = reader(__readByte)
+ __readI16 = __readZigZag
+ readI16 = reader(__readZigZag)
+ readI32 = reader(__readZigZag)
+ readI64 = reader(__readZigZag)
+
+ @reader
+ def readDouble(self):
+ buff = self.trans.readAll(8)
+ val, = unpack('!d', buff)
+ return val
+
+ def __readString(self):
+ len = self.__readSize()
+ return self.trans.readAll(len)
+ readString = reader(__readString)
+
+ def __getTType(self, byte):
+ return TTYPES[byte & 0x0f]
+
+
+class TCompactProtocolFactory:
+ def __init__(self):
+ pass
+
+ def getProtocol(self, trans):
+ return TCompactProtocol(trans)
diff --git a/api/thrift/protocol/TProtocol.py b/api/thrift/protocol/TProtocol.py
new file mode 100644
index 0000000..be3cb14
--- /dev/null
+++ b/api/thrift/protocol/TProtocol.py
@@ -0,0 +1,205 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+from thrift.Thrift import *
+
+class TProtocolException(TException):
+
+ """Custom Protocol Exception class"""
+
+ UNKNOWN = 0
+ INVALID_DATA = 1
+ NEGATIVE_SIZE = 2
+ SIZE_LIMIT = 3
+ BAD_VERSION = 4
+
+ def __init__(self, type=UNKNOWN, message=None):
+ TException.__init__(self, message)
+ self.type = type
+
+class TProtocolBase:
+
+ """Base class for Thrift protocol driver."""
+
+ def __init__(self, trans):
+ self.trans = trans
+
+ def writeMessageBegin(self, name, type, seqid):
+ pass
+
+ def writeMessageEnd(self):
+ pass
+
+ def writeStructBegin(self, name):
+ pass
+
+ def writeStructEnd(self):
+ pass
+
+ def writeFieldBegin(self, name, type, id):
+ pass
+
+ def writeFieldEnd(self):
+ pass
+
+ def writeFieldStop(self):
+ pass
+
+ def writeMapBegin(self, ktype, vtype, size):
+ pass
+
+ def writeMapEnd(self):
+ pass
+
+ def writeListBegin(self, etype, size):
+ pass
+
+ def writeListEnd(self):
+ pass
+
+ def writeSetBegin(self, etype, size):
+ pass
+
+ def writeSetEnd(self):
+ pass
+
+ def writeBool(self, bool):
+ pass
+
+ def writeByte(self, byte):
+ pass
+
+ def writeI16(self, i16):
+ pass
+
+ def writeI32(self, i32):
+ pass
+
+ def writeI64(self, i64):
+ pass
+
+ def writeDouble(self, dub):
+ pass
+
+ def writeString(self, str):
+ pass
+
+ def readMessageBegin(self):
+ pass
+
+ def readMessageEnd(self):
+ pass
+
+ def readStructBegin(self):
+ pass
+
+ def readStructEnd(self):
+ pass
+
+ def readFieldBegin(self):
+ pass
+
+ def readFieldEnd(self):
+ pass
+
+ def readMapBegin(self):
+ pass
+
+ def readMapEnd(self):
+ pass
+
+ def readListBegin(self):
+ pass
+
+ def readListEnd(self):
+ pass
+
+ def readSetBegin(self):
+ pass
+
+ def readSetEnd(self):
+ pass
+
+ def readBool(self):
+ pass
+
+ def readByte(self):
+ pass
+
+ def readI16(self):
+ pass
+
+ def readI32(self):
+ pass
+
+ def readI64(self):
+ pass
+
+ def readDouble(self):
+ pass
+
+ def readString(self):
+ pass
+
+ def skip(self, type):
+ if type == TType.STOP:
+ return
+ elif type == TType.BOOL:
+ self.readBool()
+ elif type == TType.BYTE:
+ self.readByte()
+ elif type == TType.I16:
+ self.readI16()
+ elif type == TType.I32:
+ self.readI32()
+ elif type == TType.I64:
+ self.readI64()
+ elif type == TType.DOUBLE:
+ self.readDouble()
+ elif type == TType.STRING:
+ self.readString()
+ elif type == TType.STRUCT:
+ name = self.readStructBegin()
+ while True:
+ (name, type, id) = self.readFieldBegin()
+ if type == TType.STOP:
+ break
+ self.skip(type)
+ self.readFieldEnd()
+ self.readStructEnd()
+ elif type == TType.MAP:
+ (ktype, vtype, size) = self.readMapBegin()
+ for i in range(size):
+ self.skip(ktype)
+ self.skip(vtype)
+ self.readMapEnd()
+ elif type == TType.SET:
+ (etype, size) = self.readSetBegin()
+ for i in range(size):
+ self.skip(etype)
+ self.readSetEnd()
+ elif type == TType.LIST:
+ (etype, size) = self.readListBegin()
+ for i in range(size):
+ self.skip(etype)
+ self.readListEnd()
+
+class TProtocolFactory:
+ def getProtocol(self, trans):
+ pass
diff --git a/api/thrift/protocol/__init__.py b/api/thrift/protocol/__init__.py
new file mode 100644
index 0000000..01bfe18
--- /dev/null
+++ b/api/thrift/protocol/__init__.py
@@ -0,0 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+__all__ = ['TProtocol', 'TBinaryProtocol', 'fastbinary']
diff --git a/api/thrift/protocol/fastbinary.c b/api/thrift/protocol/fastbinary.c
new file mode 100644
index 0000000..67b215a
--- /dev/null
+++ b/api/thrift/protocol/fastbinary.c
@@ -0,0 +1,1203 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include
+#include "cStringIO.h"
+#include
+#include
+#include
+
+/* Fix endianness issues on Solaris */
+#if defined (__SVR4) && defined (__sun)
+ #if defined(__i386) && !defined(__i386__)
+ #define __i386__
+ #endif
+
+ #ifndef BIG_ENDIAN
+ #define BIG_ENDIAN (4321)
+ #endif
+ #ifndef LITTLE_ENDIAN
+ #define LITTLE_ENDIAN (1234)
+ #endif
+
+ /* I386 is LE, even on Solaris */
+ #if !defined(BYTE_ORDER) && defined(__i386__)
+ #define BYTE_ORDER LITTLE_ENDIAN
+ #endif
+#endif
+
+// TODO(dreiss): defval appears to be unused. Look into removing it.
+// TODO(dreiss): Make parse_spec_args recursive, and cache the output
+// permanently in the object. (Malloc and orphan.)
+// TODO(dreiss): Why do we need cStringIO for reading, why not just char*?
+// Can cStringIO let us work with a BufferedTransport?
+// TODO(dreiss): Don't ignore the rv from cwrite (maybe).
+
+/* ====== BEGIN UTILITIES ====== */
+
+#define INIT_OUTBUF_SIZE 128
+
+// Stolen out of TProtocol.h.
+// It would be a huge pain to have both get this from one place.
+typedef enum TType {
+ T_STOP = 0,
+ T_VOID = 1,
+ T_BOOL = 2,
+ T_BYTE = 3,
+ T_I08 = 3,
+ T_I16 = 6,
+ T_I32 = 8,
+ T_U64 = 9,
+ T_I64 = 10,
+ T_DOUBLE = 4,
+ T_STRING = 11,
+ T_UTF7 = 11,
+ T_STRUCT = 12,
+ T_MAP = 13,
+ T_SET = 14,
+ T_LIST = 15,
+ T_UTF8 = 16,
+ T_UTF16 = 17
+} TType;
+
+#ifndef __BYTE_ORDER
+# if defined(BYTE_ORDER) && defined(LITTLE_ENDIAN) && defined(BIG_ENDIAN)
+# define __BYTE_ORDER BYTE_ORDER
+# define __LITTLE_ENDIAN LITTLE_ENDIAN
+# define __BIG_ENDIAN BIG_ENDIAN
+# else
+# error "Cannot determine endianness"
+# endif
+#endif
+
+// Same comment as the enum. Sorry.
+#if __BYTE_ORDER == __BIG_ENDIAN
+# define ntohll(n) (n)
+# define htonll(n) (n)
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+# if defined(__GNUC__) && defined(__GLIBC__)
+# include
+# define ntohll(n) bswap_64(n)
+# define htonll(n) bswap_64(n)
+# else /* GNUC & GLIBC */
+# define ntohll(n) ( (((unsigned long long)ntohl(n)) << 32) + ntohl(n >> 32) )
+# define htonll(n) ( (((unsigned long long)htonl(n)) << 32) + htonl(n >> 32) )
+# endif /* GNUC & GLIBC */
+#else /* __BYTE_ORDER */
+# error "Can't define htonll or ntohll!"
+#endif
+
+// Doing a benchmark shows that interning actually makes a difference, amazingly.
+#define INTERN_STRING(value) _intern_ ## value
+
+#define INT_CONV_ERROR_OCCURRED(v) ( ((v) == -1) && PyErr_Occurred() )
+#define CHECK_RANGE(v, min, max) ( ((v) <= (max)) && ((v) >= (min)) )
+
+// Py_ssize_t was not defined before Python 2.5
+#if (PY_VERSION_HEX < 0x02050000)
+typedef int Py_ssize_t;
+#endif
+
+/**
+ * A cache of the spec_args for a set or list,
+ * so we don't have to keep calling PyTuple_GET_ITEM.
+ */
+typedef struct {
+ TType element_type;
+ PyObject* typeargs;
+} SetListTypeArgs;
+
+/**
+ * A cache of the spec_args for a map,
+ * so we don't have to keep calling PyTuple_GET_ITEM.
+ */
+typedef struct {
+ TType ktag;
+ TType vtag;
+ PyObject* ktypeargs;
+ PyObject* vtypeargs;
+} MapTypeArgs;
+
+/**
+ * A cache of the spec_args for a struct,
+ * so we don't have to keep calling PyTuple_GET_ITEM.
+ */
+typedef struct {
+ PyObject* klass;
+ PyObject* spec;
+} StructTypeArgs;
+
+/**
+ * A cache of the item spec from a struct specification,
+ * so we don't have to keep calling PyTuple_GET_ITEM.
+ */
+typedef struct {
+ int tag;
+ TType type;
+ PyObject* attrname;
+ PyObject* typeargs;
+ PyObject* defval;
+} StructItemSpec;
+
+/**
+ * A cache of the two key attributes of a CReadableTransport,
+ * so we don't have to keep calling PyObject_GetAttr.
+ */
+typedef struct {
+ PyObject* stringiobuf;
+ PyObject* refill_callable;
+} DecodeBuffer;
+
+/** Pointer to interned string to speed up attribute lookup. */
+static PyObject* INTERN_STRING(cstringio_buf);
+/** Pointer to interned string to speed up attribute lookup. */
+static PyObject* INTERN_STRING(cstringio_refill);
+
+static inline bool
+check_ssize_t_32(Py_ssize_t len) {
+ // error from getting the int
+ if (INT_CONV_ERROR_OCCURRED(len)) {
+ return false;
+ }
+ if (!CHECK_RANGE(len, 0, INT32_MAX)) {
+ PyErr_SetString(PyExc_OverflowError, "string size out of range");
+ return false;
+ }
+ return true;
+}
+
+static inline bool
+parse_pyint(PyObject* o, int32_t* ret, int32_t min, int32_t max) {
+ long val = PyInt_AsLong(o);
+
+ if (INT_CONV_ERROR_OCCURRED(val)) {
+ return false;
+ }
+ if (!CHECK_RANGE(val, min, max)) {
+ PyErr_SetString(PyExc_OverflowError, "int out of range");
+ return false;
+ }
+
+ *ret = (int32_t) val;
+ return true;
+}
+
+
+/* --- FUNCTIONS TO PARSE STRUCT SPECIFICATOINS --- */
+
+static bool
+parse_set_list_args(SetListTypeArgs* dest, PyObject* typeargs) {
+ if (PyTuple_Size(typeargs) != 2) {
+ PyErr_SetString(PyExc_TypeError, "expecting tuple of size 2 for list/set type args");
+ return false;
+ }
+
+ dest->element_type = PyInt_AsLong(PyTuple_GET_ITEM(typeargs, 0));
+ if (INT_CONV_ERROR_OCCURRED(dest->element_type)) {
+ return false;
+ }
+
+ dest->typeargs = PyTuple_GET_ITEM(typeargs, 1);
+
+ return true;
+}
+
+static bool
+parse_map_args(MapTypeArgs* dest, PyObject* typeargs) {
+ if (PyTuple_Size(typeargs) != 4) {
+ PyErr_SetString(PyExc_TypeError, "expecting 4 arguments for typeargs to map");
+ return false;
+ }
+
+ dest->ktag = PyInt_AsLong(PyTuple_GET_ITEM(typeargs, 0));
+ if (INT_CONV_ERROR_OCCURRED(dest->ktag)) {
+ return false;
+ }
+
+ dest->vtag = PyInt_AsLong(PyTuple_GET_ITEM(typeargs, 2));
+ if (INT_CONV_ERROR_OCCURRED(dest->vtag)) {
+ return false;
+ }
+
+ dest->ktypeargs = PyTuple_GET_ITEM(typeargs, 1);
+ dest->vtypeargs = PyTuple_GET_ITEM(typeargs, 3);
+
+ return true;
+}
+
+static bool
+parse_struct_args(StructTypeArgs* dest, PyObject* typeargs) {
+ if (PyTuple_Size(typeargs) != 2) {
+ PyErr_SetString(PyExc_TypeError, "expecting tuple of size 2 for struct args");
+ return false;
+ }
+
+ dest->klass = PyTuple_GET_ITEM(typeargs, 0);
+ dest->spec = PyTuple_GET_ITEM(typeargs, 1);
+
+ return true;
+}
+
+static int
+parse_struct_item_spec(StructItemSpec* dest, PyObject* spec_tuple) {
+
+ // i'd like to use ParseArgs here, but it seems to be a bottleneck.
+ if (PyTuple_Size(spec_tuple) != 5) {
+ PyErr_SetString(PyExc_TypeError, "expecting 5 arguments for spec tuple");
+ return false;
+ }
+
+ dest->tag = PyInt_AsLong(PyTuple_GET_ITEM(spec_tuple, 0));
+ if (INT_CONV_ERROR_OCCURRED(dest->tag)) {
+ return false;
+ }
+
+ dest->type = PyInt_AsLong(PyTuple_GET_ITEM(spec_tuple, 1));
+ if (INT_CONV_ERROR_OCCURRED(dest->type)) {
+ return false;
+ }
+
+ dest->attrname = PyTuple_GET_ITEM(spec_tuple, 2);
+ dest->typeargs = PyTuple_GET_ITEM(spec_tuple, 3);
+ dest->defval = PyTuple_GET_ITEM(spec_tuple, 4);
+ return true;
+}
+
+/* ====== END UTILITIES ====== */
+
+
+/* ====== BEGIN WRITING FUNCTIONS ====== */
+
+/* --- LOW-LEVEL WRITING FUNCTIONS --- */
+
+static void writeByte(PyObject* outbuf, int8_t val) {
+ int8_t net = val;
+ PycStringIO->cwrite(outbuf, (char*)&net, sizeof(int8_t));
+}
+
+static void writeI16(PyObject* outbuf, int16_t val) {
+ int16_t net = (int16_t)htons(val);
+ PycStringIO->cwrite(outbuf, (char*)&net, sizeof(int16_t));
+}
+
+static void writeI32(PyObject* outbuf, int32_t val) {
+ int32_t net = (int32_t)htonl(val);
+ PycStringIO->cwrite(outbuf, (char*)&net, sizeof(int32_t));
+}
+
+static void writeI64(PyObject* outbuf, int64_t val) {
+ int64_t net = (int64_t)htonll(val);
+ PycStringIO->cwrite(outbuf, (char*)&net, sizeof(int64_t));
+}
+
+static void writeDouble(PyObject* outbuf, double dub) {
+ // Unfortunately, bitwise_cast doesn't work in C. Bad C!
+ union {
+ double f;
+ int64_t t;
+ } transfer;
+ transfer.f = dub;
+ writeI64(outbuf, transfer.t);
+}
+
+
+/* --- MAIN RECURSIVE OUTPUT FUCNTION -- */
+
+static int
+output_val(PyObject* output, PyObject* value, TType type, PyObject* typeargs) {
+ /*
+ * Refcounting Strategy:
+ *
+ * We assume that elements of the thrift_spec tuple are not going to be
+ * mutated, so we don't ref count those at all. Other than that, we try to
+ * keep a reference to all the user-created objects while we work with them.
+ * output_val assumes that a reference is already held. The *caller* is
+ * responsible for handling references
+ */
+
+ switch (type) {
+
+ case T_BOOL: {
+ int v = PyObject_IsTrue(value);
+ if (v == -1) {
+ return false;
+ }
+
+ writeByte(output, (int8_t) v);
+ break;
+ }
+ case T_I08: {
+ int32_t val;
+
+ if (!parse_pyint(value, &val, INT8_MIN, INT8_MAX)) {
+ return false;
+ }
+
+ writeByte(output, (int8_t) val);
+ break;
+ }
+ case T_I16: {
+ int32_t val;
+
+ if (!parse_pyint(value, &val, INT16_MIN, INT16_MAX)) {
+ return false;
+ }
+
+ writeI16(output, (int16_t) val);
+ break;
+ }
+ case T_I32: {
+ int32_t val;
+
+ if (!parse_pyint(value, &val, INT32_MIN, INT32_MAX)) {
+ return false;
+ }
+
+ writeI32(output, val);
+ break;
+ }
+ case T_I64: {
+ int64_t nval = PyLong_AsLongLong(value);
+
+ if (INT_CONV_ERROR_OCCURRED(nval)) {
+ return false;
+ }
+
+ if (!CHECK_RANGE(nval, INT64_MIN, INT64_MAX)) {
+ PyErr_SetString(PyExc_OverflowError, "int out of range");
+ return false;
+ }
+
+ writeI64(output, nval);
+ break;
+ }
+
+ case T_DOUBLE: {
+ double nval = PyFloat_AsDouble(value);
+ if (nval == -1.0 && PyErr_Occurred()) {
+ return false;
+ }
+
+ writeDouble(output, nval);
+ break;
+ }
+
+ case T_STRING: {
+ Py_ssize_t len = PyString_Size(value);
+
+ if (!check_ssize_t_32(len)) {
+ return false;
+ }
+
+ writeI32(output, (int32_t) len);
+ PycStringIO->cwrite(output, PyString_AsString(value), (int32_t) len);
+ break;
+ }
+
+ case T_LIST:
+ case T_SET: {
+ Py_ssize_t len;
+ SetListTypeArgs parsedargs;
+ PyObject *item;
+ PyObject *iterator;
+
+ if (!parse_set_list_args(&parsedargs, typeargs)) {
+ return false;
+ }
+
+ len = PyObject_Length(value);
+
+ if (!check_ssize_t_32(len)) {
+ return false;
+ }
+
+ writeByte(output, parsedargs.element_type);
+ writeI32(output, (int32_t) len);
+
+ iterator = PyObject_GetIter(value);
+ if (iterator == NULL) {
+ return false;
+ }
+
+ while ((item = PyIter_Next(iterator))) {
+ if (!output_val(output, item, parsedargs.element_type, parsedargs.typeargs)) {
+ Py_DECREF(item);
+ Py_DECREF(iterator);
+ return false;
+ }
+ Py_DECREF(item);
+ }
+
+ Py_DECREF(iterator);
+
+ if (PyErr_Occurred()) {
+ return false;
+ }
+
+ break;
+ }
+
+ case T_MAP: {
+ PyObject *k, *v;
+ Py_ssize_t pos = 0;
+ Py_ssize_t len;
+
+ MapTypeArgs parsedargs;
+
+ len = PyDict_Size(value);
+ if (!check_ssize_t_32(len)) {
+ return false;
+ }
+
+ if (!parse_map_args(&parsedargs, typeargs)) {
+ return false;
+ }
+
+ writeByte(output, parsedargs.ktag);
+ writeByte(output, parsedargs.vtag);
+ writeI32(output, len);
+
+ // TODO(bmaurer): should support any mapping, not just dicts
+ while (PyDict_Next(value, &pos, &k, &v)) {
+ // TODO(dreiss): Think hard about whether these INCREFs actually
+ // turn any unsafe scenarios into safe scenarios.
+ Py_INCREF(k);
+ Py_INCREF(v);
+
+ if (!output_val(output, k, parsedargs.ktag, parsedargs.ktypeargs)
+ || !output_val(output, v, parsedargs.vtag, parsedargs.vtypeargs)) {
+ Py_DECREF(k);
+ Py_DECREF(v);
+ return false;
+ }
+ Py_DECREF(k);
+ Py_DECREF(v);
+ }
+ break;
+ }
+
+ // TODO(dreiss): Consider breaking this out as a function
+ // the way we did for decode_struct.
+ case T_STRUCT: {
+ StructTypeArgs parsedargs;
+ Py_ssize_t nspec;
+ Py_ssize_t i;
+
+ if (!parse_struct_args(&parsedargs, typeargs)) {
+ return false;
+ }
+
+ nspec = PyTuple_Size(parsedargs.spec);
+
+ if (nspec == -1) {
+ return false;
+ }
+
+ for (i = 0; i < nspec; i++) {
+ StructItemSpec parsedspec;
+ PyObject* spec_tuple;
+ PyObject* instval = NULL;
+
+ spec_tuple = PyTuple_GET_ITEM(parsedargs.spec, i);
+ if (spec_tuple == Py_None) {
+ continue;
+ }
+
+ if (!parse_struct_item_spec (&parsedspec, spec_tuple)) {
+ return false;
+ }
+
+ instval = PyObject_GetAttr(value, parsedspec.attrname);
+
+ if (!instval) {
+ return false;
+ }
+
+ if (instval == Py_None) {
+ Py_DECREF(instval);
+ continue;
+ }
+
+ writeByte(output, (int8_t) parsedspec.type);
+ writeI16(output, parsedspec.tag);
+
+ if (!output_val(output, instval, parsedspec.type, parsedspec.typeargs)) {
+ Py_DECREF(instval);
+ return false;
+ }
+
+ Py_DECREF(instval);
+ }
+
+ writeByte(output, (int8_t)T_STOP);
+ break;
+ }
+
+ case T_STOP:
+ case T_VOID:
+ case T_UTF16:
+ case T_UTF8:
+ case T_U64:
+ default:
+ PyErr_SetString(PyExc_TypeError, "Unexpected TType");
+ return false;
+
+ }
+
+ return true;
+}
+
+
+/* --- TOP-LEVEL WRAPPER FOR OUTPUT -- */
+
+static PyObject *
+encode_binary(PyObject *self, PyObject *args) {
+ PyObject* enc_obj;
+ PyObject* type_args;
+ PyObject* buf;
+ PyObject* ret = NULL;
+
+ if (!PyArg_ParseTuple(args, "OO", &enc_obj, &type_args)) {
+ return NULL;
+ }
+
+ buf = PycStringIO->NewOutput(INIT_OUTBUF_SIZE);
+ if (output_val(buf, enc_obj, T_STRUCT, type_args)) {
+ ret = PycStringIO->cgetvalue(buf);
+ }
+
+ Py_DECREF(buf);
+ return ret;
+}
+
+/* ====== END WRITING FUNCTIONS ====== */
+
+
+/* ====== BEGIN READING FUNCTIONS ====== */
+
+/* --- LOW-LEVEL READING FUNCTIONS --- */
+
+static void
+free_decodebuf(DecodeBuffer* d) {
+ Py_XDECREF(d->stringiobuf);
+ Py_XDECREF(d->refill_callable);
+}
+
+static bool
+decode_buffer_from_obj(DecodeBuffer* dest, PyObject* obj) {
+ dest->stringiobuf = PyObject_GetAttr(obj, INTERN_STRING(cstringio_buf));
+ if (!dest->stringiobuf) {
+ return false;
+ }
+
+ if (!PycStringIO_InputCheck(dest->stringiobuf)) {
+ free_decodebuf(dest);
+ PyErr_SetString(PyExc_TypeError, "expecting stringio input");
+ return false;
+ }
+
+ dest->refill_callable = PyObject_GetAttr(obj, INTERN_STRING(cstringio_refill));
+
+ if(!dest->refill_callable) {
+ free_decodebuf(dest);
+ return false;
+ }
+
+ if (!PyCallable_Check(dest->refill_callable)) {
+ free_decodebuf(dest);
+ PyErr_SetString(PyExc_TypeError, "expecting callable");
+ return false;
+ }
+
+ return true;
+}
+
+static bool readBytes(DecodeBuffer* input, char** output, int len) {
+ int read;
+
+ // TODO(dreiss): Don't fear the malloc. Think about taking a copy of
+ // the partial read instead of forcing the transport
+ // to prepend it to its buffer.
+
+ read = PycStringIO->cread(input->stringiobuf, output, len);
+
+ if (read == len) {
+ return true;
+ } else if (read == -1) {
+ return false;
+ } else {
+ PyObject* newiobuf;
+
+ // using building functions as this is a rare codepath
+ newiobuf = PyObject_CallFunction(
+ input->refill_callable, "s#i", *output, read, len, NULL);
+ if (newiobuf == NULL) {
+ return false;
+ }
+
+ // must do this *AFTER* the call so that we don't deref the io buffer
+ Py_CLEAR(input->stringiobuf);
+ input->stringiobuf = newiobuf;
+
+ read = PycStringIO->cread(input->stringiobuf, output, len);
+
+ if (read == len) {
+ return true;
+ } else if (read == -1) {
+ return false;
+ } else {
+ // TODO(dreiss): This could be a valid code path for big binary blobs.
+ PyErr_SetString(PyExc_TypeError,
+ "refill claimed to have refilled the buffer, but didn't!!");
+ return false;
+ }
+ }
+}
+
+static int8_t readByte(DecodeBuffer* input) {
+ char* buf;
+ if (!readBytes(input, &buf, sizeof(int8_t))) {
+ return -1;
+ }
+
+ return *(int8_t*) buf;
+}
+
+static int16_t readI16(DecodeBuffer* input) {
+ char* buf;
+ if (!readBytes(input, &buf, sizeof(int16_t))) {
+ return -1;
+ }
+
+ return (int16_t) ntohs(*(int16_t*) buf);
+}
+
+static int32_t readI32(DecodeBuffer* input) {
+ char* buf;
+ if (!readBytes(input, &buf, sizeof(int32_t))) {
+ return -1;
+ }
+ return (int32_t) ntohl(*(int32_t*) buf);
+}
+
+
+static int64_t readI64(DecodeBuffer* input) {
+ char* buf;
+ if (!readBytes(input, &buf, sizeof(int64_t))) {
+ return -1;
+ }
+
+ return (int64_t) ntohll(*(int64_t*) buf);
+}
+
+static double readDouble(DecodeBuffer* input) {
+ union {
+ int64_t f;
+ double t;
+ } transfer;
+
+ transfer.f = readI64(input);
+ if (transfer.f == -1) {
+ return -1;
+ }
+ return transfer.t;
+}
+
+static bool
+checkTypeByte(DecodeBuffer* input, TType expected) {
+ TType got = readByte(input);
+ if (INT_CONV_ERROR_OCCURRED(got)) {
+ return false;
+ }
+
+ if (expected != got) {
+ PyErr_SetString(PyExc_TypeError, "got wrong ttype while reading field");
+ return false;
+ }
+ return true;
+}
+
+static bool
+skip(DecodeBuffer* input, TType type) {
+#define SKIPBYTES(n) \
+ do { \
+ if (!readBytes(input, &dummy_buf, (n))) { \
+ return false; \
+ } \
+ } while(0)
+
+ char* dummy_buf;
+
+ switch (type) {
+
+ case T_BOOL:
+ case T_I08: SKIPBYTES(1); break;
+ case T_I16: SKIPBYTES(2); break;
+ case T_I32: SKIPBYTES(4); break;
+ case T_I64:
+ case T_DOUBLE: SKIPBYTES(8); break;
+
+ case T_STRING: {
+ // TODO(dreiss): Find out if these check_ssize_t32s are really necessary.
+ int len = readI32(input);
+ if (!check_ssize_t_32(len)) {
+ return false;
+ }
+ SKIPBYTES(len);
+ break;
+ }
+
+ case T_LIST:
+ case T_SET: {
+ TType etype;
+ int len, i;
+
+ etype = readByte(input);
+ if (etype == -1) {
+ return false;
+ }
+
+ len = readI32(input);
+ if (!check_ssize_t_32(len)) {
+ return false;
+ }
+
+ for (i = 0; i < len; i++) {
+ if (!skip(input, etype)) {
+ return false;
+ }
+ }
+ break;
+ }
+
+ case T_MAP: {
+ TType ktype, vtype;
+ int len, i;
+
+ ktype = readByte(input);
+ if (ktype == -1) {
+ return false;
+ }
+
+ vtype = readByte(input);
+ if (vtype == -1) {
+ return false;
+ }
+
+ len = readI32(input);
+ if (!check_ssize_t_32(len)) {
+ return false;
+ }
+
+ for (i = 0; i < len; i++) {
+ if (!(skip(input, ktype) && skip(input, vtype))) {
+ return false;
+ }
+ }
+ break;
+ }
+
+ case T_STRUCT: {
+ while (true) {
+ TType type;
+
+ type = readByte(input);
+ if (type == -1) {
+ return false;
+ }
+
+ if (type == T_STOP)
+ break;
+
+ SKIPBYTES(2); // tag
+ if (!skip(input, type)) {
+ return false;
+ }
+ }
+ break;
+ }
+
+ case T_STOP:
+ case T_VOID:
+ case T_UTF16:
+ case T_UTF8:
+ case T_U64:
+ default:
+ PyErr_SetString(PyExc_TypeError, "Unexpected TType");
+ return false;
+
+ }
+
+ return true;
+
+#undef SKIPBYTES
+}
+
+
+/* --- HELPER FUNCTION FOR DECODE_VAL --- */
+
+static PyObject*
+decode_val(DecodeBuffer* input, TType type, PyObject* typeargs);
+
+static bool
+decode_struct(DecodeBuffer* input, PyObject* output, PyObject* spec_seq) {
+ int spec_seq_len = PyTuple_Size(spec_seq);
+ if (spec_seq_len == -1) {
+ return false;
+ }
+
+ while (true) {
+ TType type;
+ int16_t tag;
+ PyObject* item_spec;
+ PyObject* fieldval = NULL;
+ StructItemSpec parsedspec;
+
+ type = readByte(input);
+ if (type == -1) {
+ return false;
+ }
+ if (type == T_STOP) {
+ break;
+ }
+ tag = readI16(input);
+ if (INT_CONV_ERROR_OCCURRED(tag)) {
+ return false;
+ }
+ if (tag >= 0 && tag < spec_seq_len) {
+ item_spec = PyTuple_GET_ITEM(spec_seq, tag);
+ } else {
+ item_spec = Py_None;
+ }
+
+ if (item_spec == Py_None) {
+ if (!skip(input, type)) {
+ return false;
+ } else {
+ continue;
+ }
+ }
+
+ if (!parse_struct_item_spec(&parsedspec, item_spec)) {
+ return false;
+ }
+ if (parsedspec.type != type) {
+ if (!skip(input, type)) {
+ PyErr_SetString(PyExc_TypeError, "struct field had wrong type while reading and can't be skipped");
+ return false;
+ } else {
+ continue;
+ }
+ }
+
+ fieldval = decode_val(input, parsedspec.type, parsedspec.typeargs);
+ if (fieldval == NULL) {
+ return false;
+ }
+
+ if (PyObject_SetAttr(output, parsedspec.attrname, fieldval) == -1) {
+ Py_DECREF(fieldval);
+ return false;
+ }
+ Py_DECREF(fieldval);
+ }
+ return true;
+}
+
+
+/* --- MAIN RECURSIVE INPUT FUCNTION --- */
+
+// Returns a new reference.
+static PyObject*
+decode_val(DecodeBuffer* input, TType type, PyObject* typeargs) {
+ switch (type) {
+
+ case T_BOOL: {
+ int8_t v = readByte(input);
+ if (INT_CONV_ERROR_OCCURRED(v)) {
+ return NULL;
+ }
+
+ switch (v) {
+ case 0: Py_RETURN_FALSE;
+ case 1: Py_RETURN_TRUE;
+ // Don't laugh. This is a potentially serious issue.
+ default: PyErr_SetString(PyExc_TypeError, "boolean out of range"); return NULL;
+ }
+ break;
+ }
+ case T_I08: {
+ int8_t v = readByte(input);
+ if (INT_CONV_ERROR_OCCURRED(v)) {
+ return NULL;
+ }
+
+ return PyInt_FromLong(v);
+ }
+ case T_I16: {
+ int16_t v = readI16(input);
+ if (INT_CONV_ERROR_OCCURRED(v)) {
+ return NULL;
+ }
+ return PyInt_FromLong(v);
+ }
+ case T_I32: {
+ int32_t v = readI32(input);
+ if (INT_CONV_ERROR_OCCURRED(v)) {
+ return NULL;
+ }
+ return PyInt_FromLong(v);
+ }
+
+ case T_I64: {
+ int64_t v = readI64(input);
+ if (INT_CONV_ERROR_OCCURRED(v)) {
+ return NULL;
+ }
+ // TODO(dreiss): Find out if we can take this fastpath always when
+ // sizeof(long) == sizeof(long long).
+ if (CHECK_RANGE(v, LONG_MIN, LONG_MAX)) {
+ return PyInt_FromLong((long) v);
+ }
+
+ return PyLong_FromLongLong(v);
+ }
+
+ case T_DOUBLE: {
+ double v = readDouble(input);
+ if (v == -1.0 && PyErr_Occurred()) {
+ return false;
+ }
+ return PyFloat_FromDouble(v);
+ }
+
+ case T_STRING: {
+ Py_ssize_t len = readI32(input);
+ char* buf;
+ if (!readBytes(input, &buf, len)) {
+ return NULL;
+ }
+
+ return PyString_FromStringAndSize(buf, len);
+ }
+
+ case T_LIST:
+ case T_SET: {
+ SetListTypeArgs parsedargs;
+ int32_t len;
+ PyObject* ret = NULL;
+ int i;
+
+ if (!parse_set_list_args(&parsedargs, typeargs)) {
+ return NULL;
+ }
+
+ if (!checkTypeByte(input, parsedargs.element_type)) {
+ return NULL;
+ }
+
+ len = readI32(input);
+ if (!check_ssize_t_32(len)) {
+ return NULL;
+ }
+
+ ret = PyList_New(len);
+ if (!ret) {
+ return NULL;
+ }
+
+ for (i = 0; i < len; i++) {
+ PyObject* item = decode_val(input, parsedargs.element_type, parsedargs.typeargs);
+ if (!item) {
+ Py_DECREF(ret);
+ return NULL;
+ }
+ PyList_SET_ITEM(ret, i, item);
+ }
+
+ // TODO(dreiss): Consider biting the bullet and making two separate cases
+ // for list and set, avoiding this post facto conversion.
+ if (type == T_SET) {
+ PyObject* setret;
+#if (PY_VERSION_HEX < 0x02050000)
+ // hack needed for older versions
+ setret = PyObject_CallFunctionObjArgs((PyObject*)&PySet_Type, ret, NULL);
+#else
+ // official version
+ setret = PySet_New(ret);
+#endif
+ Py_DECREF(ret);
+ return setret;
+ }
+ return ret;
+ }
+
+ case T_MAP: {
+ int32_t len;
+ int i;
+ MapTypeArgs parsedargs;
+ PyObject* ret = NULL;
+
+ if (!parse_map_args(&parsedargs, typeargs)) {
+ return NULL;
+ }
+
+ if (!checkTypeByte(input, parsedargs.ktag)) {
+ return NULL;
+ }
+ if (!checkTypeByte(input, parsedargs.vtag)) {
+ return NULL;
+ }
+
+ len = readI32(input);
+ if (!check_ssize_t_32(len)) {
+ return false;
+ }
+
+ ret = PyDict_New();
+ if (!ret) {
+ goto error;
+ }
+
+ for (i = 0; i < len; i++) {
+ PyObject* k = NULL;
+ PyObject* v = NULL;
+ k = decode_val(input, parsedargs.ktag, parsedargs.ktypeargs);
+ if (k == NULL) {
+ goto loop_error;
+ }
+ v = decode_val(input, parsedargs.vtag, parsedargs.vtypeargs);
+ if (v == NULL) {
+ goto loop_error;
+ }
+ if (PyDict_SetItem(ret, k, v) == -1) {
+ goto loop_error;
+ }
+
+ Py_DECREF(k);
+ Py_DECREF(v);
+ continue;
+
+ // Yuck! Destructors, anyone?
+ loop_error:
+ Py_XDECREF(k);
+ Py_XDECREF(v);
+ goto error;
+ }
+
+ return ret;
+
+ error:
+ Py_XDECREF(ret);
+ return NULL;
+ }
+
+ case T_STRUCT: {
+ StructTypeArgs parsedargs;
+ if (!parse_struct_args(&parsedargs, typeargs)) {
+ return NULL;
+ }
+
+ PyObject* ret = PyObject_CallObject(parsedargs.klass, NULL);
+ if (!ret) {
+ return NULL;
+ }
+
+ if (!decode_struct(input, ret, parsedargs.spec)) {
+ Py_DECREF(ret);
+ return NULL;
+ }
+
+ return ret;
+ }
+
+ case T_STOP:
+ case T_VOID:
+ case T_UTF16:
+ case T_UTF8:
+ case T_U64:
+ default:
+ PyErr_SetString(PyExc_TypeError, "Unexpected TType");
+ return NULL;
+ }
+}
+
+
+/* --- TOP-LEVEL WRAPPER FOR INPUT -- */
+
+static PyObject*
+decode_binary(PyObject *self, PyObject *args) {
+ PyObject* output_obj = NULL;
+ PyObject* transport = NULL;
+ PyObject* typeargs = NULL;
+ StructTypeArgs parsedargs;
+ DecodeBuffer input = {};
+
+ if (!PyArg_ParseTuple(args, "OOO", &output_obj, &transport, &typeargs)) {
+ return NULL;
+ }
+
+ if (!parse_struct_args(&parsedargs, typeargs)) {
+ return NULL;
+ }
+
+ if (!decode_buffer_from_obj(&input, transport)) {
+ return NULL;
+ }
+
+ if (!decode_struct(&input, output_obj, parsedargs.spec)) {
+ free_decodebuf(&input);
+ return NULL;
+ }
+
+ free_decodebuf(&input);
+
+ Py_RETURN_NONE;
+}
+
+/* ====== END READING FUNCTIONS ====== */
+
+
+/* -- PYTHON MODULE SETUP STUFF --- */
+
+static PyMethodDef ThriftFastBinaryMethods[] = {
+
+ {"encode_binary", encode_binary, METH_VARARGS, ""},
+ {"decode_binary", decode_binary, METH_VARARGS, ""},
+
+ {NULL, NULL, 0, NULL} /* Sentinel */
+};
+
+PyMODINIT_FUNC
+initfastbinary(void) {
+#define INIT_INTERN_STRING(value) \
+ do { \
+ INTERN_STRING(value) = PyString_InternFromString(#value); \
+ if(!INTERN_STRING(value)) return; \
+ } while(0)
+
+ INIT_INTERN_STRING(cstringio_buf);
+ INIT_INTERN_STRING(cstringio_refill);
+#undef INIT_INTERN_STRING
+
+ PycString_IMPORT;
+ if (PycStringIO == NULL) return;
+
+ (void) Py_InitModule("thrift.protocol.fastbinary", ThriftFastBinaryMethods);
+}
diff --git a/api/thrift/server/THttpServer.py b/api/thrift/server/THttpServer.py
new file mode 100644
index 0000000..3047d9c
--- /dev/null
+++ b/api/thrift/server/THttpServer.py
@@ -0,0 +1,82 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+import BaseHTTPServer
+
+from thrift.server import TServer
+from thrift.transport import TTransport
+
+class ResponseException(Exception):
+ """Allows handlers to override the HTTP response
+
+ Normally, THttpServer always sends a 200 response. If a handler wants
+ to override this behavior (e.g., to simulate a misconfigured or
+ overloaded web server during testing), it can raise a ResponseException.
+ The function passed to the constructor will be called with the
+ RequestHandler as its only argument.
+ """
+ def __init__(self, handler):
+ self.handler = handler
+
+
+class THttpServer(TServer.TServer):
+ """A simple HTTP-based Thrift server
+
+ This class is not very performant, but it is useful (for example) for
+ acting as a mock version of an Apache-based PHP Thrift endpoint."""
+
+ def __init__(self, processor, server_address,
+ inputProtocolFactory, outputProtocolFactory = None,
+ server_class = BaseHTTPServer.HTTPServer):
+ """Set up protocol factories and HTTP server.
+
+ See BaseHTTPServer for server_address.
+ See TServer for protocol factories."""
+
+ if outputProtocolFactory is None:
+ outputProtocolFactory = inputProtocolFactory
+
+ TServer.TServer.__init__(self, processor, None, None, None,
+ inputProtocolFactory, outputProtocolFactory)
+
+ thttpserver = self
+
+ class RequestHander(BaseHTTPServer.BaseHTTPRequestHandler):
+ def do_POST(self):
+ # Don't care about the request path.
+ itrans = TTransport.TFileObjectTransport(self.rfile)
+ otrans = TTransport.TFileObjectTransport(self.wfile)
+ itrans = TTransport.TBufferedTransport(itrans, int(self.headers['Content-Length']))
+ otrans = TTransport.TMemoryBuffer()
+ iprot = thttpserver.inputProtocolFactory.getProtocol(itrans)
+ oprot = thttpserver.outputProtocolFactory.getProtocol(otrans)
+ try:
+ thttpserver.processor.process(iprot, oprot)
+ except ResponseException, exn:
+ exn.handler(self)
+ else:
+ self.send_response(200)
+ self.send_header("content-type", "application/x-thrift")
+ self.end_headers()
+ self.wfile.write(otrans.getvalue())
+
+ self.httpd = server_class(server_address, RequestHander)
+
+ def serve(self):
+ self.httpd.serve_forever()
diff --git a/api/thrift/server/TNonblockingServer.py b/api/thrift/server/TNonblockingServer.py
new file mode 100644
index 0000000..ea348a0
--- /dev/null
+++ b/api/thrift/server/TNonblockingServer.py
@@ -0,0 +1,310 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+"""Implementation of non-blocking server.
+
+The main idea of the server is reciving and sending requests
+only from main thread.
+
+It also makes thread pool server in tasks terms, not connections.
+"""
+import threading
+import socket
+import Queue
+import select
+import struct
+import logging
+
+from thrift.transport import TTransport
+from thrift.protocol.TBinaryProtocol import TBinaryProtocolFactory
+
+__all__ = ['TNonblockingServer']
+
+class Worker(threading.Thread):
+ """Worker is a small helper to process incoming connection."""
+ def __init__(self, queue):
+ threading.Thread.__init__(self)
+ self.queue = queue
+
+ def run(self):
+ """Process queries from task queue, stop if processor is None."""
+ while True:
+ try:
+ processor, iprot, oprot, otrans, callback = self.queue.get()
+ if processor is None:
+ break
+ processor.process(iprot, oprot)
+ callback(True, otrans.getvalue())
+ except Exception:
+ logging.exception("Exception while processing request")
+ callback(False, '')
+
+WAIT_LEN = 0
+WAIT_MESSAGE = 1
+WAIT_PROCESS = 2
+SEND_ANSWER = 3
+CLOSED = 4
+
+def locked(func):
+ "Decorator which locks self.lock."
+ def nested(self, *args, **kwargs):
+ self.lock.acquire()
+ try:
+ return func(self, *args, **kwargs)
+ finally:
+ self.lock.release()
+ return nested
+
+def socket_exception(func):
+ "Decorator close object on socket.error."
+ def read(self, *args, **kwargs):
+ try:
+ return func(self, *args, **kwargs)
+ except socket.error:
+ self.close()
+ return read
+
+class Connection:
+ """Basic class is represented connection.
+
+ It can be in state:
+ WAIT_LEN --- connection is reading request len.
+ WAIT_MESSAGE --- connection is reading request.
+ WAIT_PROCESS --- connection has just read whole request and
+ waits for call ready routine.
+ SEND_ANSWER --- connection is sending answer string (including length
+ of answer).
+ CLOSED --- socket was closed and connection should be deleted.
+ """
+ def __init__(self, new_socket, wake_up):
+ self.socket = new_socket
+ self.socket.setblocking(False)
+ self.status = WAIT_LEN
+ self.len = 0
+ self.message = ''
+ self.lock = threading.Lock()
+ self.wake_up = wake_up
+
+ def _read_len(self):
+ """Reads length of request.
+
+ It's really paranoic routine and it may be replaced by
+ self.socket.recv(4)."""
+ read = self.socket.recv(4 - len(self.message))
+ if len(read) == 0:
+ # if we read 0 bytes and self.message is empty, it means client close
+ # connection
+ if len(self.message) != 0:
+ logging.error("can't read frame size from socket")
+ self.close()
+ return
+ self.message += read
+ if len(self.message) == 4:
+ self.len, = struct.unpack('!i', self.message)
+ if self.len < 0:
+ logging.error("negative frame size, it seems client"\
+ " doesn't use FramedTransport")
+ self.close()
+ elif self.len == 0:
+ logging.error("empty frame, it's really strange")
+ self.close()
+ else:
+ self.message = ''
+ self.status = WAIT_MESSAGE
+
+ @socket_exception
+ def read(self):
+ """Reads data from stream and switch state."""
+ assert self.status in (WAIT_LEN, WAIT_MESSAGE)
+ if self.status == WAIT_LEN:
+ self._read_len()
+ # go back to the main loop here for simplicity instead of
+ # falling through, even though there is a good chance that
+ # the message is already available
+ elif self.status == WAIT_MESSAGE:
+ read = self.socket.recv(self.len - len(self.message))
+ if len(read) == 0:
+ logging.error("can't read frame from socket (get %d of %d bytes)" %
+ (len(self.message), self.len))
+ self.close()
+ return
+ self.message += read
+ if len(self.message) == self.len:
+ self.status = WAIT_PROCESS
+
+ @socket_exception
+ def write(self):
+ """Writes data from socket and switch state."""
+ assert self.status == SEND_ANSWER
+ sent = self.socket.send(self.message)
+ if sent == len(self.message):
+ self.status = WAIT_LEN
+ self.message = ''
+ self.len = 0
+ else:
+ self.message = self.message[sent:]
+
+ @locked
+ def ready(self, all_ok, message):
+ """Callback function for switching state and waking up main thread.
+
+ This function is the only function witch can be called asynchronous.
+
+ The ready can switch Connection to three states:
+ WAIT_LEN if request was oneway.
+ SEND_ANSWER if request was processed in normal way.
+ CLOSED if request throws unexpected exception.
+
+ The one wakes up main thread.
+ """
+ assert self.status == WAIT_PROCESS
+ if not all_ok:
+ self.close()
+ self.wake_up()
+ return
+ self.len = ''
+ if len(message) == 0:
+ # it was a oneway request, do not write answer
+ self.message = ''
+ self.status = WAIT_LEN
+ else:
+ self.message = struct.pack('!i', len(message)) + message
+ self.status = SEND_ANSWER
+ self.wake_up()
+
+ @locked
+ def is_writeable(self):
+ "Returns True if connection should be added to write list of select."
+ return self.status == SEND_ANSWER
+
+ # it's not necessary, but...
+ @locked
+ def is_readable(self):
+ "Returns True if connection should be added to read list of select."
+ return self.status in (WAIT_LEN, WAIT_MESSAGE)
+
+ @locked
+ def is_closed(self):
+ "Returns True if connection is closed."
+ return self.status == CLOSED
+
+ def fileno(self):
+ "Returns the file descriptor of the associated socket."
+ return self.socket.fileno()
+
+ def close(self):
+ "Closes connection"
+ self.status = CLOSED
+ self.socket.close()
+
+class TNonblockingServer:
+ """Non-blocking server."""
+ def __init__(self, processor, lsocket, inputProtocolFactory=None,
+ outputProtocolFactory=None, threads=10):
+ self.processor = processor
+ self.socket = lsocket
+ self.in_protocol = inputProtocolFactory or TBinaryProtocolFactory()
+ self.out_protocol = outputProtocolFactory or self.in_protocol
+ self.threads = int(threads)
+ self.clients = {}
+ self.tasks = Queue.Queue()
+ self._read, self._write = socket.socketpair()
+ self.prepared = False
+
+ def setNumThreads(self, num):
+ """Set the number of worker threads that should be created."""
+ # implement ThreadPool interface
+ assert not self.prepared, "You can't change number of threads for working server"
+ self.threads = num
+
+ def prepare(self):
+ """Prepares server for serve requests."""
+ self.socket.listen()
+ for _ in xrange(self.threads):
+ thread = Worker(self.tasks)
+ thread.setDaemon(True)
+ thread.start()
+ self.prepared = True
+
+ def wake_up(self):
+ """Wake up main thread.
+
+ The server usualy waits in select call in we should terminate one.
+ The simplest way is using socketpair.
+
+ Select always wait to read from the first socket of socketpair.
+
+ In this case, we can just write anything to the second socket from
+ socketpair."""
+ self._write.send('1')
+
+ def _select(self):
+ """Does select on open connections."""
+ readable = [self.socket.handle.fileno(), self._read.fileno()]
+ writable = []
+ for i, connection in self.clients.items():
+ if connection.is_readable():
+ readable.append(connection.fileno())
+ if connection.is_writeable():
+ writable.append(connection.fileno())
+ if connection.is_closed():
+ del self.clients[i]
+ return select.select(readable, writable, readable)
+
+ def handle(self):
+ """Handle requests.
+
+ WARNING! You must call prepare BEFORE calling handle.
+ """
+ assert self.prepared, "You have to call prepare before handle"
+ rset, wset, xset = self._select()
+ for readable in rset:
+ if readable == self._read.fileno():
+ # don't care i just need to clean readable flag
+ self._read.recv(1024)
+ elif readable == self.socket.handle.fileno():
+ client = self.socket.accept().handle
+ self.clients[client.fileno()] = Connection(client, self.wake_up)
+ else:
+ connection = self.clients[readable]
+ connection.read()
+ if connection.status == WAIT_PROCESS:
+ itransport = TTransport.TMemoryBuffer(connection.message)
+ otransport = TTransport.TMemoryBuffer()
+ iprot = self.in_protocol.getProtocol(itransport)
+ oprot = self.out_protocol.getProtocol(otransport)
+ self.tasks.put([self.processor, iprot, oprot,
+ otransport, connection.ready])
+ for writeable in wset:
+ self.clients[writeable].write()
+ for oob in xset:
+ self.clients[oob].close()
+ del self.clients[oob]
+
+ def close(self):
+ """Closes the server."""
+ for _ in xrange(self.threads):
+ self.tasks.put([None, None, None, None, None])
+ self.socket.close()
+ self.prepared = False
+
+ def serve(self):
+ """Serve forever."""
+ self.prepare()
+ while True:
+ self.handle()
diff --git a/api/thrift/server/TServer.py b/api/thrift/server/TServer.py
new file mode 100644
index 0000000..8456e2d
--- /dev/null
+++ b/api/thrift/server/TServer.py
@@ -0,0 +1,274 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+import logging
+import sys
+import os
+import traceback
+import threading
+import Queue
+
+from thrift.Thrift import TProcessor
+from thrift.transport import TTransport
+from thrift.protocol import TBinaryProtocol
+
+class TServer:
+
+ """Base interface for a server, which must have a serve method."""
+
+ """ 3 constructors for all servers:
+ 1) (processor, serverTransport)
+ 2) (processor, serverTransport, transportFactory, protocolFactory)
+ 3) (processor, serverTransport,
+ inputTransportFactory, outputTransportFactory,
+ inputProtocolFactory, outputProtocolFactory)"""
+ def __init__(self, *args):
+ if (len(args) == 2):
+ self.__initArgs__(args[0], args[1],
+ TTransport.TTransportFactoryBase(),
+ TTransport.TTransportFactoryBase(),
+ TBinaryProtocol.TBinaryProtocolFactory(),
+ TBinaryProtocol.TBinaryProtocolFactory())
+ elif (len(args) == 4):
+ self.__initArgs__(args[0], args[1], args[2], args[2], args[3], args[3])
+ elif (len(args) == 6):
+ self.__initArgs__(args[0], args[1], args[2], args[3], args[4], args[5])
+
+ def __initArgs__(self, processor, serverTransport,
+ inputTransportFactory, outputTransportFactory,
+ inputProtocolFactory, outputProtocolFactory):
+ self.processor = processor
+ self.serverTransport = serverTransport
+ self.inputTransportFactory = inputTransportFactory
+ self.outputTransportFactory = outputTransportFactory
+ self.inputProtocolFactory = inputProtocolFactory
+ self.outputProtocolFactory = outputProtocolFactory
+
+ def serve(self):
+ pass
+
+class TSimpleServer(TServer):
+
+ """Simple single-threaded server that just pumps around one transport."""
+
+ def __init__(self, *args):
+ TServer.__init__(self, *args)
+
+ def serve(self):
+ self.serverTransport.listen()
+ while True:
+ client = self.serverTransport.accept()
+ itrans = self.inputTransportFactory.getTransport(client)
+ otrans = self.outputTransportFactory.getTransport(client)
+ iprot = self.inputProtocolFactory.getProtocol(itrans)
+ oprot = self.outputProtocolFactory.getProtocol(otrans)
+ try:
+ while True:
+ self.processor.process(iprot, oprot)
+ except TTransport.TTransportException, tx:
+ pass
+ except Exception, x:
+ logging.exception(x)
+
+ itrans.close()
+ otrans.close()
+
+class TThreadedServer(TServer):
+
+ """Threaded server that spawns a new thread per each connection."""
+
+ def __init__(self, *args, **kwargs):
+ TServer.__init__(self, *args)
+ self.daemon = kwargs.get("daemon", False)
+
+ def serve(self):
+ self.serverTransport.listen()
+ while True:
+ try:
+ client = self.serverTransport.accept()
+ t = threading.Thread(target = self.handle, args=(client,))
+ t.setDaemon(self.daemon)
+ t.start()
+ except KeyboardInterrupt:
+ raise
+ except Exception, x:
+ logging.exception(x)
+
+ def handle(self, client):
+ itrans = self.inputTransportFactory.getTransport(client)
+ otrans = self.outputTransportFactory.getTransport(client)
+ iprot = self.inputProtocolFactory.getProtocol(itrans)
+ oprot = self.outputProtocolFactory.getProtocol(otrans)
+ try:
+ while True:
+ self.processor.process(iprot, oprot)
+ except TTransport.TTransportException, tx:
+ pass
+ except Exception, x:
+ logging.exception(x)
+
+ itrans.close()
+ otrans.close()
+
+class TThreadPoolServer(TServer):
+
+ """Server with a fixed size pool of threads which service requests."""
+
+ def __init__(self, *args, **kwargs):
+ TServer.__init__(self, *args)
+ self.clients = Queue.Queue()
+ self.threads = 10
+ self.daemon = kwargs.get("daemon", False)
+
+ def setNumThreads(self, num):
+ """Set the number of worker threads that should be created"""
+ self.threads = num
+
+ def serveThread(self):
+ """Loop around getting clients from the shared queue and process them."""
+ while True:
+ try:
+ client = self.clients.get()
+ self.serveClient(client)
+ except Exception, x:
+ logging.exception(x)
+
+ def serveClient(self, client):
+ """Process input/output from a client for as long as possible"""
+ itrans = self.inputTransportFactory.getTransport(client)
+ otrans = self.outputTransportFactory.getTransport(client)
+ iprot = self.inputProtocolFactory.getProtocol(itrans)
+ oprot = self.outputProtocolFactory.getProtocol(otrans)
+ try:
+ while True:
+ self.processor.process(iprot, oprot)
+ except TTransport.TTransportException, tx:
+ pass
+ except Exception, x:
+ logging.exception(x)
+
+ itrans.close()
+ otrans.close()
+
+ def serve(self):
+ """Start a fixed number of worker threads and put client into a queue"""
+ for i in range(self.threads):
+ try:
+ t = threading.Thread(target = self.serveThread)
+ t.setDaemon(self.daemon)
+ t.start()
+ except Exception, x:
+ logging.exception(x)
+
+ # Pump the socket for clients
+ self.serverTransport.listen()
+ while True:
+ try:
+ client = self.serverTransport.accept()
+ self.clients.put(client)
+ except Exception, x:
+ logging.exception(x)
+
+
+class TForkingServer(TServer):
+
+ """A Thrift server that forks a new process for each request"""
+ """
+ This is more scalable than the threaded server as it does not cause
+ GIL contention.
+
+ Note that this has different semantics from the threading server.
+ Specifically, updates to shared variables will no longer be shared.
+ It will also not work on windows.
+
+ This code is heavily inspired by SocketServer.ForkingMixIn in the
+ Python stdlib.
+ """
+
+ def __init__(self, *args):
+ TServer.__init__(self, *args)
+ self.children = []
+
+ def serve(self):
+ def try_close(file):
+ try:
+ file.close()
+ except IOError, e:
+ logging.warning(e, exc_info=True)
+
+
+ self.serverTransport.listen()
+ while True:
+ client = self.serverTransport.accept()
+ try:
+ pid = os.fork()
+
+ if pid: # parent
+ # add before collect, otherwise you race w/ waitpid
+ self.children.append(pid)
+ self.collect_children()
+
+ # Parent must close socket or the connection may not get
+ # closed promptly
+ itrans = self.inputTransportFactory.getTransport(client)
+ otrans = self.outputTransportFactory.getTransport(client)
+ try_close(itrans)
+ try_close(otrans)
+ else:
+ itrans = self.inputTransportFactory.getTransport(client)
+ otrans = self.outputTransportFactory.getTransport(client)
+
+ iprot = self.inputProtocolFactory.getProtocol(itrans)
+ oprot = self.outputProtocolFactory.getProtocol(otrans)
+
+ ecode = 0
+ try:
+ try:
+ while True:
+ self.processor.process(iprot, oprot)
+ except TTransport.TTransportException, tx:
+ pass
+ except Exception, e:
+ logging.exception(e)
+ ecode = 1
+ finally:
+ try_close(itrans)
+ try_close(otrans)
+
+ os._exit(ecode)
+
+ except TTransport.TTransportException, tx:
+ pass
+ except Exception, x:
+ logging.exception(x)
+
+
+ def collect_children(self):
+ while self.children:
+ try:
+ pid, status = os.waitpid(0, os.WNOHANG)
+ except os.error:
+ pid = None
+
+ if pid:
+ self.children.remove(pid)
+ else:
+ break
+
+
diff --git a/api/thrift/server/__init__.py b/api/thrift/server/__init__.py
new file mode 100644
index 0000000..1bf6e25
--- /dev/null
+++ b/api/thrift/server/__init__.py
@@ -0,0 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+__all__ = ['TServer', 'TNonblockingServer']
diff --git a/api/thrift/transport/THttpClient.py b/api/thrift/transport/THttpClient.py
new file mode 100644
index 0000000..5026978
--- /dev/null
+++ b/api/thrift/transport/THttpClient.py
@@ -0,0 +1,126 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+from TTransport import *
+from cStringIO import StringIO
+
+import urlparse
+import httplib
+import warnings
+import socket
+
+class THttpClient(TTransportBase):
+
+ """Http implementation of TTransport base."""
+
+ def __init__(self, uri_or_host, port=None, path=None):
+ """THttpClient supports two different types constructor parameters.
+
+ THttpClient(host, port, path) - deprecated
+ THttpClient(uri)
+
+ Only the second supports https."""
+
+ if port is not None:
+ warnings.warn("Please use the THttpClient('http://host:port/path') syntax", DeprecationWarning, stacklevel=2)
+ self.host = uri_or_host
+ self.port = port
+ assert path
+ self.path = path
+ self.scheme = 'http'
+ else:
+ parsed = urlparse.urlparse(uri_or_host)
+ self.scheme = parsed.scheme
+ assert self.scheme in ('http', 'https')
+ if self.scheme == 'http':
+ self.port = parsed.port or httplib.HTTP_PORT
+ elif self.scheme == 'https':
+ self.port = parsed.port or httplib.HTTPS_PORT
+ self.host = parsed.hostname
+ self.path = parsed.path
+ if parsed.query:
+ self.path += '?%s' % parsed.query
+ self.__wbuf = StringIO()
+ self.__http = None
+ self.__timeout = None
+
+ def open(self):
+ if self.scheme == 'http':
+ self.__http = httplib.HTTP(self.host, self.port)
+ else:
+ self.__http = httplib.HTTPS(self.host, self.port)
+
+ def close(self):
+ self.__http.close()
+ self.__http = None
+
+ def isOpen(self):
+ return self.__http != None
+
+ def setTimeout(self, ms):
+ if not hasattr(socket, 'getdefaulttimeout'):
+ raise NotImplementedError
+
+ if ms is None:
+ self.__timeout = None
+ else:
+ self.__timeout = ms/1000.0
+
+ def read(self, sz):
+ return self.__http.file.read(sz)
+
+ def write(self, buf):
+ self.__wbuf.write(buf)
+
+ def __withTimeout(f):
+ def _f(*args, **kwargs):
+ orig_timeout = socket.getdefaulttimeout()
+ socket.setdefaulttimeout(args[0].__timeout)
+ result = f(*args, **kwargs)
+ socket.setdefaulttimeout(orig_timeout)
+ return result
+ return _f
+
+ def flush(self):
+ if self.isOpen():
+ self.close()
+ self.open();
+
+ # Pull data out of buffer
+ data = self.__wbuf.getvalue()
+ self.__wbuf = StringIO()
+
+ # HTTP request
+ self.__http.putrequest('POST', self.path)
+
+ # Write headers
+ self.__http.putheader('Host', self.host)
+ self.__http.putheader('Content-Type', 'application/x-thrift')
+ self.__http.putheader('Content-Length', str(len(data)))
+ self.__http.endheaders()
+
+ # Write payload
+ self.__http.send(data)
+
+ # Get reply to flush the request
+ self.code, self.message, self.headers = self.__http.getreply()
+
+ # Decorate if we know how to timeout
+ if hasattr(socket, 'getdefaulttimeout'):
+ flush = __withTimeout(flush)
diff --git a/api/thrift/transport/TSocket.py b/api/thrift/transport/TSocket.py
new file mode 100644
index 0000000..d77e358
--- /dev/null
+++ b/api/thrift/transport/TSocket.py
@@ -0,0 +1,163 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+from TTransport import *
+import os
+import errno
+import socket
+import sys
+
+class TSocketBase(TTransportBase):
+ def _resolveAddr(self):
+ if self._unix_socket is not None:
+ return [(socket.AF_UNIX, socket.SOCK_STREAM, None, None, self._unix_socket)]
+ else:
+ return socket.getaddrinfo(self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE | socket.AI_ADDRCONFIG)
+
+ def close(self):
+ if self.handle:
+ self.handle.close()
+ self.handle = None
+
+class TSocket(TSocketBase):
+ """Socket implementation of TTransport base."""
+
+ def __init__(self, host='localhost', port=9090, unix_socket=None):
+ """Initialize a TSocket
+
+ @param host(str) The host to connect to.
+ @param port(int) The (TCP) port to connect to.
+ @param unix_socket(str) The filename of a unix socket to connect to.
+ (host and port will be ignored.)
+ """
+
+ self.host = host
+ self.port = port
+ self.handle = None
+ self._unix_socket = unix_socket
+ self._timeout = None
+
+ def setHandle(self, h):
+ self.handle = h
+
+ def isOpen(self):
+ return self.handle != None
+
+ def setTimeout(self, ms):
+ if ms is None:
+ self._timeout = None
+ else:
+ self._timeout = ms/1000.0
+
+ if (self.handle != None):
+ self.handle.settimeout(self._timeout)
+
+ def open(self):
+ try:
+ res0 = self._resolveAddr()
+ for res in res0:
+ self.handle = socket.socket(res[0], res[1])
+ self.handle.settimeout(self._timeout)
+ try:
+ self.handle.connect(res[4])
+ except socket.error, e:
+ if res is not res0[-1]:
+ continue
+ else:
+ raise e
+ break
+ except socket.error, e:
+ if self._unix_socket:
+ message = 'Could not connect to socket %s' % self._unix_socket
+ else:
+ message = 'Could not connect to %s:%d' % (self.host, self.port)
+ raise TTransportException(type=TTransportException.NOT_OPEN, message=message)
+
+ def read(self, sz):
+ try:
+ buff = self.handle.recv(sz)
+ except socket.error, e:
+ if (e.args[0] == errno.ECONNRESET and
+ (sys.platform == 'darwin' or sys.platform.startswith('freebsd'))):
+ # freebsd and Mach don't follow POSIX semantic of recv
+ # and fail with ECONNRESET if peer performed shutdown.
+ # See corresponding comment and code in TSocket::read()
+ # in lib/cpp/src/transport/TSocket.cpp.
+ self.close()
+ # Trigger the check to raise the END_OF_FILE exception below.
+ buff = ''
+ else:
+ raise
+ if len(buff) == 0:
+ raise TTransportException(type=TTransportException.END_OF_FILE, message='TSocket read 0 bytes')
+ return buff
+
+ def write(self, buff):
+ if not self.handle:
+ raise TTransportException(type=TTransportException.NOT_OPEN, message='Transport not open')
+ sent = 0
+ have = len(buff)
+ while sent < have:
+ plus = self.handle.send(buff)
+ if plus == 0:
+ raise TTransportException(type=TTransportException.END_OF_FILE, message='TSocket sent 0 bytes')
+ sent += plus
+ buff = buff[plus:]
+
+ def flush(self):
+ pass
+
+class TServerSocket(TSocketBase, TServerTransportBase):
+ """Socket implementation of TServerTransport base."""
+
+ def __init__(self, port=9090, unix_socket=None):
+ self.host = None
+ self.port = port
+ self._unix_socket = unix_socket
+ self.handle = None
+
+ def listen(self):
+ res0 = self._resolveAddr()
+ for res in res0:
+ if res[0] is socket.AF_INET6 or res is res0[-1]:
+ break
+
+ # We need remove the old unix socket if the file exists and
+ # nobody is listening on it.
+ if self._unix_socket:
+ tmp = socket.socket(res[0], res[1])
+ try:
+ tmp.connect(res[4])
+ except socket.error, err:
+ eno, message = err.args
+ if eno == errno.ECONNREFUSED:
+ os.unlink(res[4])
+
+ self.handle = socket.socket(res[0], res[1])
+ self.handle.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ if hasattr(self.handle, 'set_timeout'):
+ self.handle.set_timeout(None)
+ self.handle.bind(res[4])
+ self.handle.listen(128)
+
+ def accept(self):
+ client, addr = self.handle.accept()
+ result = TSocket()
+ result.setHandle(client)
+ return result
diff --git a/api/thrift/transport/TTransport.py b/api/thrift/transport/TTransport.py
new file mode 100644
index 0000000..12e51a9
--- /dev/null
+++ b/api/thrift/transport/TTransport.py
@@ -0,0 +1,331 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+from cStringIO import StringIO
+from struct import pack,unpack
+from thrift.Thrift import TException
+
+class TTransportException(TException):
+
+ """Custom Transport Exception class"""
+
+ UNKNOWN = 0
+ NOT_OPEN = 1
+ ALREADY_OPEN = 2
+ TIMED_OUT = 3
+ END_OF_FILE = 4
+
+ def __init__(self, type=UNKNOWN, message=None):
+ TException.__init__(self, message)
+ self.type = type
+
+class TTransportBase:
+
+ """Base class for Thrift transport layer."""
+
+ def isOpen(self):
+ pass
+
+ def open(self):
+ pass
+
+ def close(self):
+ pass
+
+ def read(self, sz):
+ pass
+
+ def readAll(self, sz):
+ buff = ''
+ have = 0
+ while (have < sz):
+ chunk = self.read(sz-have)
+ have += len(chunk)
+ buff += chunk
+
+ if len(chunk) == 0:
+ raise EOFError()
+
+ return buff
+
+ def write(self, buf):
+ pass
+
+ def flush(self):
+ pass
+
+# This class should be thought of as an interface.
+class CReadableTransport:
+ """base class for transports that are readable from C"""
+
+ # TODO(dreiss): Think about changing this interface to allow us to use
+ # a (Python, not c) StringIO instead, because it allows
+ # you to write after reading.
+
+ # NOTE: This is a classic class, so properties will NOT work
+ # correctly for setting.
+ @property
+ def cstringio_buf(self):
+ """A cStringIO buffer that contains the current chunk we are reading."""
+ pass
+
+ def cstringio_refill(self, partialread, reqlen):
+ """Refills cstringio_buf.
+
+ Returns the currently used buffer (which can but need not be the same as
+ the old cstringio_buf). partialread is what the C code has read from the
+ buffer, and should be inserted into the buffer before any more reads. The
+ return value must be a new, not borrowed reference. Something along the
+ lines of self._buf should be fine.
+
+ If reqlen bytes can't be read, throw EOFError.
+ """
+ pass
+
+class TServerTransportBase:
+
+ """Base class for Thrift server transports."""
+
+ def listen(self):
+ pass
+
+ def accept(self):
+ pass
+
+ def close(self):
+ pass
+
+class TTransportFactoryBase:
+
+ """Base class for a Transport Factory"""
+
+ def getTransport(self, trans):
+ return trans
+
+class TBufferedTransportFactory:
+
+ """Factory transport that builds buffered transports"""
+
+ def getTransport(self, trans):
+ buffered = TBufferedTransport(trans)
+ return buffered
+
+
+class TBufferedTransport(TTransportBase,CReadableTransport):
+
+ """Class that wraps another transport and buffers its I/O.
+
+ The implementation uses a (configurable) fixed-size read buffer
+ but buffers all writes until a flush is performed.
+ """
+
+ DEFAULT_BUFFER = 4096
+
+ def __init__(self, trans, rbuf_size = DEFAULT_BUFFER):
+ self.__trans = trans
+ self.__wbuf = StringIO()
+ self.__rbuf = StringIO("")
+ self.__rbuf_size = rbuf_size
+
+ def isOpen(self):
+ return self.__trans.isOpen()
+
+ def open(self):
+ return self.__trans.open()
+
+ def close(self):
+ return self.__trans.close()
+
+ def read(self, sz):
+ ret = self.__rbuf.read(sz)
+ if len(ret) != 0:
+ return ret
+
+ self.__rbuf = StringIO(self.__trans.read(max(sz, self.__rbuf_size)))
+ return self.__rbuf.read(sz)
+
+ def write(self, buf):
+ self.__wbuf.write(buf)
+
+ def flush(self):
+ out = self.__wbuf.getvalue()
+ # reset wbuf before write/flush to preserve state on underlying failure
+ self.__wbuf = StringIO()
+ self.__trans.write(out)
+ self.__trans.flush()
+
+ # Implement the CReadableTransport interface.
+ @property
+ def cstringio_buf(self):
+ return self.__rbuf
+
+ def cstringio_refill(self, partialread, reqlen):
+ retstring = partialread
+ if reqlen < self.__rbuf_size:
+ # try to make a read of as much as we can.
+ retstring += self.__trans.read(self.__rbuf_size)
+
+ # but make sure we do read reqlen bytes.
+ if len(retstring) < reqlen:
+ retstring += self.__trans.readAll(reqlen - len(retstring))
+
+ self.__rbuf = StringIO(retstring)
+ return self.__rbuf
+
+class TMemoryBuffer(TTransportBase, CReadableTransport):
+ """Wraps a cStringIO object as a TTransport.
+
+ NOTE: Unlike the C++ version of this class, you cannot write to it
+ then immediately read from it. If you want to read from a
+ TMemoryBuffer, you must either pass a string to the constructor.
+ TODO(dreiss): Make this work like the C++ version.
+ """
+
+ def __init__(self, value=None):
+ """value -- a value to read from for stringio
+
+ If value is set, this will be a transport for reading,
+ otherwise, it is for writing"""
+ if value is not None:
+ self._buffer = StringIO(value)
+ else:
+ self._buffer = StringIO()
+
+ def isOpen(self):
+ return not self._buffer.closed
+
+ def open(self):
+ pass
+
+ def close(self):
+ self._buffer.close()
+
+ def read(self, sz):
+ return self._buffer.read(sz)
+
+ def write(self, buf):
+ self._buffer.write(buf)
+
+ def flush(self):
+ pass
+
+ def getvalue(self):
+ return self._buffer.getvalue()
+
+ # Implement the CReadableTransport interface.
+ @property
+ def cstringio_buf(self):
+ return self._buffer
+
+ def cstringio_refill(self, partialread, reqlen):
+ # only one shot at reading...
+ raise EOFError()
+
+class TFramedTransportFactory:
+
+ """Factory transport that builds framed transports"""
+
+ def getTransport(self, trans):
+ framed = TFramedTransport(trans)
+ return framed
+
+
+class TFramedTransport(TTransportBase, CReadableTransport):
+
+ """Class that wraps another transport and frames its I/O when writing."""
+
+ def __init__(self, trans,):
+ self.__trans = trans
+ self.__rbuf = StringIO()
+ self.__wbuf = StringIO()
+
+ def isOpen(self):
+ return self.__trans.isOpen()
+
+ def open(self):
+ return self.__trans.open()
+
+ def close(self):
+ return self.__trans.close()
+
+ def read(self, sz):
+ ret = self.__rbuf.read(sz)
+ if len(ret) != 0:
+ return ret
+
+ self.readFrame()
+ return self.__rbuf.read(sz)
+
+ def readFrame(self):
+ buff = self.__trans.readAll(4)
+ sz, = unpack('!i', buff)
+ self.__rbuf = StringIO(self.__trans.readAll(sz))
+
+ def write(self, buf):
+ self.__wbuf.write(buf)
+
+ def flush(self):
+ wout = self.__wbuf.getvalue()
+ wsz = len(wout)
+ # reset wbuf before write/flush to preserve state on underlying failure
+ self.__wbuf = StringIO()
+ # N.B.: Doing this string concatenation is WAY cheaper than making
+ # two separate calls to the underlying socket object. Socket writes in
+ # Python turn out to be REALLY expensive, but it seems to do a pretty
+ # good job of managing string buffer operations without excessive copies
+ buf = pack("!i", wsz) + wout
+ self.__trans.write(buf)
+ self.__trans.flush()
+
+ # Implement the CReadableTransport interface.
+ @property
+ def cstringio_buf(self):
+ return self.__rbuf
+
+ def cstringio_refill(self, prefix, reqlen):
+ # self.__rbuf will already be empty here because fastbinary doesn't
+ # ask for a refill until the previous buffer is empty. Therefore,
+ # we can start reading new frames immediately.
+ while len(prefix) < reqlen:
+ self.readFrame()
+ prefix += self.__rbuf.getvalue()
+ self.__rbuf = StringIO(prefix)
+ return self.__rbuf
+
+
+class TFileObjectTransport(TTransportBase):
+ """Wraps a file-like object to make it work as a Thrift transport."""
+
+ def __init__(self, fileobj):
+ self.fileobj = fileobj
+
+ def isOpen(self):
+ return True
+
+ def close(self):
+ self.fileobj.close()
+
+ def read(self, sz):
+ return self.fileobj.read(sz)
+
+ def write(self, buf):
+ self.fileobj.write(buf)
+
+ def flush(self):
+ self.fileobj.flush()
diff --git a/api/thrift/transport/TTwisted.py b/api/thrift/transport/TTwisted.py
new file mode 100644
index 0000000..b6dcb4e
--- /dev/null
+++ b/api/thrift/transport/TTwisted.py
@@ -0,0 +1,219 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+from zope.interface import implements, Interface, Attribute
+from twisted.internet.protocol import Protocol, ServerFactory, ClientFactory, \
+ connectionDone
+from twisted.internet import defer
+from twisted.protocols import basic
+from twisted.python import log
+from twisted.web import server, resource, http
+
+from thrift.transport import TTransport
+from cStringIO import StringIO
+
+
+class TMessageSenderTransport(TTransport.TTransportBase):
+
+ def __init__(self):
+ self.__wbuf = StringIO()
+
+ def write(self, buf):
+ self.__wbuf.write(buf)
+
+ def flush(self):
+ msg = self.__wbuf.getvalue()
+ self.__wbuf = StringIO()
+ self.sendMessage(msg)
+
+ def sendMessage(self, message):
+ raise NotImplementedError
+
+
+class TCallbackTransport(TMessageSenderTransport):
+
+ def __init__(self, func):
+ TMessageSenderTransport.__init__(self)
+ self.func = func
+
+ def sendMessage(self, message):
+ self.func(message)
+
+
+class ThriftClientProtocol(basic.Int32StringReceiver):
+
+ MAX_LENGTH = 2 ** 31 - 1
+
+ def __init__(self, client_class, iprot_factory, oprot_factory=None):
+ self._client_class = client_class
+ self._iprot_factory = iprot_factory
+ if oprot_factory is None:
+ self._oprot_factory = iprot_factory
+ else:
+ self._oprot_factory = oprot_factory
+
+ self.recv_map = {}
+ self.started = defer.Deferred()
+
+ def dispatch(self, msg):
+ self.sendString(msg)
+
+ def connectionMade(self):
+ tmo = TCallbackTransport(self.dispatch)
+ self.client = self._client_class(tmo, self._oprot_factory)
+ self.started.callback(self.client)
+
+ def connectionLost(self, reason=connectionDone):
+ for k,v in self.client._reqs.iteritems():
+ tex = TTransport.TTransportException(
+ type=TTransport.TTransportException.END_OF_FILE,
+ message='Connection closed')
+ v.errback(tex)
+
+ def stringReceived(self, frame):
+ tr = TTransport.TMemoryBuffer(frame)
+ iprot = self._iprot_factory.getProtocol(tr)
+ (fname, mtype, rseqid) = iprot.readMessageBegin()
+
+ try:
+ method = self.recv_map[fname]
+ except KeyError:
+ method = getattr(self.client, 'recv_' + fname)
+ self.recv_map[fname] = method
+
+ method(iprot, mtype, rseqid)
+
+
+class ThriftServerProtocol(basic.Int32StringReceiver):
+
+ MAX_LENGTH = 2 ** 31 - 1
+
+ def dispatch(self, msg):
+ self.sendString(msg)
+
+ def processError(self, error):
+ self.transport.loseConnection()
+
+ def processOk(self, _, tmo):
+ msg = tmo.getvalue()
+
+ if len(msg) > 0:
+ self.dispatch(msg)
+
+ def stringReceived(self, frame):
+ tmi = TTransport.TMemoryBuffer(frame)
+ tmo = TTransport.TMemoryBuffer()
+
+ iprot = self.factory.iprot_factory.getProtocol(tmi)
+ oprot = self.factory.oprot_factory.getProtocol(tmo)
+
+ d = self.factory.processor.process(iprot, oprot)
+ d.addCallbacks(self.processOk, self.processError,
+ callbackArgs=(tmo,))
+
+
+class IThriftServerFactory(Interface):
+
+ processor = Attribute("Thrift processor")
+
+ iprot_factory = Attribute("Input protocol factory")
+
+ oprot_factory = Attribute("Output protocol factory")
+
+
+class IThriftClientFactory(Interface):
+
+ client_class = Attribute("Thrift client class")
+
+ iprot_factory = Attribute("Input protocol factory")
+
+ oprot_factory = Attribute("Output protocol factory")
+
+
+class ThriftServerFactory(ServerFactory):
+
+ implements(IThriftServerFactory)
+
+ protocol = ThriftServerProtocol
+
+ def __init__(self, processor, iprot_factory, oprot_factory=None):
+ self.processor = processor
+ self.iprot_factory = iprot_factory
+ if oprot_factory is None:
+ self.oprot_factory = iprot_factory
+ else:
+ self.oprot_factory = oprot_factory
+
+
+class ThriftClientFactory(ClientFactory):
+
+ implements(IThriftClientFactory)
+
+ protocol = ThriftClientProtocol
+
+ def __init__(self, client_class, iprot_factory, oprot_factory=None):
+ self.client_class = client_class
+ self.iprot_factory = iprot_factory
+ if oprot_factory is None:
+ self.oprot_factory = iprot_factory
+ else:
+ self.oprot_factory = oprot_factory
+
+ def buildProtocol(self, addr):
+ p = self.protocol(self.client_class, self.iprot_factory,
+ self.oprot_factory)
+ p.factory = self
+ return p
+
+
+class ThriftResource(resource.Resource):
+
+ allowedMethods = ('POST',)
+
+ def __init__(self, processor, inputProtocolFactory,
+ outputProtocolFactory=None):
+ resource.Resource.__init__(self)
+ self.inputProtocolFactory = inputProtocolFactory
+ if outputProtocolFactory is None:
+ self.outputProtocolFactory = inputProtocolFactory
+ else:
+ self.outputProtocolFactory = outputProtocolFactory
+ self.processor = processor
+
+ def getChild(self, path, request):
+ return self
+
+ def _cbProcess(self, _, request, tmo):
+ msg = tmo.getvalue()
+ request.setResponseCode(http.OK)
+ request.setHeader("content-type", "application/x-thrift")
+ request.write(msg)
+ request.finish()
+
+ def render_POST(self, request):
+ request.content.seek(0, 0)
+ data = request.content.read()
+ tmi = TTransport.TMemoryBuffer(data)
+ tmo = TTransport.TMemoryBuffer()
+
+ iprot = self.inputProtocolFactory.getProtocol(tmi)
+ oprot = self.outputProtocolFactory.getProtocol(tmo)
+
+ d = self.processor.process(iprot, oprot)
+ d.addCallback(self._cbProcess, request, tmo)
+ return server.NOT_DONE_YET
diff --git a/api/thrift/transport/__init__.py b/api/thrift/transport/__init__.py
new file mode 100644
index 0000000..02c6048
--- /dev/null
+++ b/api/thrift/transport/__init__.py
@@ -0,0 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+__all__ = ['TTransport', 'TSocket', 'THttpClient']
diff --git a/api/urls.py b/api/urls.py
new file mode 100644
index 0000000..3760419
--- /dev/null
+++ b/api/urls.py
@@ -0,0 +1,28 @@
+from django.conf.urls.defaults import * #@UnusedWildImport
+
+# Uncomment the next two lines to enable the admin:
+from django.contrib import admin
+
+from api import restapi
+
+admin.autodiscover()
+
+VERSION = r'^v(?P\d+)'
+INDEX = r'/indexes/(?P[^/]+)'
+FUNCTION = r'/functions/(?P-?\d+)'
+
+urlpatterns = patterns('',
+ url('^/?$', 'api.restapi.default', name='default'),
+ url(VERSION + '/?$', restapi.Version.as_view()),
+ url(VERSION + '/indexes/?$', restapi.Indexes.as_view()),
+ url(VERSION + INDEX + '/?$', restapi.Index.as_view()),
+ url(VERSION + INDEX + '/docs/?$', restapi.Document.as_view()),
+ url(VERSION + INDEX + '/docs/variables/?$', restapi.Variables.as_view()),
+ url(VERSION + INDEX + '/docs/categories/?$', restapi.Categories.as_view()),
+ url(VERSION + INDEX + '/functions/?$', restapi.Functions.as_view()),
+ url(VERSION + INDEX + FUNCTION + '/?$', restapi.Function.as_view()),
+ url(VERSION + INDEX + '/search/?$', restapi.Search.as_view()),
+ url(VERSION + INDEX + '/promote/?$', restapi.Promote.as_view()),
+ url(VERSION + INDEX + '/autocomplete/?$', restapi.AutoComplete.as_view()),
+ url(VERSION + INDEX + '/instantlinks/?$', restapi.InstantLinks.as_view()),
+)
diff --git a/api/wsgi.py b/api/wsgi.py
new file mode 100644
index 0000000..6fb3570
--- /dev/null
+++ b/api/wsgi.py
@@ -0,0 +1,8 @@
+import os
+import sys
+
+sys.path.append(os.path.abspath(os.path.dirname(__file__)))
+os.environ['DJANGO_SETTINGS_MODULE'] = 'api.settings'
+
+import django.core.handlers.wsgi
+application = django.core.handlers.wsgi.WSGIHandler()
diff --git a/backoffice/.project b/backoffice/.project
new file mode 100644
index 0000000..a0d8ab7
--- /dev/null
+++ b/backoffice/.project
@@ -0,0 +1,18 @@
+
+
+ backoffice
+
+
+
+
+
+ org.python.pydev.PyDevBuilder
+
+
+
+
+
+ org.python.pydev.pythonNature
+ org.python.pydev.django.djangoNature
+
+
diff --git a/backoffice/.pydevproject b/backoffice/.pydevproject
new file mode 100644
index 0000000..1a746e5
--- /dev/null
+++ b/backoffice/.pydevproject
@@ -0,0 +1,14 @@
+
+
+
+
+Default
+python 2.6
+
+DJANGO_MANAGE_LOCATION
+backoffice/manage.py
+
+
+/backoffice
+
+
diff --git a/backoffice/__init__.py b/backoffice/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backoffice/amazon_credential.py b/backoffice/amazon_credential.py
new file mode 100644
index 0000000..c2910f5
--- /dev/null
+++ b/backoffice/amazon_credential.py
@@ -0,0 +1,2 @@
+AMAZON_USER = ""
+AMAZON_PASSWORD = ""
diff --git a/backoffice/api_linked_models.py b/backoffice/api_linked_models.py
new file mode 100644
index 0000000..0908f60
--- /dev/null
+++ b/backoffice/api_linked_models.py
@@ -0,0 +1,938 @@
+import hashlib
+import random
+import binascii
+
+from lib.indextank.client import ApiClient, IndexAlreadyExists
+from lib.authorizenet import AuthorizeNet, BillingException
+
+from django.db import models
+from django.contrib.auth.models import User
+from django.utils import simplejson as json
+from django.db import IntegrityError
+from django.db.models.aggregates import Sum, Count
+
+from lib import encoder, flaptor_logging
+
+from django.conf import settings
+from datetime import datetime
+
+logger = flaptor_logging.get_logger('Models')
+
+# idea taken from https://www.grc.com/passwords.htm
+def generate_apikey(id):
+ key = "2A1A8AE7CAEFAC47D6F74920CE4B0CE46430CDA6CF03D254C1C29402D727E570"
+ while True:
+ hash = hashlib.md5()
+ hash.update('%d' % id)
+ hash.update(key)
+ hash.update('%d' % random.randint(0,1000000))
+ random_part = binascii.b2a_base64(hash.digest())[:14]
+ if not '/' in random_part:
+ break
+
+ unique_part = encoder.to_key(id)
+
+ return unique_part + '-' + random_part
+
+def generate_onetimepass(id):
+ key = "CAEFAC47D6F7D727E57024920CE4B0CE46430CDA6CF03D254C1C29402A1A8AE7"
+ while True:
+ hash = hashlib.md5()
+ hash.update('%d' % id)
+ hash.update(key)
+ hash.update('%d' % random.randint(0,1000000))
+ random_part = binascii.b2a_base64(hash.digest())[:5]
+ if not '/' in random_part:
+ break
+
+ unique_part = encoder.to_key(id)
+
+ return unique_part + random_part
+
+def generate_forgotpass(id):
+ key = "E57024920CE4B0CE4643CAEFAC47D6F7D7270CDA6CF03D254C1C29402A1A8AE7"
+ while True:
+ hash = hashlib.md5()
+ hash.update('%d' % id)
+ hash.update(key)
+ hash.update('%d' % random.randint(0,1000000))
+ random_part = binascii.b2a_base64(hash.digest())[:6]
+ if not '/' in random_part:
+ break
+
+ unique_part = encoder.to_key(id)
+
+ return random_part + unique_part
+
+
+# StoreFront models
+class Account(models.Model):
+ apikey = models.CharField(max_length=22, unique=True)
+ creation_time = models.DateTimeField()
+ package = models.ForeignKey('Package', null=True)
+ status = models.CharField(max_length=30, null=False)
+ provisioner = models.ForeignKey('Provisioner', null=True)
+
+ configuration = models.ForeignKey('IndexConfiguration', null=True)
+ default_analyzer = models.ForeignKey('Analyzer', null=True, related_name="accounts")
+
+ class Statuses:
+ operational = 'OPERATIONAL'
+ creating = 'CREATING'
+ closed = 'CLOSED'
+
+ def __repr__(self):
+ return 'Account (%s):\n\tuser_email: %s\n\tapikey: %s\n\tcreation_time: %s\n\tstatus: %s\n\tpackage: %s\n\tconfiguration: %s\n' % (self.id, PFUser.objects.filter(account=self)[0].email, str(self.apikey), str(self.creation_time), str(self.status), self.package.name, self.configuration.description)
+
+ def __str__(self):
+ return '(apikey: %s; creation_time: %s; status: %s)' % (str(self.apikey), str(self.creation_time), str(self.status))
+
+ def count_indexes(self):
+ return self.indexes.aggregate(cnt=Count('id'))['cnt']
+
+ def count_documents(self):
+ return self.indexes.aggregate(cnt=Sum('current_docs_number'))['cnt']
+
+ def is_operational(self):
+ return self.status == Account.Statuses.operational
+
+ def is_heroku(self):
+ # HACK UNTIL HEROKU IS A PROVISIONER
+ return self.package.code.startswith('HEROKU_')
+ #return self.provisioner and self.provisioner.name == 'heroku'
+
+ @classmethod
+ def create_account(cls, dt, email=None, password=None):
+ account = Account()
+
+ account.creation_time = datetime.now()
+ account.status = Account.Statuses.creating
+ account.save()
+
+ account.apikey = generate_apikey(account.id)
+ account.save()
+
+ unique_part, random_part = account.apikey.split('-', 1)
+ if email is None:
+ email = '%s@indextank.com' % unique_part
+
+ if password is None:
+ password = random_part
+
+ try:
+ user = User.objects.create_user(email, '', password)
+ except IntegrityError, e:
+ account.delete()
+ raise e
+
+ try:
+ pfu = PFUser()
+ pfu.account = account
+ pfu.user = user
+ pfu.email = email
+
+ pfu.save()
+ except IntegrityError, e:
+ account.delete()
+ user.delete()
+ raise e
+
+ return account, pfu
+
+ def create_index(self, index_name, public_search=None):
+ index = Index()
+
+ # basic index data
+ index.populate_for_account(self)
+ index.name = index_name
+ index.creation_time = datetime.now()
+ index.language_code = 'en'
+ index.status = Index.States.new
+ if not public_search is None:
+ index.public_api = public_search
+
+ # test for name uniqueness
+ # raises IntegrityError if the index name already exists
+ index.save()
+
+ # define the default function
+ function = ScoreFunction()
+ function.index = index
+ function.name = '0'
+ function.definition = '-age'
+ function.save()
+
+ # deduce code from id
+ index.code = encoder.to_key(index.id)
+ index.save()
+
+ return index
+
+ def create_demo_index(self):
+ try:
+ dataset = DataSet.objects.get(code='DEMO')
+ except DataSet.DoesNotExist:
+ logger.exception('DemoIndex dataset not present in database. Aborting demo index creation')
+ return
+
+ index = self.create_index('DemoIndex')
+
+ index.public_api = True
+ index.save()
+
+ population = IndexPopulation()
+ population.index = index
+ population.status = IndexPopulation.Statuses.created
+ population.dataset = dataset
+ population.time = datetime.now()
+ population.populated_size = 0
+
+ population.save()
+
+ def close(self):
+ # Dropping an account implies:
+
+ # - removing the payment information from the account
+ # - removing the subscriptions from authorize.net
+ for info in self.payment_informations.all():
+ auth = AuthorizeNet()
+ for subscription in info.subscriptions.all():
+ auth.subscription_cancel(subscription.reference_id, subscription.subscription_id)
+ subscription.delete()
+ info.delete()
+
+
+ # - changing the status to CLOSED
+ self.status = Account.Statuses.closed
+
+ # - removing and stopping the indexes for the account
+ for index in self.indexes.all():
+ self.drop_index(index)
+
+ # - notify
+ # send_notification(//close account)
+
+ # - FIXME: handle authorize net errors!
+
+
+ self.save()
+
+ def drop_index(self, index):
+ client = ApiClient(self.get_private_apiurl())
+ client.delete_index(index.name)
+
+ def apply_package(self, package):
+ self.package = package
+
+ self.configuration = package.configuration
+
+ def update_apikey(self):
+ self.apikey = generate_apikey(self.id)
+
+ def get_private_apikey(self):
+ return self.apikey.split('-', 1)[1]
+
+ def get_public_apikey(self):
+ return self.apikey.split('-', 1)[0]
+
+ def get_private_apiurl(self):
+ return 'http://:%s@%s.api.indextank.com' % (self.get_private_apikey(), self.get_public_apikey())
+
+ def get_public_apiurl(self):
+ return 'http://%s.api.indextank.com' % self.get_public_apikey()
+
+ class Meta:
+ db_table = 'storefront_account'
+
+class AccountPayingInformation(models.Model):
+ account = models.ForeignKey('Account', related_name='payment_informations')
+
+ first_name = models.CharField(max_length=50, null=True)
+ last_name = models.CharField(max_length=50, null=True)
+ address = models.CharField(max_length=60, null=True)
+ city = models.CharField(max_length=60, null=True)
+ state = models.CharField(max_length=2, null=True)
+ zip_code = models.CharField(max_length=60, null=True)
+ country = models.CharField(max_length=2, null=True)
+
+ company = models.CharField(max_length=50, null=True)
+
+ credit_card_last_digits = models.CharField(max_length=4, null=True)
+ contact_email = models.EmailField(max_length=255, null=True)
+
+ #custom subscription
+ monthly_amount = models.DecimalField(max_digits=8, decimal_places=2, null=True)
+ subscription_status = models.CharField(max_length=30, null=True)
+ subscription_type = models.CharField(max_length=30, null=True)
+
+
+ class Meta:
+ db_table = 'storefront_accountpayinginformation'
+
+
+class PaymentSubscription(models.Model):
+ account = models.ForeignKey('AccountPayingInformation', related_name='subscriptions')
+
+ # authorizenet id
+ subscription_id = models.CharField(max_length=20, null=False, blank=False)
+ # indextank id
+ reference_id = models.CharField(max_length=13, null=False, blank=False)
+
+ amount = models.DecimalField(max_digits=8, decimal_places=2, null=False)
+
+ # Frequency
+ start_date = models.DateTimeField()
+ frequency_length = models.IntegerField(null=False)
+ frequency_unit = models.CharField(max_length=10, null=False, blank=False)
+
+ class Meta:
+ db_table = 'storefront_paymentsubscription'
+
+
+class EffectivePayment(models.Model):
+ account = models.ForeignKey('AccountPayingInformation', related_name='payments')
+
+ transaction_date = models.DateTimeField()
+
+ # authorizenet data
+ transaction_id = models.CharField(max_length=12, null=False, blank=False)
+ customer_id = models.CharField(max_length=8, null=False, blank=False)
+ transaction_message = models.CharField(max_length=300, null=True)
+ subscription_id = models.CharField(max_length=20, null=False, blank=False)
+ subscription_payment_number = models.IntegerField(null=False)
+ first_name = models.CharField(max_length=50, null=False, blank=False)
+ last_name = models.CharField(max_length=50, null=False, blank=False)
+ address = models.CharField(max_length=60, null=True)
+ city = models.CharField(max_length=60, null=True)
+ state = models.CharField(max_length=2, null=True)
+ zip_code = models.CharField(max_length=60, null=True)
+ country = models.CharField(max_length=2, null=True)
+ company = models.CharField(max_length=50, null=True)
+
+ # Inherited data (from account information
+ credit_card_last_digits = models.CharField(max_length=4, null=False, blank=False)
+ contact_email = models.EmailField(max_length=255)
+
+ amount = models.DecimalField(max_digits=8, decimal_places=2, null=False)
+
+ class Meta:
+ db_table = 'storefront_effectivepayment'
+
+class DataSet(models.Model):
+ name = models.CharField(null=True, max_length=50, unique=True)
+ code = models.CharField(null=True, max_length=15, unique=True)
+ filename = models.CharField(null=True, max_length=100, unique=True)
+ size = models.IntegerField(default=0)
+
+ class Meta:
+ db_table = 'storefront_dataset'
+
+class IndexPopulation(models.Model):
+ index = models.ForeignKey('Index', related_name='datasets')
+ dataset = models.ForeignKey('DataSet', related_name='indexes')
+ time = models.DateTimeField()
+ populated_size = models.IntegerField(default=0)
+
+ status = models.CharField(max_length=50,null=True)
+
+ class Statuses:
+ created = 'CREATED'
+ populating = 'POPULATING'
+ finished = 'FINISHED'
+
+ class Meta:
+ db_table = 'storefront_indexpopulation'
+
+
+class Index(models.Model):
+ account = models.ForeignKey('Account', related_name='indexes')
+ code = models.CharField(null=True, max_length=22, unique=True)
+ name = models.CharField(max_length=50)
+ language_code = models.CharField(max_length=2)
+ creation_time = models.DateTimeField()
+
+ analyzer_config = models.TextField(null=True)
+ configuration = models.ForeignKey('IndexConfiguration', null=True)
+ public_api = models.BooleanField(default=False, null=False)
+
+ status = models.CharField(max_length=50)
+
+ deleted = models.BooleanField(default=False, null=False)
+
+ class States:
+ new = 'NEW'
+ live = 'LIVE'
+ hibernate_requested = 'HIBERNATE_REQUESTED'
+ hibernated = 'HIBERNATED'
+ waking_up = 'WAKING_UP'
+
+ def get_json_for_analyzer(self):
+ if self.analyzer_config is None:
+ return None
+ configuration = json.loads(self.analyzer_config)
+ final_configuration = {}
+
+ if configuration.has_key('per_field'):
+ per_field_final = {}
+ per_field = configuration.get('per_field')
+ for field in per_field.keys():
+ per_field_final[field] = Index.get_analyzer(per_field[field])
+ final_configuration['perField'] = per_field_final
+ final_configuration['default'] = Index.get_analyzer(per_field.get('default'))
+ else:
+ final_configuration = Index.get_analyzer(configuration)
+
+ return final_configuration
+
+ @classmethod
+ def get_analyzer(cls, configuration):
+ analyzer_map = {}
+ code = configuration.get('code')
+ if code is None:
+ raise ValueError('Analyzer configuration has no "code" key')
+
+ try:
+ analyzer = AnalyzerComponent.objects.get(code=code)
+ except AnalyzerComponent.DoesNotExist:
+ raise ValueError('Analyzer configuration "code" key doesn\'t match any analyzers')
+
+ analyzer_map['factory'] = analyzer.factory
+ analyzer_map['configuration'] = json.loads(analyzer.config)
+
+ if configuration.has_key('filters'):
+ filters_list = []
+ for filter in configuration.get('filters'):
+ filters_list.append(Index.get_analyzer(filter))
+ analyzer_map['configuration']['filters'] = filters_list
+
+ return analyzer_map
+
+# allows_adds = models.BooleanField(null=False,default=True)
+# allows_queries = models.BooleanField(null=False,default=True)
+
+ # index creation data
+# allows_snippets = models.BooleanField()
+#
+# allows_autocomplete = models.BooleanField(default=True)
+# autocomplete_type = models.models.CharField(max_length=10, null=True) # NEW
+#
+# allows_faceting = models.BooleanField()
+# facets_bits = models.IntegerField(null=True) # NEW
+#
+# max_variables = models.IntegerField(null=False) # NEW
+#
+# max_memory_mb = models.IntegerField(null=False) # NEW
+# rti_documents_number = models.IntegerField(null=False) # NEW
+
+ # statistics
+ current_size = models.FloatField(default=0)
+ current_docs_number = models.IntegerField(default=0)
+ queries_per_day = models.FloatField(default=0)
+
+ #demo
+ base_port = models.IntegerField(null=True)
+
+ def __repr__(self):
+ return 'Index (%s):\n\tname: %s\n\tcode: %s\n\tcreation_time: %s\n\tconfiguration: %s\n\taccount\'s package: %s\ncurrent deploys: %r' % (self.id, self.name, self.code, self.creation_time, self.configuration.description, self.account.package.name, self.deploys.all())
+
+ def is_populating(self):
+ for population in self.datasets.all():
+ if not population.status == IndexPopulation.Statuses.finished:
+ return True
+ return False
+
+ def is_demo(self):
+ return self.name == 'DemoIndex' and self.datasets.count() > 0
+
+
+ def is_ready(self):
+ '''
+ Returns True if the end-user can use the index.
+ (this means for read and write, and it's meant to
+ be shown in the storefront page). Internally, this
+ means that at least one deployment for this index
+ is readable, and at least one is writable.
+ '''
+ return self.is_writable() and self.is_readable()
+
+ def is_hibernated(self):
+ return self.status in (Index.States.hibernated, Index.States.waking_up)
+
+ def is_writable(self):
+ '''
+ Returns true if there's at least one index that can be written.
+ '''
+ for deploy in self.deploys.all():
+ if deploy.is_writable():
+ return True
+
+ def is_readable(self):
+ '''
+ Returns true if there's at least one index that can be read.
+ '''
+ for deploy in self.deploys.all():
+ if deploy.is_readable():
+ return True
+
+ def populate_for_account(self, account):
+ self.account = account
+ self.configuration = account.configuration
+ if account.default_analyzer is not None:
+ self.analyzer_config = account.default_analyzer.configuration
+
+ def searchable_deploy(self):
+ '''Returns a single deploy that can be used to search. If no deploy is searcheable
+ it returns None. Note that if more than one deploy is searcheable, there are no warranties
+ of wich one will be returned.'''
+ # TODO : should change once deploy roles are implemented
+ ds = self.deploys.all()
+ ds = [d for d in ds if d.is_readable()]
+ return ds[0] if ds else None
+
+ def indexable_deploys(self):
+ '''Returns the list of all deploys that should be updated (adds/updates/deletes/etc)'''
+ # TODO : should change once deploy roles are implemented
+ ds = self.deploys.all()
+ ds = [d for d in ds if d.is_writable()]
+ return ds
+
+ def get_functions_dict(self):
+ return dict((str(f.name), f.definition) for f in self.scorefunctions.all())
+
+ def get_debug_info(self):
+ info = 'Index: %s [%s]\n' % (self.name, self.code) +\
+ 'Account: %s\n' % self.account.user.email +\
+ 'Deploys:\n'
+ for d in self.deploys.all():
+ info += ' [deploy:%d] %s on [worker:%s] %s:%s' % (d.id, d.status, d.worker.id, d.worker.wan_dns, d.base_port)
+ return info
+
+ def update_status(self, new_status):
+ print 'updating status %s for %r' % (new_status, self)
+ logger.debug('Updating status to %s for idnex %r', new_status, self)
+ Index.objects.filter(id=self.id).update(status=new_status)
+
+ def mark_deleted(self):
+ Index.objects.filter(id=self.id).update(deleted=True)
+
+ class AutocompleTypes:
+ created = 'DOCUMENTS'
+ initializing = 'QUERIES'
+
+ class Meta:
+ unique_together = (('code','account'),('name','account'))
+ db_table = 'storefront_index'
+
+class Insight(models.Model):
+ index = models.ForeignKey(Index, related_name='insights')
+ code = models.CharField(max_length=30, null=False)
+ data = models.TextField(null=False)
+ last_update = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ unique_together = ('index', 'code')
+ db_table = 'storefront_insight'
+
+class IndexConfiguration(models.Model):
+ description = models.TextField(null=False)
+ creation_date = models.DateField()
+ json_configuration = models.TextField(null=False)
+
+ def __repr__(self):
+ j_map = json.loads(self.json_configuration)
+ mapStr = '{\n'
+ for m in j_map:
+ mapStr += '\t\t%s -> %s\n' % (m, j_map[m])
+ mapStr += '\t}\n'
+ return 'IndexConfiguration (%s):\n\tdescription: %s\n\tcreation_date: %s\n\tjson_configuration: %s\n' % (self.id, self.description, str(self.creation_date), mapStr)
+
+ def __str__(self):
+ return '(description: %s; creation_date: %s; json_configuration: %s)' % (self.description, str(self.creation_date), self.json_configuration)
+
+ def get_data(self):
+ map = json.loads(self.json_configuration)
+ data = {}
+ for k,v in map.items():
+ data[str(k)] = v
+ data['ram'] = data.get('xmx',0) + data.get('bdb_cache',0)
+ return data
+ def set_data(self, data):
+ self.json_configuration = json.dumps(data)
+
+ class Meta:
+ db_table = 'storefront_indexconfiguration'
+
+class Analyzer(models.Model):
+ account = models.ForeignKey('Account', related_name='analyzers')
+ code = models.CharField(max_length=64)
+ configuration = models.TextField()
+
+ class Meta:
+ db_table = 'storefront_analyzer'
+
+class AnalyzerComponent(models.Model):
+ code = models.CharField(max_length=15, unique=True)
+ name = models.CharField(max_length=200)
+ description = models.CharField(max_length=1000)
+ config = models.TextField(null=False,blank=False)
+ factory = models.CharField(max_length=200)
+ type = models.CharField(max_length=20)
+ enabled = models.BooleanField()
+
+ class Types:
+ tokenizer = 'TOKENIZER'
+ filter = 'FILTER'
+
+ class Meta:
+ db_table = 'storefront_analyzercomponent'
+
+def create_analyzer(code, name, config, factory, type, enabled):
+ analyzer = None
+ try:
+ analyzer = AnalyzerComponent.objects.get(code=code)
+
+ analyzer.name = name
+ analyzer.config = config
+ analyzer.factory = factory
+ analyzer.type = type
+ analyzer.enabled = enabled
+
+ analyzer.save()
+ except AnalyzerComponent.DoesNotExist:
+ analyzer = AnalyzerComponent(code=code, name=name, config=config, type=type, enabled=enabled)
+ analyzer.save()
+
+class Package(models.Model):
+ '''
+ Packages define what a user have the right to when creating an Account and how does the indexes in that Account
+ behave.
+ There are two sections for what the Package configures. A fixed section with the control and limits information
+ that is used by nebu, storefront or api (base_price, index_max_size, searches_per_day, max_indexes). A dynamic
+ section that is handled by the IndexConfiguration object. The information of that section is passed to the IndexEngine
+ as it is and handled by it.
+ '''
+ name = models.CharField(max_length=50)
+ code = models.CharField(max_length=30)
+ base_price = models.FloatField()
+ index_max_size = models.IntegerField()
+ searches_per_day = models.IntegerField()
+ max_indexes = models.IntegerField()
+
+ configuration = models.ForeignKey('IndexConfiguration', null=True)
+
+ def __repr__(self):
+ return 'Package (%s):\n\tname: %s\n\tcode: %s\n\tbase_price: %.2f\n\tindex_max_size: %i\n\tsearches_per_day: %i\n\tmax_indexes: %i\n' % (self.id, self.name, self.code, self.base_price, self.index_max_size, self.searches_per_day, self.max_indexes)
+
+ def __str__(self):
+ return '(name: %s; code: %s; base_price: %.2f; index_max_size: %i; searches_per_day: %i; max_indexes: %i)' % (self.name, self.code, self.base_price, self.index_max_size, self.searches_per_day, self.max_indexes)
+
+ def max_size_mb(self):
+ return self.index_max_size * settings.INDEX_SIZE_RATIO
+ class Meta:
+ db_table = 'storefront_package'
+
+class ScoreFunction(models.Model):
+ index = models.ForeignKey(Index, related_name='scorefunctions')
+ name = models.IntegerField(null=False) # TODO the java API expects an int. But a String may be nicer for name.
+ definition = models.CharField(max_length=255, blank=False, null=True)
+
+ class Meta:
+ db_table = 'storefront_scorefunction'
+ unique_together = (('index','name'))
+
+
+def create_configuration(description, data, creation_date=None):
+ configuration = IndexConfiguration()
+ configuration.description = description
+ configuration.creation_date = creation_date or datetime.now()
+ configuration.json_configuration = json.dumps(data)
+
+ configuration.save()
+ return configuration
+
+def create_package(code, name, base_price, index_max_size, searches_per_day, max_indexes, configuration_map):
+# The configuration_map will only be considered if the package if new or if it didn't already have a configuration
+
+ package = None
+ try:
+ package = Package.objects.get(code=code)
+
+ package.name = name
+ package.base_price = base_price
+ package.index_max_size = index_max_size
+ package.searches_per_day = searches_per_day
+ package.max_indexes = max_indexes
+
+ if not package.configuration:
+ package.configuration = create_configuration('package:' + code, configuration_map)
+
+ package.save()
+ except Package.DoesNotExist:
+ configuration = create_configuration('package:' + code, configuration_map)
+ package = Package(code=code, base_price=base_price, index_max_size=index_max_size, searches_per_day=searches_per_day, max_indexes=max_indexes, configuration=configuration)
+ package.save()
+
+def create_provisioner(name, token, email, plans):
+ provisioner = None
+ try:
+ provisioner = Provisioner.objects.get(name=name)
+ except Provisioner.DoesNotExist:
+ provisioner = Provisioner()
+ provisioner.name = name
+ provisioner.token = token
+ provisioner.email = email
+ provisioner.save()
+
+ provisioner.plans.all().delete()
+ for plan, code in plans.items():
+ pp = ProvisionerPlan()
+ pp.plan = plan
+ pp.provisioner = provisioner
+ pp.package = Package.objects.get(code=code)
+ pp.save()
+
+
+class AccountMovement(models.Model):
+ account = models.ForeignKey('Account', related_name='movements')
+ class Meta:
+ db_table = 'storefront_accountmovement'
+
+class ActionLog(models.Model):
+ account = models.ForeignKey('Account', related_name='actions')
+ class Meta:
+ db_table = 'storefront_actionlog'
+
+class PFUser(models.Model):
+ user = models.ForeignKey(User, unique=True)
+ account = models.OneToOneField('Account', related_name='user')
+ email = models.EmailField(unique=True, max_length=255)
+ change_password = models.BooleanField(default=False, null=False)
+ class Meta:
+ db_table = 'storefront_pfuser'
+
+
+
+MAX_USABLE_RAM_PERCENTAGE = 0.9
+# Nebulyzer stuff
+class Worker(models.Model):
+ '''
+ Describes an amazon ec2 instance.
+ '''
+ instance_name = models.CharField(max_length=50,null=False,blank=False)
+ lan_dns = models.CharField(max_length=100,null=False,blank=False)
+ wan_dns = models.CharField(max_length=100,null=False,blank=False)
+ status = models.CharField(max_length=30)
+ timestamp = models.DateTimeField(auto_now=True,auto_now_add=True)
+ #physical memory in MegaBytes
+ ram = models.IntegerField()
+
+ class States:
+ created = 'CREATED'
+ initializing = 'INITIALIZING'
+ updating = 'UPDATING'
+ controllable = 'CONTROLLABLE'
+ decommissioning = 'DECOMMISSIONING'
+ dying = 'DYING'
+ dead = 'DEAD'
+
+ class Meta:
+ db_table = 'storefront_worker'
+
+ def get_usable_ram(self):
+ '''Return the amount of ram that can be used in this machine for
+ indexengines. It's calculated as a fixed percentage of the physical
+ ram. Value returned in MegaBytes'''
+ return MAX_USABLE_RAM_PERCENTAGE * self.ram
+
+ def get_used_ram(self):
+ xmx = self.deploys.aggregate(xmx=Sum('effective_xmx'))['xmx']
+ bdb = self.deploys.aggregate(bdb=Sum('effective_bdb'))['bdb']
+ if xmx == None:
+ xmx = 0
+ if bdb == None:
+ bdb = 0
+ return xmx + bdb
+
+ def is_assignable(self):
+ return self.status != Worker.States.decommissioning
+
+ def is_ready(self):
+ return self.status in [Worker.States.controllable, Worker.States.decommissioning]
+
+ def __repr__(self):
+ return 'Worker (%s):\n\tinstance_name: %s\n\tlan_dns: %s\n\twan_dns: %s\n\tstatus: %s\n\ttimestamp: %s\n\tram: %s\n' %(self.pk, self.instance_name, self.lan_dns, self.wan_dns, self.status, self.timestamp, self.ram)
+
+class Service(models.Model):
+ name = models.CharField(max_length=50,null=False,blank=False)
+ type = models.CharField(max_length=50,null=True,blank=True )
+ host = models.CharField(max_length=100,null=False,blank=False)
+ port = models.IntegerField()
+ timestamp = models.DateTimeField(auto_now=True,auto_now_add=True)
+
+ class Meta:
+ db_table = 'storefront_service'
+
+ def __repr__(self):
+ return 'Service (%s):\n\tname: %s\n\ttype: %s\n\thost: %s\n\tport: %s\n\ttimestamp: %s\n' % (self.pk, self.name, self.type, self.host, self.port, self.timestamp)
+
+
+# CPU Stats
+class WorkerMountInfo(models.Model):
+ worker = models.ForeignKey(Worker, related_name="disk_infos")
+ timestamp = models.DateTimeField()
+
+ mount = models.CharField(max_length=100,null=False,blank=False)
+ available = models.IntegerField()
+ used = models.IntegerField()
+
+ class Meta:
+ db_table = 'storefront_workermountinfo'
+
+
+class WorkerLoadInfo(models.Model):
+ worker = models.ForeignKey(Worker, related_name="load_infos")
+ timestamp = models.DateTimeField()
+
+ load_average = models.FloatField()
+
+ class Meta:
+ db_table = 'storefront_workerloadinfo'
+
+class WorkerIndexInfo(models.Model):
+ worker = models.ForeignKey(Worker, related_name="indexes_infos")
+ timestamp = models.DateTimeField()
+
+ deploy = models.ForeignKey('Deploy', related_name="index_infos")
+ used_disk = models.IntegerField()
+ used_mem = models.IntegerField()
+
+ class Meta:
+ db_table = 'storefront_workerindexinfo'
+
+
+class Deploy(models.Model):
+ '''
+ Describes a deploy of an index on a worker, and it's status.
+ The idea is that an index can be moving from one worker to another,
+ so queries and indexing requests have to be mapped to one or more
+ index engines.
+ '''
+ index = models.ForeignKey(Index, related_name="deploys")
+ worker = models.ForeignKey(Worker, related_name="deploys")
+ base_port = models.IntegerField()
+ status = models.CharField(max_length=30)
+ timestamp = models.DateTimeField(auto_now=True,auto_now_add=True) # Last time we updated this deploy.
+ parent = models.ForeignKey('self', related_name='children', null=True) # For moving deploys.
+ effective_xmx = models.IntegerField()
+ effective_bdb = models.IntegerField()
+ dying = models.BooleanField(default=False, null=False)
+
+ # TODO add role fields
+ #searching_role = models.BooleanField()
+ #indexing_role = models.BooleanField()
+
+ def __repr__(self):
+ return 'Deploy (%s):\n\tparent deploy: %s\n\tindex code: %s\n\tstatus: %s\n\tworker ip: %s\n\tport: %d\n\teffective_xmx: %d\n\teffective_bdb: %d\n' % (self.id, self.parent_id, self.index.code, self.status, self.worker.lan_dns, self.base_port, self.effective_xmx, self.effective_bdb)
+
+ def __unicode__(self):
+ return "Deploy: %s on %s:%d" % (self.status, self.worker.lan_dns, self.base_port)
+
+ def is_readable(self):
+ '''Returns true if a search can be performed on this deployment, and
+ the returned data is up to date'''
+ return self.status == Deploy.States.controllable or \
+ self.status == Deploy.States.move_requested or \
+ self.status == Deploy.States.moving or \
+ (self.status == Deploy.States.recovering and not self.parent)
+
+ def is_writable(self):
+ '''Returns True if new data has to be written to this deployment.'''
+ return self.status == Deploy.States.controllable or \
+ self.status == Deploy.States.recovering or \
+ self.status == Deploy.States.move_requested or \
+ self.status == Deploy.States.moving
+
+ def total_ram(self):
+ return self.effective_xmx + self.effective_bdb
+
+ def update_status(self, new_status):
+ print 'updating status %s for %r' % (new_status, self)
+ logger.debug('Updating status to %s for deploy %r', new_status, self)
+ Deploy.objects.filter(id=self.id).update(status=new_status, timestamp=datetime.now())
+
+ def update_parent(self, new_parent):
+ logger.debug('Updating parent to %s for deploy %r', new_parent, self)
+ Deploy.objects.filter(id=self.id).update(parent=new_parent)
+
+ class States:
+ created = 'CREATED'
+ initializing = 'INITIALIZING'
+ recovering = 'RECOVERING'
+ resurrecting = 'RESURRECTING'
+ controllable = 'CONTROLLABLE'
+ move_requested = 'MOVE_REQUESTED'
+ moving = 'MOVING'
+ decommissioning = 'DECOMMISSIONING'
+
+ class Meta:
+ db_table = 'storefront_deploy'
+
+class BetaTestRequest(models.Model):
+ email = models.EmailField(unique=True, max_length=255)
+ site_url = models.CharField(max_length=200,null=False,blank=False)
+ summary = models.TextField(null=False,blank=False)
+
+ request_date = models.DateTimeField(default=datetime.now)
+ status = models.CharField(max_length=50,null=True)
+
+ class Meta:
+ db_table = 'storefront_betatestrequest'
+
+class BetaInvitation(models.Model):
+ password = models.CharField(max_length=20, null=True)
+ account = models.ForeignKey('Account', null=True)
+ assigned_customer = models.CharField(max_length=50, null=True)
+ beta_requester = models.ForeignKey('BetaTestRequest', null=True, related_name="invitation")
+
+ invitation_date = models.DateTimeField(default=datetime.now)
+ forced_package = models.ForeignKey('Package', null=False)
+
+ class Meta:
+ db_table = 'storefront_signupotp'
+
+class ContactInfo(models.Model):
+ name = models.CharField(max_length=64)
+ email = models.EmailField(unique=True, max_length=255)
+ request_date = models.DateTimeField(default=datetime.now)
+ source = models.CharField(max_length=64, null=True)
+
+ class Meta:
+ db_table = 'storefront_contactinfo'
+
+
+
+class Provisioner(models.Model):
+ name = models.CharField(max_length=64)
+ token = models.CharField(max_length=64, null=False, blank=False)
+ email = models.EmailField(max_length=255) # contact info for the provisioner
+
+ class Meta:
+ db_table = "storefront_provisioner"
+
+class ProvisionerPlan(models.Model):
+ plan = models.CharField(max_length=50)
+ provisioner = models.ForeignKey('Provisioner', related_name='plans')
+ package = models.ForeignKey('Package')
+
+ class Meta:
+ db_table = "storefront_provisionerplan"
+
+class BlogPostInfo(models.Model):
+ title = models.CharField(max_length=200)
+ url = models.CharField(max_length=1024)
+ date = models.DateTimeField()
+ author = models.CharField(max_length=64)
+
+ class Meta:
+ db_table = 'storefront_blogpost'
+
diff --git a/backoffice/api_linked_rpc.py b/backoffice/api_linked_rpc.py
new file mode 100644
index 0000000..2fd7649
--- /dev/null
+++ b/backoffice/api_linked_rpc.py
@@ -0,0 +1,169 @@
+from flaptor.indextank.rpc import Indexer, Searcher, Suggestor, Storage, LogWriter, WorkerManager,\
+ DeployManager, Controller, FrontendManager
+
+from flaptor.indextank.rpc.ttypes import NebuException, IndextankException
+
+''' ===========================
+ THRIFT STUFF
+ =========================== '''
+from thrift.transport import TSocket, TTransport
+from thrift.protocol import TBinaryProtocol
+from lib import flaptor_logging, exceptions
+from thrift.transport.TTransport import TTransportException
+from socket import socket
+from socket import error as SocketError
+
+
+logger = flaptor_logging.get_logger('RPC')
+
+# Missing a way to close transport
+def getThriftControllerClient(host, timeout_ms=None):
+ protocol, transport = __getThriftProtocolTransport(host,19010, timeout_ms)
+ client = Controller.Client(protocol)
+ transport.open()
+ return client
+
+# Missing a way to close transport
+def getThriftIndexerClient(host, base_port, timeout_ms=None):
+ protocol, transport = __getThriftProtocolTransport(host, base_port + 1, timeout_ms)
+ client = Indexer.Client(protocol)
+ transport.open()
+ return client
+
+def getThriftSearcherClient(host, base_port, timeout_ms=None):
+ protocol, transport = __getThriftProtocolTransport(host, base_port + 2, timeout_ms)
+ client = Searcher.Client(protocol)
+ transport.open()
+ return client
+
+def getThriftSuggestorClient(host, base_port):
+ protocol, transport = __getThriftProtocolTransport(host, base_port + 3)
+ client = Suggestor.Client(protocol)
+ transport.open()
+ return client
+
+storage_port = 10000
+def getThriftStorageClient():
+ protocol, transport = __getThriftProtocolTransport('storage',storage_port)
+ client = Storage.Client(protocol)
+ transport.open()
+ return client
+
+def getThriftLogWriterClient(host, port, timeout_ms=500):
+ protocol, transport = __getThriftProtocolTransport(host,port,timeout_ms)
+ client = LogWriter.Client(protocol)
+ transport.open()
+ return client
+
+def getThriftLogReaderClient(host, port, timeout_ms=None):
+ protocol, transport = __getThriftProtocolTransport(host,port,timeout_ms)
+ client = LogWriter.Client(protocol)
+ transport.open()
+ return client
+
+class ReconnectingClient:
+ def __init__(self, factory):
+ self.factory = factory
+ self.delegate = None #factory()
+
+ def __getattr__(self, name):
+ import types
+ if self.delegate is None:
+ self.delegate = self.factory()
+ att = getattr(self.delegate, name)
+ if type(att) is types.MethodType:
+ def wrap(*args, **kwargs):
+ try:
+ return att(*args, **kwargs)
+ except (NebuException, IndextankException):
+ logger.warn('raising catcheable exception')
+ raise
+ except (TTransportException, IOError, SocketError):
+ logger.warn('failed to run %s, reconnecting once', name)
+ self.delegate = self.factory()
+ att2 = getattr(self.delegate, name)
+ return att2(*args, **kwargs)
+ except Exception:
+ logger.exception('Unexpected failure to run %s, reconnecting once', name)
+ self.delegate = self.factory()
+ att2 = getattr(self.delegate, name)
+ return att2(*args, **kwargs)
+
+ return wrap
+ else:
+ return att
+
+def getReconnectingStorageClient():
+ return ReconnectingClient(getThriftStorageClient)
+
+def getReconnectingLogWriterClient(host, port):
+ return ReconnectingClient(lambda: getThriftLogWriterClient(host, port))
+
+worker_manager_port = 8799
+def getThriftWorkerManagerClient(host):
+ protocol, transport = __getThriftProtocolTransport(host,worker_manager_port)
+ client = WorkerManager.Client(protocol)
+ transport.open()
+ return client
+
+deploymanager_port = 8899
+def get_deploy_manager():
+ protocol, transport = __getThriftProtocolTransport('deploymanager',deploymanager_port)
+ client = DeployManager.Client(protocol)
+ transport.open()
+ return client
+
+
+def __getThriftProtocolTransport(host, port=0, timeout_ms=None):
+ ''' returns protocol,transport'''
+ # Make socket
+ transport = TSocket.TSocket(host, port)
+
+ if timeout_ms is not None:
+ transport.setTimeout(timeout_ms)
+
+ # Buffering is critical. Raw sockets are very slow
+ transport = TTransport.TBufferedTransport(transport)
+
+ # Wrap in a protocol
+ protocol = TBinaryProtocol.TBinaryProtocol(transport)
+ return protocol, transport
+
+
+def get_searcher_client(index, timeout_ms=None):
+ '''
+ This method returns a single searcherclient, or None
+ '''
+ deploy = index.searchable_deploy()
+ if deploy:
+ return getThriftSearcherClient(deploy.worker.lan_dns, int(deploy.base_port), timeout_ms)
+ else:
+ return None
+
+def get_worker_controller(worker, timeout_ms=None):
+ return getThriftControllerClient(worker. lan_dns)
+
+def get_suggestor_client(index):
+ '''
+ This method returns a single suggestorclient, or None
+ '''
+ deploy = index.searchable_deploy()
+ if deploy:
+ return getThriftSuggestorClient(deploy.worker.lan_dns, int(deploy.base_port))
+ else:
+ return None
+
+def get_indexer_clients(index, timeout_ms=1000):
+ '''
+ This method returns the list of all indexerclients that should be updated
+ on add,delete,update, and category updates.
+ @raise exceptions.NoIndexerException if this index has no writable deploy.
+ '''
+ deploys = index.indexable_deploys()
+ retval = []
+ for d in deploys:
+ retval.append(getThriftIndexerClient(d.worker.lan_dns, int(d.base_port), timeout_ms))
+ if retval:
+ return retval
+ else:
+ raise exceptions.NoIndexerException()
diff --git a/backoffice/cloud/amazon_credential.py b/backoffice/cloud/amazon_credential.py
new file mode 100644
index 0000000..c2910f5
--- /dev/null
+++ b/backoffice/cloud/amazon_credential.py
@@ -0,0 +1,2 @@
+AMAZON_USER = ""
+AMAZON_PASSWORD = ""
diff --git a/backoffice/cloud/config_dbslave.py b/backoffice/cloud/config_dbslave.py
new file mode 100644
index 0000000..fc527a3
--- /dev/null
+++ b/backoffice/cloud/config_dbslave.py
@@ -0,0 +1,201 @@
+import sys, time
+import subprocess, shlex
+import MySQLdb
+
+from optparse import OptionParser
+
+def config_dbslave(master_host, slave_host, user, password):
+
+ print 'Configuring DB slave'
+ print 'MASTER:%s' % master_host
+ print 'SLAVE:%s' % slave_host
+
+ # ASSIGN SERVER-ID AND CONFIGURE MY.CNF FILE
+ slave_id = int(time.time())
+ print 'SLAVE: Configuring mysql slave with id: %s' % slave_id
+
+ set_up_mysql_config = (
+ "sudo service mysql stop;" +
+ "sudo sed -i 's/server-id = 0/server-id = %s/' /etc/mysql/my.cnf;" % slave_id +
+ "sudo service mysql start"
+ )
+
+ retry = 0
+ ssh_done = False
+
+ while not ssh_done:
+ ret_code = subprocess.call(['ssh','-oUserKnownHostsFile=/dev/null','-oStrictHostKeyChecking=no', 'flaptor@%s' % slave_host, set_up_mysql_config])
+
+ if ret_code:
+ if retry < 3:
+ print 'WARNING: Couldn\'t configure and start mysql service at slave %s. Retrying' % slave_host
+ retry += 1
+ time.sleep(1)
+ else:
+ print 'ERROR:Couldn\'t configure and start mysql service at slave %s' % slave_host
+ sys.exit(1)
+ else:
+ ssh_done = True
+
+ #############################################################################################
+ # This script requires a machine running with mysql, '****' user, and 'indextank' database
+ # 1.- Promote master_host to master (if applies)
+ # 2.- Dump data from master
+ # 3.- Import data in slave
+ # 4.- turn on replication
+ # 5.- check tables are synced
+ #############################################################################################
+
+ ####################
+ # Open connections #
+ ####################
+
+ dbmaster = MySQLdb.connect(host=master_host,user=user,passwd=password,db='indextank')
+ dbslave = MySQLdb.connect(host=slave_host,user=user,passwd=password,db='indextank')
+
+ ##########################################
+ # 1.- Promote master to slave (if applies)
+ ##########################################
+
+ print 'MASTER: Checking master_host is master (and promoting to master if not)'
+
+ mc = dbmaster.cursor()
+ is_slave = mc.execute('SHOW SLAVE STATUS')
+ if is_slave:
+ mc.execute('STOP SLAVE')
+ mc.execute('RESET MASTER')
+ mc.execute("CHANGE MASTER TO MASTER_HOST=''")
+
+ is_slave = mc.execute('SHOW SLAVE STATUS')
+ if is_slave:
+ print 'Warning: MASTER (%s) seems to still work as slave'
+
+ ############################
+ # 2.- dump data from master
+ ############################
+ print 'MASTER: Locking tables'
+ start_lock_time = time.time()
+ mc.execute('FLUSH TABLES WITH READ LOCK')
+
+ print 'MASTER: Dumping data'
+ command_line = 'mysqldump --tables -h%s -u%s -p%s indextank' % (master_host, user, password)
+ dump_data_process = subprocess.Popen(shlex.split(command_line), stdout=subprocess.PIPE)
+
+ ##########################
+ # 3.- import data in slave
+ ##########################
+
+ # 3.1.- Stop slave
+ print 'SLAVE: Stopping slave process'
+ sc = dbslave.cursor()
+ sc.execute('stop slave')
+
+ # 2.2.- import data
+ print 'SLAVE: Importing master dump'
+ ret_code = subprocess.call(['mysql', '-h%s' % slave_host, '-u%s' % user, '-p%s' % password, 'indextank'], stdin=dump_data_process.stdout)
+ dump_data_process.stdout.close()
+
+ if ret_code:
+ print 'ERROR SLAVE: Import data failed on (%s)' % slave_host
+ sys.exit(1)
+
+ #########################
+ # 4.- turn on replication
+ #########################
+
+ # 4.1 Request master status
+ print 'MASTER: request master status'
+ mc.execute('SHOW MASTER STATUS')
+ master_status = mc.fetchone()
+ if not master_status:
+ print "ERROR: Couldn't turn replication on"
+ sys.exit(1)
+
+ binlog_filename = master_status[0]
+ binlog_position = master_status[1]
+
+ # 4.2 config master in slave and start slave
+ print 'SLAVE: config master in slave'
+
+ sc.execute("CHANGE MASTER TO MASTER_HOST=%s, MASTER_USER=%s, MASTER_PASSWORD=%s, MASTER_LOG_FILE=%s, MASTER_LOG_POS=%s", [master_host, user, password, binlog_filename, binlog_position])
+
+ sc.execute('START SLAVE')
+
+ is_slave = sc.execute('SHOW SLAVE STATUS')
+
+ if not is_slave:
+ print 'WARNING SLAVE: Setting up slave failed'
+
+ # 4.3 unlock master tables
+ print 'MASTER: unlock tables'
+ mc.execute('UNLOCK TABLES')
+ end_lock_time = time.time()
+
+ print 'Locking time: %s sec.' % (end_lock_time - start_lock_time)
+
+ #####################
+ # Close connections #
+ #####################
+ mc.close()
+ dbmaster.close()
+
+ sc.close()
+ dbslave.close()
+
+ ##################################
+ # 5. check tables are synchronized
+ ##################################
+ print 'Checking tables are synchronized'
+ chescksum_process = subprocess.Popen(['mk-table-checksum', '--databases', 'indextank', '-u'+user, '-p'+password, master_host, slave_host], stdout=subprocess.PIPE)
+
+ checksum_output = chescksum_process.communicate()[0]
+
+ checksum_lines = checksum_output.splitlines()
+
+ found_error = False
+ for i in range(0,len(checksum_lines)/2):
+ table_check1 = checksum_lines[2*i+1].split()
+ table_check2 = checksum_lines[2*i+2].split()
+ is_synchronized = table_check1[0] == table_check2[0] and table_check1[1] == table_check2[1] and table_check1[4] == table_check2[4] and table_check1[6] == table_check2[6]
+ if not is_synchronized:
+ found_error = True
+ print 'ERROR: table %s.%s out of sync \n%s \n%s' % (table_check1[0], table_check1[1], table_check1, table_check2)
+
+ if found_error:
+ print 'Some errors were found in synchronization. Please check master-slave status.'
+ else:
+ print 'Synchronization succesful.'
+
+# SET AND READ PARAMETERS
+if __name__ == '__main__':
+ parser = OptionParser(usage="usage: %prog -m master_host -s slave_host -u db_user -p db_pass")
+ parser.add_option("-m", "--master_host", dest="master", help="Master host")
+ parser.add_option("-s", "--slave_host", dest="slave", help="Slave host")
+ parser.add_option("-u", "--user", dest="user", default=True, help="Database user")
+ parser.add_option("-p", "--passwd", dest="password", help="Database user")
+
+ options, _ = parser.parse_args()
+
+ if not options.master:
+ print 'master_host option is required'
+ sys.exit(1)
+ master_host = options.master
+
+ if not options.slave:
+ print 'master_slave option is required'
+ sys.exit(1)
+ slave_host = options.slave
+
+ if not options.user:
+ print 'user option is required'
+ sys.exit(1)
+ user = options.user
+
+ if options.password:
+ password = options.password
+ else:
+ print 'Enter DB password:'
+ password = sys.stdin.readline()
+
+ config_dbslave(master_host, slave_host, user, password)
+
diff --git a/backoffice/cloud/create_dbslave.py b/backoffice/cloud/create_dbslave.py
new file mode 100644
index 0000000..1ac441a
--- /dev/null
+++ b/backoffice/cloud/create_dbslave.py
@@ -0,0 +1,22 @@
+import subprocess, sys, json
+from config_dbslave import config_dbslave
+from launch_ami import launch_ami
+
+def create_dbslave(master_host):
+ print 'Creating DB SLAVE from MASTER %s' % master_host
+ instance_data = launch_ami('db')
+
+ slave_host = instance_data['public_dns']
+ config_dbslave(master_host, slave_host, '****', '****')
+
+ print 'DB Slave succesfully started in: %s' % slave_host
+
+if __name__ == '__main__':
+ if len(sys.argv) < 2:
+ print "Usage: %s " % sys.argv[0]
+ sys.exit(1)
+
+ master_host = sys.argv[1]
+ create_dbslave(master_host)
+
+
diff --git a/backoffice/cloud/launch_ami.py b/backoffice/cloud/launch_ami.py
new file mode 100644
index 0000000..e9e320f
--- /dev/null
+++ b/backoffice/cloud/launch_ami.py
@@ -0,0 +1,42 @@
+import boto, sys, json, re
+
+from amazon_credential import AMAZON_USER, AMAZON_PASSWORD
+
+from replicate_instance import replicate_instance
+
+def ec2_connection():
+ return boto.connect_ec2(AMAZON_USER, AMAZON_PASSWORD)
+
+def launch_ami(ami_type):
+ print 'Launching %s instance' % ami_type
+ print 'Finding %s ami' % ami_type
+
+ conn = ec2_connection()
+ amis = conn.get_all_images()
+
+ sort_function = lambda x: x.location
+
+ selected_amis = []
+ for ami in amis:
+ if re.search('indextank-%s' % ami_type, ami.location):
+ selected_amis.append(ami)
+
+ if len(selected_amis) == 0:
+ print 'ERROR: no ami for type %s' % ami_type
+ sys.exit(1)
+
+ selected_amis.sort(key=sort_function)
+
+ ami = selected_amis[-1]
+
+ print 'AMI found: %s %s' % (ami.id, ami.location)
+ return replicate_instance(ami_type, ami.id, logging=True)
+
+if __name__ == '__main__':
+ if len(sys.argv) < 2:
+ print "Usage: %s [fend|api|db]" % sys.argv[0]
+ sys.exit(1)
+
+ ami_type = sys.argv[1]
+ print json.dumps(launch_ami(ami_type))
+
diff --git a/backoffice/cloud/launch_ami.sh b/backoffice/cloud/launch_ami.sh
new file mode 100755
index 0000000..213ea4d
--- /dev/null
+++ b/backoffice/cloud/launch_ami.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+show_help() { echo; echo "Usage: $0 [fend|api|db|log]"; echo; exit; }
+
+if [ $# -lt 1 ]; then
+ show_help
+fi
+
+ami_type=$1
+
+ami_id=`ec2-describe-images | awk '/^IMAGE/ {if($3~/indextank-'${ami_type}'-/) {print $2,$3}}' | sed -r 's/(ami-[0-9a-f]+) .*indextank-.*-([0-9.]+).*/\2 \1/' | sort -nr | head -1 | awk '{print $2}'`
+
+echo python replicate_instance.py $ami_type $ami_id
+python replicate_instance.py $ami_type $ami_id
diff --git a/backoffice/cloud/replicate_instance.py b/backoffice/cloud/replicate_instance.py
new file mode 100644
index 0000000..79301b4
--- /dev/null
+++ b/backoffice/cloud/replicate_instance.py
@@ -0,0 +1,68 @@
+import boto, time
+import socket, sys, json
+
+from amazon_credential import AMAZON_USER, AMAZON_PASSWORD
+
+def ec2_connection():
+ return boto.connect_ec2(AMAZON_USER, AMAZON_PASSWORD)
+
+def replicate_instance(ami_type, ami_id, zone='us-east-1a', security_group=None, logging=False):
+ INSTANCE_TYPES = {
+ 'fend': 'c1.medium',
+ 'api': 'm1.xlarge',
+ 'db': 'm1.large',
+ 'log': 'm1.large'
+ }
+
+ SECURITY_GROUPS = {
+ 'fend': 'indextank-main',
+ 'api': 'indextank-main',
+ 'db': 'indextank-db',
+ 'log': 'indextank-logstorage'
+ }
+
+ if not ami_type in INSTANCE_TYPES or (not security_group and not ami_type in SECURITY_GROUPS):
+ raise Exception("Invalid instance type")
+
+ if not security_group:
+ security_group = SECURITY_GROUPS[ami_type]
+
+ conn = ec2_connection()
+ res = conn.run_instances(image_id=ami_id, security_groups=[security_group], instance_type=INSTANCE_TYPES[ami_type], placement=zone)
+
+ if len(res.instances) == 0:
+ raise Exception("Replicated instance creation failed")
+
+ instance = res.instances[0]
+ instance_name = instance.id
+
+ if logging:
+ print "Successfully created replica (Instance: " + instance_name + ")"
+
+ time.sleep(5)
+
+ reservations = conn.get_all_instances([instance_name])
+ instance = reservations[0].instances[0]
+
+ while not instance.state == 'running':
+ if logging:
+ print "Waiting for the instance to start..."
+ time.sleep(10)
+
+ reservations = conn.get_all_instances([instance_name])
+ instance = reservations[0].instances[0]
+
+ time.sleep(2)
+
+ return {'id': instance_name, 'public_dns': instance.public_dns_name, 'private_dns': instance.private_dns_name}
+
+if __name__ == "__main__":
+ if len(sys.argv) < 3:
+ print 'replicate_instance receives at least two arguments'
+ sys.exit(1)
+
+ if len(sys.argv) == 4:
+ print json.dumps(replicate_instance(sys.argv[1], sys.argv[2], sys.argv[3]))
+ else:
+ print json.dumps(replicate_instance(sys.argv[1], sys.argv[2]))
+
diff --git a/backoffice/forms.py b/backoffice/forms.py
new file mode 100644
index 0000000..244d2aa
--- /dev/null
+++ b/backoffice/forms.py
@@ -0,0 +1,9 @@
+from django import forms
+from django.forms.widgets import HiddenInput
+
+class LoginForm(forms.Form):
+ email = forms.EmailField(required=True, label='E-Mail')
+ password = forms.CharField(widget=forms.PasswordInput, label='Password')
+
+class InvitationForm(forms.Form):
+ requesting_customer = forms.CharField(max_length=50, required=True)
\ No newline at end of file
diff --git a/backoffice/kill_webapp.sh b/backoffice/kill_webapp.sh
new file mode 100755
index 0000000..35e822c
--- /dev/null
+++ b/backoffice/kill_webapp.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+kill `cat pid`
diff --git a/backoffice/lib/__init__.py b/backoffice/lib/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backoffice/lib/authorizenet.py b/backoffice/lib/authorizenet.py
new file mode 100644
index 0000000..00bcd6c
--- /dev/null
+++ b/backoffice/lib/authorizenet.py
@@ -0,0 +1,153 @@
+from xml.dom.minidom import Document, parseString
+import httplib
+import urlparse
+
+
+class AuthorizeNet:
+ """
+ Basic client for Authorize.net's Automated Recurring Billing (ARB) service
+ """
+
+ def __init__(self):
+ from django.conf import settings
+ f = open("authorize.settings.prod") if not settings.DEBUG else open("authorize.settings.debug")
+ for line in f:
+ line = line.strip()
+ if len(line) > 0 and not line.startswith('#'):
+ parts = line.split('=',1)
+ var = parts[0].strip()
+ val = parts[1].strip()
+ if var in ['host_url','api_login_id','transaction_key']:
+ cmd = 'self.%s = %s' % (var,val)
+ exec(cmd)
+
+ def subscription_create(self, refId, name, length, unit, startDate, totalOccurrences, trialOccurrences,
+ amount, trialAmount, cardNumber, expirationDate, firstName, lastName, company,
+ address, city, state, zip, country):
+ doc,root = self._new_doc("ARBCreateSubscriptionRequest")
+ self._add_text_node(doc, root, 'refId', refId)
+ subscription = self._add_node(doc, root, 'subscription')
+ self._add_text_node(doc, subscription, 'name', name)
+ paymentSchedule = self._add_node(doc, subscription, 'paymentSchedule')
+ interval = self._add_node(doc, paymentSchedule, 'interval')
+ self._add_text_node(doc, interval, 'length', length)
+ self._add_text_node(doc, interval, 'unit', unit)
+ self._add_text_node(doc, paymentSchedule, 'startDate', startDate)
+ self._add_text_node(doc, paymentSchedule, 'totalOccurrences', totalOccurrences)
+ self._add_text_node(doc, paymentSchedule, 'trialOccurrences', trialOccurrences)
+ self._add_text_node(doc, subscription, 'amount', amount)
+ self._add_text_node(doc, subscription, 'trialAmount', trialAmount)
+ payment = self._add_node(doc, subscription, 'payment')
+ creditcard = self._add_node(doc, payment, 'creditCard')
+ self._add_text_node(doc, creditcard, 'cardNumber', cardNumber)
+ self._add_text_node(doc, creditcard, 'expirationDate', expirationDate)
+ billto = self._add_node(doc, subscription, 'billTo')
+ self._add_text_node(doc, billto, 'firstName', firstName)
+ self._add_text_node(doc, billto, 'lastName', lastName)
+ self._add_text_node(doc, billto, 'company', company)
+ self._add_text_node(doc, billto, 'address', address)
+ self._add_text_node(doc, billto, 'city', city)
+ self._add_text_node(doc, billto, 'state', state)
+ self._add_text_node(doc, billto, 'zip', zip)
+ self._add_text_node(doc, billto, 'country', country)
+ res = self._send_xml(doc.toxml())
+ subscriptionId = res.getElementsByTagName('subscriptionId')[0].childNodes[0].nodeValue
+ return subscriptionId
+
+
+ def subscription_update(self, refId, subscriptionId, name, startDate, totalOccurrences, trialOccurrences,
+ amount, trialAmount, cardNumber, expirationDate, firstName, lastName, company,
+ address, city, state, zip, country):
+ doc,root = self._new_doc("ARBUpdateSubscriptionRequest")
+ self._add_text_node(doc, root, 'refId', refId)
+ self._add_text_node(doc, root, 'subscriptionId', subscriptionId)
+ subscription = self._add_node(doc, root, 'subscription')
+ if name:
+ self._add_text_node(doc, subscription, 'name', name)
+ if startDate or totalOccurrences or trialOccurrences:
+ paymentSchedule = self._add_node(doc, subscription, 'paymentSchedule')
+ if startDate:
+ self._add_text_node(doc, paymentSchedule, 'startDate', startDate)
+ if totalOccurrences:
+ self._add_text_node(doc, paymentSchedule, 'totalOccurrences', totalOccurrences)
+ if trialOccurrences:
+ self._add_text_node(doc, paymentSchedule, 'trialOccurrences', trialOccurrences)
+ if amount:
+ self._add_text_node(doc, subscription, 'amount', amount)
+ if trialAmount:
+ self._add_text_node(doc, subscription, 'trialAmount', trialAmount)
+ if cardNumber and expirationDate:
+ payment = self._add_node(doc, subscription, 'payment')
+ creditcard = self._add_node(doc, payment, 'creditCard')
+ self._add_text_node(doc, creditcard, 'cardNumber', cardNumber)
+ self._add_text_node(doc, creditcard, 'expirationDate', expirationDate)
+ if firstName and lastName:
+ billto = self._add_node(doc, subscription, 'billTo')
+ self._add_text_node(doc, billto, 'firstName', firstName)
+ self._add_text_node(doc, billto, 'lastName', lastName)
+ if company:
+ self._add_text_node(doc, billto, 'company', company)
+ if address and city and state and zip and country:
+ self._add_text_node(doc, billto, 'address', address)
+ self._add_text_node(doc, billto, 'city', city)
+ self._add_text_node(doc, billto, 'state', state)
+ self._add_text_node(doc, billto, 'zip', zip)
+ self._add_text_node(doc, billto, 'country', country)
+ self._send_xml(doc.toxml())
+
+
+ def subscription_cancel(self, refId, subscriptionId):
+ doc,root = self._new_doc("ARBCancelSubscriptionRequest")
+ self._add_text_node(doc, root, 'refId', refId)
+ self._add_text_node(doc, root, 'subscriptionId', subscriptionId)
+ self._send_xml(doc.toxml())
+
+
+ def _add_node(self, doc, node, name):
+ elem = doc.createElement(name)
+ node.appendChild(elem)
+ return elem
+
+ def _add_text_node(self, doc, node, name, text):
+ elem = self._add_node(doc, node, name)
+ text_node = doc.createTextNode(text)
+ elem.appendChild(text_node)
+ return elem
+
+ def _new_doc(self, operation):
+ doc = Document()
+ root = doc.createElement(operation)
+ root.setAttribute('xmlns','AnetApi/xml/v1/schema/AnetApiSchema.xsd')
+ doc.appendChild(root)
+ auth = self._add_node(doc, root, 'merchantAuthentication')
+ self._add_text_node(doc, auth, 'name', self.api_login_id)
+ self._add_text_node(doc, auth, 'transactionKey', self.transaction_key)
+ return doc, root
+
+ def _send_xml(self, xml):
+ splits = urlparse.urlsplit(self.host_url)
+ print "connection.request('POST', "+self.host_url+", xml, {'Content-Type':'text/xml'})"
+ print "xml: "+xml
+ connection = httplib.HTTPSConnection(splits.hostname)
+ connection.request('POST', self.host_url, xml, {'Content-Type':'text/xml'})
+ response = connection.getresponse()
+ response.body = response.read()
+ connection.close()
+ print "resp: "+response.body
+ res = parseString(response.body)
+ ok = res.getElementsByTagName('resultCode')[0].childNodes[0].nodeValue == "Ok"
+ if not ok:
+ code = res.getElementsByTagName('message')[0].childNodes[0].childNodes[0].nodeValue
+ msg = res.getElementsByTagName('message')[0].childNodes[1].childNodes[0].nodeValue + " (%s)"%code
+ raise BillingException(msg,code)
+ return res
+
+
+class BillingException(Exception):
+ def __init__(self, msg, code):
+ self.msg = msg
+ self.code = code
+ def __str__(self):
+ return repr(self.msg)
+
+
diff --git a/backoffice/lib/encoder.py b/backoffice/lib/encoder.py
new file mode 100644
index 0000000..f6bb4dd
--- /dev/null
+++ b/backoffice/lib/encoder.py
@@ -0,0 +1,74 @@
+# Short URL Generator
+
+#DEFAULT_ALPHABET = 'JedR8LNFY2j6MrhkBSADUyfP5amuH9xQCX4VqbgpsGtnW7vc3TwKE'
+DEFAULT_ALPHABET = 'ed82j6rhkyf5amu9x4qbgpstn7vc3w1ioz'
+DEFAULT_BLOCK_SIZE = 22
+
+class Encoder(object):
+ def __init__(self, alphabet=DEFAULT_ALPHABET, block_size=DEFAULT_BLOCK_SIZE):
+ self.alphabet = alphabet
+ self.block_size = block_size
+ self.mask = (1 << block_size) - 1
+ self.mapping = range(block_size)
+ self.mapping.reverse()
+ def encode_url(self, n, min_length=0):
+ return self.enbase(self.encode(n), min_length)
+ def decode_url(self, n):
+ return self.decode(self.debase(n))
+ def encode(self, n):
+ return (n & ~self.mask) | self._encode(n & self.mask)
+ def _encode(self, n):
+ result = 0
+ for i, b in enumerate(self.mapping):
+ if n & (1 << i):
+ result |= (1 << b)
+ return result
+ def decode(self, n):
+ return (n & ~self.mask) | self._decode(n & self.mask)
+ def _decode(self, n):
+ result = 0
+ for i, b in enumerate(self.mapping):
+ if n & (1 << b):
+ result |= (1 << i)
+ return result
+ def enbase(self, x, min_length=0):
+ result = self._enbase(x)
+ padding = self.alphabet[0] * (min_length - len(result))
+ return '%s%s' % (padding, result)
+ def _enbase(self, x):
+ n = len(self.alphabet)
+ if x < n:
+ return self.alphabet[x]
+ return self.enbase(x/n) + self.alphabet[x%n]
+ def debase(self, x):
+ n = len(self.alphabet)
+ result = 0
+ for i, c in enumerate(reversed(x)):
+ result += self.alphabet.index(c) * (n**i)
+ return result
+
+DEFAULT_ENCODER = Encoder()
+
+def encode(n):
+ return DEFAULT_ENCODER.encode(n)
+
+def decode(n):
+ return DEFAULT_ENCODER.decode(n)
+
+def enbase(n, min_length=0):
+ return DEFAULT_ENCODER.enbase(n, min_length)
+
+def debase(n):
+ return DEFAULT_ENCODER.debase(n)
+
+def encode_url(n, min_length=0):
+ return DEFAULT_ENCODER.encode_url(n, min_length)
+
+def decode_url(n):
+ return DEFAULT_ENCODER.decode_url(n)
+
+def to_key(n):
+ return enbase(encode(n))
+
+def from_key(n):
+ return decode(debase(n))
diff --git a/backoffice/lib/error_logging.py b/backoffice/lib/error_logging.py
new file mode 100644
index 0000000..dbf927b
--- /dev/null
+++ b/backoffice/lib/error_logging.py
@@ -0,0 +1,13 @@
+import traceback
+from lib import flaptor_logging
+from django.http import HttpResponse
+
+logger = flaptor_logging.get_logger('error_logging')
+
+class ViewErrorLoggingMiddleware:
+
+ def process_view(self, request, view_func, view_args, view_kwargs):
+ self.view_name = view_func.__name__
+ def process_exception(self, request, exception):
+ logger.error('UNEXPECTED EXCEPTION in view "%s". Exception is: %s', self.view_name, repr(traceback.print_exc()))
+ return HttpResponse('{"status":"ERROR", "message":"Unexpected error."}')
diff --git a/backoffice/lib/exceptions.py b/backoffice/lib/exceptions.py
new file mode 100644
index 0000000..c6ff0a7
--- /dev/null
+++ b/backoffice/lib/exceptions.py
@@ -0,0 +1,8 @@
+
+
+class CloudException(Exception):
+ pass
+
+class NoIndexerException(CloudException):
+ pass
+
diff --git a/backoffice/lib/flaptor_logging.py b/backoffice/lib/flaptor_logging.py
new file mode 100644
index 0000000..1af893c
--- /dev/null
+++ b/backoffice/lib/flaptor_logging.py
@@ -0,0 +1,100 @@
+import logging as pylogging
+from logging import config
+import os
+
+usingNativeLogger = True
+
+__loggers = {}
+
+
+
+def get_logger(name, force_new=False):
+ '''Get the Logger instance for a given name'''
+ global __loggers
+ if __loggers is None:
+ __loggers = {}
+ if force_new:
+ return pylogging.getLogger(name)
+ if not __loggers.has_key(name):
+ __loggers[name] = pylogging.getLogger(name)
+ return __loggers[name]
+
+class SpecialFormatter(pylogging.Formatter):
+ BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
+ RESET_SEQ = "\033[0m"
+ COLOR_SEQ = "\033[37;4%dm"
+ PIDCOLOR_SEQ = "\033[1;3%dm"
+ BOLD_SEQ = "\033[1m"
+ COLORS = {
+ 'WARN': YELLOW,
+ 'INFO': GREEN,
+ 'DEBU': BLUE,
+ 'CRIT': RED,
+ 'ERRO': RED
+ }
+
+ def __init__(self, *args, **kwargs):
+ pylogging.Formatter.__init__(self, *args, **kwargs)
+ def format(self, record):
+ if not hasattr(record, 'prefix'): record.prefix = ''
+ if not hasattr(record, 'suffix'): record.suffix = ''
+ if not hasattr(record, 'compname'): record.compname = ''
+ record.pid = os.getpid()
+
+ record.levelname = record.levelname[:4]
+
+ r = pylogging.Formatter.format(self, record)
+ if record.levelname in SpecialFormatter.COLORS:
+ levelcolor = SpecialFormatter.COLOR_SEQ % (SpecialFormatter.COLORS[record.levelname])
+ r = r.replace('$LEVELCOLOR', levelcolor)
+ r = r.replace('$RESET', SpecialFormatter.RESET_SEQ)
+ else:
+ r = r.replace('$COLOR', '')
+ r = r.replace('$RESET', '')
+ pidcolor = SpecialFormatter.COLOR_SEQ % (1 + (record.pid % 5))
+ r = r.replace('$PIDCOLOR', pidcolor)
+ r = r.replace('$BOLD', SpecialFormatter.BOLD_SEQ)
+ return r
+
+pylogging.SpecialFormatter = SpecialFormatter
+
+if usingNativeLogger:
+ try:
+ config.fileConfig('logging.conf')
+ except Exception, e:
+ print e
+
+#class NativePythonLogger:
+# def __init__(self, name):
+# '''Creates a new Logger for the given name.
+# Do not call this method directly, instead use
+# get_logger(name) to get the appropriate instance'''
+# self.name = name
+# self.__logger = pylogging.getLogger(name)
+# #self.updateLevel(5)
+#
+# def updateLevel(self, level):
+# self.__level = level
+# if level == 1:
+# self.__logger.setLevel(pylogging.CRITICAL)
+# elif level == 2:
+# self.__logger.setLevel(pylogging.INFO)
+# elif level == 3:
+# self.__logger.setLevel(pylogging.WARNING)
+# elif level == 4:
+# self.__logger.setLevel(pylogging.INFO)
+# elif level == 5:
+# self.__logger.setLevel(pylogging.DEBUG)
+#
+# def debug(self, format_str, *values):
+# self.__logger.debug(format_str, *values)
+# def info(self, format_str, *values):
+# self.__logger.info(format_str, *values)
+# def warn(self, format_str, *values):
+# self.__logger.warn(format_str, *values)
+# def error(self, format_str, *values):
+# self.__logger.error(format_str, *values)
+# def exception(self, format_str, *values):
+# self.__logger.exception(format_str, *values)
+# def fatal(self, format_str, *values):
+# self.__logger.critical(format_str, *values)
diff --git a/backoffice/lib/mail.py b/backoffice/lib/mail.py
new file mode 100644
index 0000000..b1d3a08
--- /dev/null
+++ b/backoffice/lib/mail.py
@@ -0,0 +1,101 @@
+from django.core.mail import send_mail
+
+try:
+ ENV=open('/data/env.name').readline().strip()
+except Exception:
+ ENV='PROD+EXC'
+
+def _no_fail(method, *args, **kwargs):
+ def decorated(*args, **kwargs):
+ try:
+ return method(*args, **kwargs)
+ except Exception, e:
+ print e
+ return
+ return decorated
+
+
+
+@_no_fail
+def report_payment_data(account):
+ activity_report = 'An Account has entered payment data\n'
+ activity_report += '---------------------------\n'
+ activity_report += 'Plan: ' + account.package.name + '\n'
+ activity_report += 'Email: ' + account.user.email + '\n'
+ activity_report += 'API KEY: ' + account.apikey + ''
+
+ report_activity('Payment Data for ' + account.package.name + ' Account (' + account.user.email + ')', activity_report)
+
+@_no_fail
+def report_new_account(account):
+ activity_report = 'A new Account was created\n'
+ activity_report += '---------------------------\n'
+ activity_report += 'Plan: ' + account.package.name + '\n'
+ activity_report += 'Email: ' + account.user.email + '\n'
+ activity_report += 'API KEY: ' + account.apikey + ''
+
+ report_activity('New ' + account.package.name + ' Account (' + account.user.email + ')', activity_report)
+
+@_no_fail
+def report_new_index(index):
+ activity_report = 'A new Index was created\n'
+ activity_report += '---------------------------\n'
+ activity_report += 'Plan: ' + index.account.package.name + '\n'
+ activity_report += 'User Email: ' + index.account.user.email + '\n'
+ activity_report += 'Index Name: ' + index.name + ''
+
+ report_activity('Index activity (' + index.code + ')', activity_report, 'l')
+
+@_no_fail
+def report_new_deploy(deploy):
+ activity_report = 'A new Deploy is now controllable\n'
+ activity_report += '---------------------------\n'
+ activity_report += 'Plan: ' + deploy.index.account.package.name + '\n'
+ activity_report += 'User Email: ' + deploy.index.account.user.email + '\n'
+ activity_report += 'Index Name: ' + deploy.index.name + '\n'
+ activity_report += 'Worker: #' + str(deploy.worker.id) + '\n'
+ activity_report += ('Deploy: %r' % deploy) + '\n'
+ activity_report += ('Container Index: %r' % deploy.index) + '\n'
+
+ report_activity('Index activity (' + deploy.index.code + ')', activity_report, 'l')
+
+@_no_fail
+def report_delete_index(index):
+ activity_report = 'An Index has been deleted\n'
+ activity_report += '---------------------------\n'
+ activity_report += 'Plan: ' + index.account.package.name + '\n'
+ activity_report += 'User Email: ' + index.account.user.email + '\n'
+ activity_report += 'Index Name: ' + index.name + '\n'
+
+ report_activity('Index activity (' + index.code + ')', activity_report, 'l')
+
+@_no_fail
+def report_new_worker(worker):
+ activity_report = 'A new Worker was created\n'
+ activity_report += '---------------------------\n'
+ activity_report += repr(worker)
+
+ report_activity('New Worker (%d)' % (worker.pk), activity_report, 't')
+
+@_no_fail
+def report_automatic_redeploy(deploy, initial_xmx, new_xmx):
+ activity_report = 'Automatic redeploy.\n'
+ activity_report += '---------------------------\n'
+ activity_report += 'initial xmx value: %d\n' % (initial_xmx)
+ activity_report += 'new xmx value: %d\n' % (new_xmx)
+ activity_report += repr(deploy)
+
+ report_activity('Automatic redeploy', activity_report, 't')
+
+@_no_fail
+def report_activity(subject, body, type='b'):
+ if type == 'b':
+ mail_to = 'activity@indextank.com'
+ elif type == 't':
+ mail_to = 'activitytech@indextank.com'
+ elif type == 'l':
+ mail_to = 'lowactivity@indextank.com'
+ else:
+ raise Exception('Wrong report type')
+
+ send_mail(ENV + ' - ' + subject, body, 'IndexTank Activity ', [mail_to], fail_silently=False)
diff --git a/backoffice/lib/monitor.py b/backoffice/lib/monitor.py
new file mode 100644
index 0000000..3fe9818
--- /dev/null
+++ b/backoffice/lib/monitor.py
@@ -0,0 +1,148 @@
+
+from threading import Thread
+from traceback import format_tb
+import time, datetime
+import sys
+import shelve
+
+from django.core.mail import send_mail
+
+from lib import flaptor_logging
+
+try:
+ ENV=open('/data/env.name').readline().strip()
+except Exception:
+ ENV='PROD+EXC'
+
+#helper functions
+def is_prod():
+ return ENV == 'PROD' or ENV == 'QoS_Monitor'
+
+def env_name():
+ if ENV == 'PROD':
+ return 'PRODUCTION'
+ elif ENV == 'QoS_Monitor':
+ return 'QoS_Monitor'
+ else:
+ return ENV
+
+class Monitor(Thread):
+ def __init__(self, pagerduty_email='api-monitor@flaptor.pagerduty.com'):
+ super(Monitor, self).__init__()
+ self.name = self.__class__.__name__
+ self.statuses = shelve.open('/data/monitor-%s.shelf' % self.name)
+ self.logger = flaptor_logging.get_logger(self.name)
+ self.failure_threshold = 1
+ self.fatal_failure_threshold = 0
+ self.severity = 'WARNING'
+ self.title_template = '%s::%s: [%s] %s'
+ self.pagerduty_email = pagerduty_email
+
+ def iterable(self):
+ return [None]
+
+ def run(self):
+ self.step = 1
+ while True:
+ starttime = int(time.time())
+ try:
+ self.logger.info("running cycle %d", self.step)
+ for object in self.iterable():
+ self._monitor(object)
+ self.report_ok("unexpected error in monitor cycle")
+ self.clean()
+ except Exception:
+ self.logger.exception("Unexpected error while executing cycle")
+ self.report_bad("unexpected error in monitor cycle", 1, 0, 'UNEXPECTED ERROR IN THE CYCLE OF %s\n\n%s' % (self.name, self.describe_error()))
+ self.step += 1
+ self.statuses.sync()
+ time.sleep(max(0, self.period - (int(time.time()) - starttime)))
+
+ def clean(self):
+ for title, status in self.statuses.items():
+ if not status['working']:
+ if status['last_update'] != self.step:
+ self.report_ok(title)
+ else:
+ del self.statuses[title]
+
+
+ def _monitor(self, object):
+ try:
+ if self.monitor(object):
+ self.report_ok(str(self.alert_title(object)))
+ else:
+ self.report_bad(str(self.alert_title(object)), self.failure_threshold, self.fatal_failure_threshold, self.alert_msg(object))
+ self.report_ok("unexpected error in monitor")
+ except Exception, e:
+ self.logger.exception("Unexpected error while executing monitor. Exception is: %s" % (e))
+ message = 'UNEXPECTED ERROR IN THE MONITORING OF %s FOR TITLE: %s\n\n%s' % (self.name, self.alert_title(object), self.describe_error())
+ self.report_bad("unexpected error in monitor", 1, 'WARNING', message)
+
+ def describe_error(self):
+ exc_type, exc_value, exc_traceback = sys.exc_info()
+ return 'EXCEPTION: %s : %s\ntraceback:\n%s' % (exc_type, exc_value, ''.join(format_tb(exc_traceback)))
+
+ def update_status(self, key, **kwargs):
+ self.statuses[key] = kwargs
+
+ def send_alert(self, title, message, severity):
+ try:
+ if is_prod():
+ if severity == 'FATAL':
+ name = 'FATAL ALERT (%s)' % env_name()
+ else:
+ name = 'ALERT (%s)' % env_name()
+ else:
+ name = '%s test alert' % ENV
+
+ title = self.title_template % (ENV, self.name, severity, title)
+ message += '\n\n--------SENT AT ' + str(datetime.datetime.now())
+ to = ['alerts@indextank.com']
+ if severity == 'FATAL' and is_prod():
+ to.append('alerts+fatal@indextank.com')
+ to.append(self.pagerduty_email)
+ send_mail(title, message, '"%s" ' % name, to, fail_silently=False)
+ self.logger.info('Sending alert for title: %s\n============\n%s', title, message)
+ except Exception, e:
+ self.logger.exception("Unexpected error while sending alerts. Exception is: %s" % (e))
+
+ def report_ok(self, title):
+ if title in self.statuses and not self.statuses[title]['working'] and (self.statuses[title]['alerted'] or self.statuses[title]['alerted_fatal']):
+ # it has just been resolved
+ self.send_alert(title, 'The problem is no longer reported. The last message was:\n %s' % (self.statuses[title]['message']), self.severity)
+ if title in self.statuses:
+ del self.statuses[title]
+
+ def report_bad(self, title, threshold, fatal_threshold, message):
+ if title in self.statuses and not self.statuses[title]['working']:
+ # this object had already failed, let's grab the first step in which it failed
+ first_failure = self.statuses[title]['first_failure']
+ has_alerted = self.statuses[title]['alerted']
+ has_alerted_fatal = self.statuses[title]['alerted_fatal']
+ else:
+ # this object was fine, first failure is now
+ first_failure = self.step
+ has_alerted = False
+ has_alerted_fatal = False
+
+
+ should_alert = self.step - first_failure + 1 >= threshold
+ should_alert_fatal = fatal_threshold > 0 and self.step - first_failure + 1 >= fatal_threshold
+
+ if should_alert_fatal:
+ if not has_alerted_fatal:
+ has_alerted_fatal = True
+ if is_prod():
+ self.send_alert(title, 'A new problem on IndexTank has been detected:\n %s' % (message), 'FATAL')
+ else:
+ self.logger.info('Fatal error was found but alert has already been sent')
+ elif should_alert:
+ if not has_alerted:
+ has_alerted = True
+ self.send_alert(title, 'A new problem on IndexTank has been detected:\n %s' % (message), self.severity)
+ else:
+ self.logger.info('Error was found but alert has already been sent')
+
+ # save current state of the object (is_failed, message, first_failure, last_update)
+ self.update_status(title, working=False, last_update=self.step, message=message, first_failure=first_failure, alerted=has_alerted, alerted_fatal=has_alerted_fatal)
diff --git a/backoffice/logging.conf b/backoffice/logging.conf
new file mode 100644
index 0000000..c09a3b7
--- /dev/null
+++ b/backoffice/logging.conf
@@ -0,0 +1,35 @@
+[loggers]
+keys=root,rpc,boto
+
+[handlers]
+keys=consoleHandler
+
+[formatters]
+keys=simpleFormatter
+
+[logger_root]
+level=DEBUG
+handlers=consoleHandler
+
+[logger_rpc]
+level=INFO
+handlers=consoleHandler
+propagate=0
+qualname=RPC
+
+[logger_boto]
+level=INFO
+handlers=consoleHandler
+propagate=0
+qualname=boto
+
+[handler_consoleHandler]
+class=StreamHandler
+formatter=simpleFormatter
+args=(sys.stdout,)
+
+[formatter_simpleFormatter]
+format=%(pid)+5s %(asctime)s %(name)+8.8s:%(levelname)s%(prefix)s %(message)-90s %(suffix)s@%(filename)s:%(lineno)s
+datefmt=%d/%m-%H.%M.%S
+class=logging.SpecialFormatter
+
diff --git a/backoffice/manage.py b/backoffice/manage.py
new file mode 100644
index 0000000..5e78ea9
--- /dev/null
+++ b/backoffice/manage.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+from django.core.management import execute_manager
+try:
+ import settings # Assumed to be in the same directory.
+except ImportError:
+ import sys
+ sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+ sys.exit(1)
+
+if __name__ == "__main__":
+ execute_manager(settings)
diff --git a/backoffice/models.py b/backoffice/models.py
new file mode 100644
index 0000000..9c32fb1
--- /dev/null
+++ b/backoffice/models.py
@@ -0,0 +1 @@
+from api_linked_models import *
\ No newline at end of file
diff --git a/backoffice/rpc.py b/backoffice/rpc.py
new file mode 100644
index 0000000..e03a661
--- /dev/null
+++ b/backoffice/rpc.py
@@ -0,0 +1 @@
+from api_linked_rpc import * #@UnusedWildImport
diff --git a/backoffice/settings.py b/backoffice/settings.py
new file mode 100644
index 0000000..05ce244
--- /dev/null
+++ b/backoffice/settings.py
@@ -0,0 +1,86 @@
+# Django settings for RTS project.
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+ # ('Your Name', 'your_email@domain.com'),
+)
+
+MANAGERS = ADMINS
+
+DATABASE_ENGINE = 'mysql' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'ado_mssql'.
+DATABASE_NAME = 'indextank' # Or path to database file if using sqlite3.
+DATABASE_USER = '****' # Not used with sqlite3.
+DATABASE_PASSWORD = '****' # Not used with sqlite3.
+DATABASE_HOST = 'database' # Set to empty string for localhost. Not used with sqlite3.
+DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
+
+# Local time zone for this installation. Choices can be found here:
+# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
+# although not all choices may be available on all operating systems.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = 'Etc/GMT+0'
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'en-us'
+
+SITE_ID = 1
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+ 'django.template.loaders.filesystem.load_template_source',
+ 'django.template.loaders.app_directories.load_template_source',
+# 'django.template.loaders.eggs.load_template_source',
+)
+
+MIDDLEWARE_CLASSES = (
+ 'django.middleware.gzip.GZipMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.locale.LocaleMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+)
+
+ROOT_URLCONF = 'backoffice.urls'
+
+TEMPLATE_DIRS = (
+ 'templates'
+ # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
+ # Always use forward slashes, even on Windows.
+ # Don't forget to use absolute paths, not relative paths.
+)
+
+INSTALLED_APPS = (
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ 'django.contrib.admin',
+ 'django.contrib.humanize',
+ 'backoffice',
+)
+
+AUTH_PROFILE_MODULE = 'backoffice.PFUser'
+STATIC_URLS = [ '/_static' ]
+ALLOWED_INCLUDE_ROOTS = ('static')
+LOGIN_URL = '/login'
+
+#PROCS_DIR = '../indexengine/'
+
+USER_COOKIE_NAME = "pf_user"
+COMMON_DOMAIN = 'localhost'
+#SESSION_COOKIE_DOMAIN = COMMON_DOMAIN
+FORCE_SCRIPT_NAME = ''
+USE_MULTITHREADED_SERVER = True
+LOGGER_CONFIG_FILE='logging.conf'
+
+# 0.001 MB per doc
+INDEX_SIZE_RATIO = 0.002
+
+EMAIL_HOST='localhost'
+EMAIL_PORT=25
+EMAIL_HOST_USER='user%localhost'
+EMAIL_HOST_PASSWORD='****'
diff --git a/backoffice/start_webapp.sh b/backoffice/start_webapp.sh
new file mode 100755
index 0000000..93209ff
--- /dev/null
+++ b/backoffice/start_webapp.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+LOGFILE='/data/logs/backoffice.log'
+nohup python manage.py runfcgi method=prefork maxchildren=30 host=127.0.0.1 port=4200 pidfile=pid workdir="$PWD" outlog="$LOGFILE" errlog="$LOGFILE" >> $LOGFILE 2>&1
diff --git a/backoffice/static/biz_stats.css b/backoffice/static/biz_stats.css
new file mode 100644
index 0000000..da6fce7
--- /dev/null
+++ b/backoffice/static/biz_stats.css
@@ -0,0 +1,30 @@
+body {
+ margin: 0;
+ padding-top: 2em;
+ padding-left: 2em;
+}
+div.h {
+ visibility: hidden;
+ position: absolute;
+ top: 3em;
+ left: 2em;
+}
+.ss8pt {
+}
+.hss8pt {
+ font-family: sans-serif;
+ font-size: 8pt;
+}
+.tss8pt {
+ font-family: sans-serif;
+ font-size: 8pt;
+ border-left: 1px solid white;
+ border-right: 1px solid silver;
+}
+
+.bs_header {
+}
+
+.bs_right {
+ text-align: right;
+}
diff --git a/backoffice/static/biz_stats.js b/backoffice/static/biz_stats.js
new file mode 100644
index 0000000..4eeef91
--- /dev/null
+++ b/backoffice/static/biz_stats.js
@@ -0,0 +1,1001 @@
+google.load("jquery", "1.4.4");
+google.load("visualization", "1", {packages: ["table", "annotatedtimeline", "corechart"]});
+
+const MS = ["01","02","03","04","05","06","07","08","09","10"];
+
+function dateFormat(d) {
+ var dd = d.getUTCDate();
+ var mm = d.getUTCMonth();
+ var ms = (mm < 10)? MS[mm]:mm;
+ var yy = d.getUTCFullYear();
+ if(dd < 10) dd = MS[dd-1];
+ return yy+"-"+ms+"-"+dd;
+}
+
+var rawJsonData = null;
+
+var activityTable = null;
+var activityData = null;
+var activityDataReset = null;
+var activitySetup = {
+ allowHtml: true,
+ showRowNumber: true,
+ width: 1000,
+ sortColumn: 0,
+ sortAscending: false,
+ cssClassNames: {tableCell: "tss8pt", headerCell: "hss8pt"}
+};
+
+const activityDataColumnIndexPackage = 4;
+const activityDataColumnIndexCount = 5;
+
+const historyDataColumnIndexPackage = 3;
+
+var historyTable = null;
+var historyData = null;
+var historyDataReset = null;
+var historySetup = {
+ allowHtml: true,
+ showRowNumber: true,
+ width: 1000,
+ sortColumn: 7,
+ sortAscending: false,
+ cssClassNames: {tableCell: "tss8pt", headerCell: "hss8pt"}
+};
+//,cssClassNames: {tableCell: "ss8pt", headerCell: "ss8pt", headerRow: "bs_header"}
+
+function historyTableHideDataColumns(columns) {
+// historyTable.hideDataColumns(0);
+// historyData.removeColumn(0);
+
+// historyTable.draw(historyData, historySetup);
+}
+
+function historyTablePackageFilter(packageNames) {
+ var rows = historyData.getNumberOfRows();
+ var toBeRemoved = [];
+ for(var i = 0; i < rows; i++) {
+ var value = historyData.getValue(i, historyDataColumnIndexPackage);
+ for(var j = 0; j < packageNames.length; j++) {
+ var packageName = packageNames[j];
+ if(packageName == value) {
+ toBeRemoved.push(i);
+ }
+ }
+ }
+ for(var i = toBeRemoved.length-1; i >= 0; i--) {
+ historyData.removeRow(toBeRemoved[i]);
+ }
+ historyTable.draw(historyData, historySetup);
+}
+
+function historyTablePackageFilterOnly(packageNames) {
+ var rows = historyData.getNumberOfRows();
+ var toBeRemoved = [];
+ for(var i = 0; i < rows; i++) {
+ var value = historyData.getValue(i, historyDataColumnIndexPackage);
+ for(var j = 0; j < packageNames.length; j++) {
+ var packageName = packageNames[j];
+ if(packageName != value) {
+ toBeRemoved.push(i);
+ }
+ }
+ }
+ for(var i = toBeRemoved.length-1; i >= 0; i--) {
+ historyData.removeRow(toBeRemoved[i]);
+ }
+ historyTable.draw(historyData, historySetup);
+}
+
+function activityTableCountFilter() {
+ var rows = activityData.getNumberOfRows();
+ var toBeRemoved = [];
+ for(var i = 0; i < rows; i++) {
+ var value = activityData.getValue(i, activityDataColumnIndexCount);
+ if(value <= 0) {
+ toBeRemoved.push(i);
+ }
+ }
+ for(var i = toBeRemoved.length-1; i >= 0; i--) {
+ activityData.removeRow(toBeRemoved[i]);
+ }
+ activityTable.draw(activityData, activitySetup);
+}
+
+function activityTablePackageFilter(packageNames) {
+ var rows = activityData.getNumberOfRows();
+ var toBeRemoved = [];
+ for(var i = 0; i < rows; i++) {
+ var value = activityData.getValue(i, activityDataColumnIndexPackage);
+ for(var j = 0; j < packageNames.length; j++) {
+ var packageName = packageNames[j];
+ if(packageName == value) {
+ toBeRemoved.push(i);
+ }
+ }
+ }
+ for(var i = toBeRemoved.length-1; i >= 0; i--) {
+ activityData.removeRow(toBeRemoved[i]);
+ }
+ activityTable.draw(activityData, activitySetup);
+}
+
+function activityTablePackageFilterOnly(packageNames) {
+ var rows = activityData.getNumberOfRows();
+ var toBeRemoved = [];
+ for(var i = 0; i < rows; i++) {
+ var value = activityData.getValue(i, activityDataColumnIndexPackage);
+ for(var j = 0; j < packageNames.length; j++) {
+ var packageName = packageNames[j];
+ if(packageName != value) {
+ toBeRemoved.push(i);
+ }
+ }
+ }
+ for(var i = toBeRemoved.length-1; i >= 0; i--) {
+ activityData.removeRow(toBeRemoved[i]);
+ }
+ activityTable.draw(activityData, activitySetup);
+}
+
+function activityTableReset() {
+ activityData = activityDataReset;
+ activityDataReset = activityData.clone();
+ activityTable.draw(activityData, activitySetup);
+}
+
+google.setOnLoadCallback(function() {
+ $(function() {
+
+ function descriptiveLogStats(logStats) {
+ return "