diff -Nru txaws-0.2/debian/changelog txaws-0.2.3/debian/changelog --- txaws-0.2/debian/changelog 2012-03-28 09:39:53.000000000 +0000 +++ txaws-0.2.3/debian/changelog 2012-08-07 00:17:13.000000000 +0000 @@ -1,102 +1,20 @@ -txaws (0.2-0ubuntu10) precise; urgency=low +txaws (0.2.3-1ubuntu1~ubuntu12.04.1) precise-backports; urgency=low - * d/patches/add-ssl-cert-verification.patch: Cherry pick patch from - upstream to enable SSL certificate verification. (LP: #781949) + * No-change backport to precise (LP: #945176) - -- Clint Byrum Wed, 28 Mar 2012 02:39:34 -0700 + -- Micah Gersten Mon, 06 Aug 2012 19:17:13 -0500 -txaws (0.2-0ubuntu9) precise; urgency=low +txaws (0.2.3-1ubuntu1) quantal; urgency=low + * Synchronize with Debian. Remaining changes: * d/patches/aws-status-add-appindicator.patch: Add support for - appindicator so GUI is visible and useful in Unity. (LP: #802950) - * d/rules,d/python-txaws.manpages,d/manpages: Generate basic manpages - using help2man. (LP: #964164) - * d/aws-status.install: install new basic manpage and add desktop file. + appindicator so GUI is visible and useful in Unity. + * debian/control: Depend on python-appindicator - -- Clint Byrum Sat, 24 Mar 2012 16:05:26 -0700 + -- Jeremy Bicha Sat, 23 Jun 2012 01:55:58 -0400 -txaws (0.2-0ubuntu8) precise; urgency=low +txaws (0.2.3-1) unstable; urgency=low - * d/patches/drop-epsilon.patch: Removes epsilon dependency in - and switches to dateutil, which is already in main. (LP: #856067) - * d/patches/drop-pytz.patch: Removes pytz dependency and - switches to dateutil. (LP: #912589) - * d/patches/drop-zope-datetime.patch: Removes zope.datetime - dependency and switches to dateutil. (LP: #912607) + * Initial Packaging (Closes: #674450) - -- Clint Byrum Fri, 06 Jan 2012 09:37:54 -0800 - -txaws (0.2-0ubuntu7) precise; urgency=low - - * d/rules: fail package build if test suite fails - * d/control: add build-deps to run test suite, and - missing deps. - - -- Clint Byrum Thu, 05 Jan 2012 13:18:52 -0800 - -txaws (0.2-0ubuntu6) precise; urgency=low - - * Rebuild to drop python2.6 dependencies. - - -- Matthias Klose Sat, 31 Dec 2011 02:14:56 +0000 - -txaws (0.2-0ubuntu5) oneiric; urgency=low - - * debian/patches/fix-openstack-terminate-instances: Make sure txaws - reliably returns from terminate_instances when talking to Nova. - (LP: #862595) - - -- Clint Byrum Thu, 29 Sep 2011 18:26:35 -0700 - -txaws (0.2-0ubuntu4) oneiric; urgency=low - - * debian/patches/fix-s3-fixes.patch: Fix regression introduced by - previous patches. (LP: #856749) - - -- Clint Byrum Thu, 22 Sep 2011 16:05:56 -0700 - -txaws (0.2-0ubuntu3) oneiric; urgency=low - - * debian/patches/fix-handling-nova-securitygroups.patch, - debian/patches/fix-s3-alternate-port.patch: Fix txaws compatibility - with OpenStack Nova. (LP: #829609 , LP: #824403) - - -- Clint Byrum Thu, 15 Sep 2011 13:13:16 -0700 - -txaws (0.2-0ubuntu2) oneiric; urgency=low - - * dh_python2 transition - * removed debian/pycompat no longer needed. - * debian/rules - - added rule to remove egg-info during clean - * debian/control - - changed XS-P-V to X-P-V - - increased min python version to >= 2.6.6-3~. - - increased min debhelper to >= 7.0.50. - - removed XB-P-V no longer needed. - - -- Charlie Smotherman Sat, 18 Jun 2011 17:53:43 -0500 - -txaws (0.2-0ubuntu1) oneiric; urgency=low - - * New upstream release. - * Dropping python-support/pycentral for dh_python2 - * Dropping unused cdbs build-dependency. - * Updating debian/copyright to latest DEP5 and adding new Copyright info. - - -- Clint Byrum Wed, 15 Jun 2011 11:15:23 -0700 - -txaws (0.0.1-0ubuntu2) natty; urgency=low - - * debian/control: - - aws-status depends on python-txaws (LP: #634022) - * Switch to dpkg-source format 3.0 (quilt) - * Bump Standards-Version to 3.9.1 (no changes) - - -- Angel Abad Sun, 13 Feb 2011 15:53:21 +0100 - -txaws (0.0.1-0ubuntu1) lucid; urgency=low - - * Initial Package (LP: #521121) - - -- Scott Moser Fri, 12 Feb 2010 14:03:06 -0500 + -- Clint Byrum Thu, 24 May 2012 16:39:07 -0700 diff -Nru txaws-0.2/debian/control txaws-0.2.3/debian/control --- txaws-0.2/debian/control 2012-03-28 05:36:57.000000000 +0000 +++ txaws-0.2.3/debian/control 2012-06-23 05:56:21.000000000 +0000 @@ -3,9 +3,13 @@ Priority: optional X-Python-Version: >= 2.5 Maintainer: Ubuntu Developers +XSBC-Original-Maintainer: Debian Python Modules Team +Uploaders: Clint Byrum Build-Depends: debhelper (>= 7.0.50), python (>= 2.6.6-3~), python-twisted, python-dateutil, help2man, ca-certificates Homepage: https://launchpad.net/txaws -Standards-Version: 3.9.1 +Standards-Version: 3.9.3 +Vcs-Svn: svn://svn.debian.org/python-modules/packages/txaws/trunk/ +Vcs-Browser: http://svn.debian.org/viewsvn/python-modules/packages/txaws/trunk/ Package: aws-status Architecture: all diff -Nru txaws-0.2/debian/copyright txaws-0.2.3/debian/copyright --- txaws-0.2/debian/copyright 2011-10-12 15:59:20.000000000 +0000 +++ txaws-0.2.3/debian/copyright 2012-06-23 05:56:21.000000000 +0000 @@ -1,10 +1,9 @@ -Format-Specification: http://anonscm.debian.org/viewvc/dep/web/deps/dep5.mdwn?revision=174&view=co&pathrev=174 +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: txaws Source: https://launchpad.net/txaws Comment: This package was debianized by Scott Moser on Fri, 12 Feb 2010 13:57:58 -0500. - -Upstream-Contact: Duncan McGreggor +Upstream-Contact: Duncan McGreggor Files: * Copyright: 2008 Tristan Seligmann diff -Nru txaws-0.2/debian/patches/add-ssl-cert-verification.patch txaws-0.2.3/debian/patches/add-ssl-cert-verification.patch --- txaws-0.2/debian/patches/add-ssl-cert-verification.patch 2012-03-28 05:29:48.000000000 +0000 +++ txaws-0.2.3/debian/patches/add-ssl-cert-verification.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,500 +0,0 @@ -Author: Thomas Herve -Bug: http://pad.lv/781949 -Description: Cherry picked patch from upstream trunk to enable ssl - certificate verification. -Origin: https://code.launchpad.net/~therve/txaws/ssl-verify/+merge/83740 - -=== modified file 'txaws/client/base.py' ---- a/txaws/client/base.py 2011-04-21 21:16:37 +0000 -+++ b/txaws/client/base.py 2011-11-29 18:51:38 +0000 -@@ -3,7 +3,7 @@ - except ImportError: - from xml.parsers.expat import ExpatError as ParseError - --from twisted.internet import reactor, ssl -+from twisted.internet.ssl import ClientContextFactory - from twisted.web import http - from twisted.web.client import HTTPClientFactory - from twisted.web.error import Error as TwistedWebError -@@ -12,6 +12,7 @@ - from txaws.credentials import AWSCredentials - from txaws.exception import AWSResponseParseError - from txaws.service import AWSServiceEndpoint -+from txaws.client.ssl import VerifyingContextFactory - - - def error_wrapper(error, errorClass): -@@ -73,13 +74,16 @@ - - class BaseQuery(object): - -- def __init__(self, action=None, creds=None, endpoint=None): -+ def __init__(self, action=None, creds=None, endpoint=None, reactor=None): - if not action: - raise TypeError("The query requires an action parameter.") - self.factory = HTTPClientFactory - self.action = action - self.creds = creds - self.endpoint = endpoint -+ if reactor is None: -+ from twisted.internet import reactor -+ self.reactor = reactor - self.client = None - - def get_page(self, url, *args, **kwds): -@@ -92,11 +96,14 @@ - contextFactory = None - scheme, host, port, path = parse(url) - self.client = self.factory(url, *args, **kwds) -- if scheme == 'https': -- contextFactory = ssl.ClientContextFactory() -- reactor.connectSSL(host, port, self.client, contextFactory) -+ if scheme == "https": -+ if self.endpoint.ssl_hostname_verification: -+ contextFactory = VerifyingContextFactory(host) -+ else: -+ contextFactory = ClientContextFactory() -+ self.reactor.connectSSL(host, port, self.client, contextFactory) - else: -- reactor.connectTCP(host, port, self.client) -+ self.reactor.connectTCP(host, port, self.client) - return self.client.deferred - - def get_request_headers(self, *args, **kwds): - -=== added file 'txaws/client/ssl.py' ---- a/txaws/client/ssl.py 1970-01-01 00:00:00 +0000 -+++ b/txaws/client/ssl.py 2011-11-29 18:51:38 +0000 -@@ -0,0 +1,100 @@ -+from glob import glob -+import os -+import re -+ -+from OpenSSL import SSL -+from OpenSSL.crypto import load_certificate, FILETYPE_PEM -+ -+from twisted.internet.ssl import CertificateOptions -+ -+ -+__all__ = ["VerifyingContextFactory", "get_ca_certs"] -+ -+ -+class VerifyingContextFactory(CertificateOptions): -+ """ -+ A SSL context factory to pass to C{connectSSL} to check for hostname -+ validity. -+ """ -+ -+ def __init__(self, host, caCerts=None): -+ if caCerts is None: -+ caCerts = get_global_ca_certs() -+ CertificateOptions.__init__(self, verify=True, caCerts=caCerts) -+ self.host = host -+ -+ def _dnsname_match(self, dn, host): -+ pats = [] -+ for frag in dn.split(r"."): -+ if frag == "*": -+ pats.append("[^.]+") -+ else: -+ frag = re.escape(frag) -+ pats.append(frag.replace(r"\*", "[^.]*")) -+ -+ rx = re.compile(r"\A" + r"\.".join(pats) + r"\Z", re.IGNORECASE) -+ return bool(rx.match(host)) -+ -+ def verify_callback(self, connection, x509, errno, depth, preverifyOK): -+ # Only check depth == 0 on chained certificates. -+ if depth == 0: -+ dns_found = False -+ if getattr(x509, "get_extension", None) is not None: -+ for index in range(x509.get_extension_count()): -+ extension = x509.get_extension(index) -+ if extension.get_short_name() != "subjectAltName": -+ continue -+ data = str(extension) -+ for element in data.split(", "): -+ key, value = element.split(":") -+ if key != "DNS": -+ continue -+ if self._dnsname_match(value, self.host): -+ return preverifyOK -+ dns_found = True -+ break -+ if not dns_found: -+ commonName = x509.get_subject().commonName -+ if commonName is None: -+ return False -+ if not self._dnsname_match(commonName, self.host): -+ return False -+ else: -+ return False -+ return preverifyOK -+ -+ def _makeContext(self): -+ context = CertificateOptions._makeContext(self) -+ context.set_verify( -+ SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT, -+ self.verify_callback) -+ return context -+ -+ -+def get_ca_certs(files="/etc/ssl/certs/*.pem"): -+ """Retrieve a list of CAs pointed by C{files}.""" -+ certificateAuthorityMap = {} -+ for certFileName in glob(files): -+ # There might be some dead symlinks in there, so let's make sure it's -+ # real. -+ if not os.path.exists(certFileName): -+ continue -+ certFile = open(certFileName) -+ data = certFile.read() -+ certFile.close() -+ x509 = load_certificate(FILETYPE_PEM, data) -+ digest = x509.digest("sha1") -+ # Now, de-duplicate in case the same cert has multiple names. -+ certificateAuthorityMap[digest] = x509 -+ return certificateAuthorityMap.values() -+ -+ -+_ca_certs = None -+ -+ -+def get_global_ca_certs(): -+ """Retrieve a singleton of CA certificates.""" -+ global _ca_certs -+ if _ca_certs is None: -+ _ca_certs = get_ca_certs() -+ return _ca_certs - -=== added file 'txaws/client/tests/badprivate.ssl' ---- a/txaws/client/tests/badprivate.ssl 1970-01-01 00:00:00 +0000 -+++ b/txaws/client/tests/badprivate.ssl 2011-11-29 18:51:38 +0000 -@@ -0,0 +1,15 @@ -+-----BEGIN RSA PRIVATE KEY----- -+MIICXgIBAAKBgQDGYFWP2Ine2OFIPjX+Tu+S403KW63EWq/I1DYXiezLoUpYPed3 -+0tAkAXH1gOwQZbARFlUn0LgvXDSpuQLvgKQZwP/e1D8SvZUZ6nexW+aYlPE9kjd1 -+dhK1xpe1h5y09AjCz02xxzcFzrJrJ47uU7vV+FGArE8FFh3hO+dz0/PmZQIDAQAB -+AoGBAKfv+983yJfgcO9QwzLULlrilQNfk36r6y6QAG7y84T7uU10spSs4kno80mL -+58yF2YTNrC91scdePrMEDikldUVcCqtPYcZKHyw5+4aGaDDO244tznexOQnQcNIe -+2BbLFuh+jmJpoFIY/H7EsLQQzn6+6dGPnYGBQfiyitWfAXRNAkEA/ShQkYCRAHgq -+g6WBIYsw/ISQydhiMiKrL2ZUXERT+pWU9MoSdMskgyMi3S7wzwJQXkrHA36q8QkL -++H8n5K+f5wJBAMiajfEtv0wRW0awX40qJtuqW3cSKeGHBH9mMObcRJd5OcK6giC/ -+Cc5st/ZcuE/8i4r44DfeC+cwY6QdIqI8rdMCQQCKuq78LWJIyZEyt12+ThK4LsVR -+d1zIcKsyvHb6YQ9MQPBx/NKEYlZN7tFKOFEKgBAevAe3aJCwqe5/bN8luQB9AkEA -+uQVD8bR+AgzoIPS/zJWaLXSc09/e3PIJBfAdHnD+mq7mxWH8b3OD+e5wZjvyi2Ok -+2NLfCug0FlGdNVrh/Lz2nQJATdcNvHNzJcWOHe05lo+xAqkjz73FWGpPNrdXRigG -+YnjIsZVy4k48xIxPhT2rC44yo1iPEP5EnHCE2bLyUlTAYA== -+-----END RSA PRIVATE KEY----- - -=== added file 'txaws/client/tests/badpublic.ssl' ---- a/txaws/client/tests/badpublic.ssl 1970-01-01 00:00:00 +0000 -+++ b/txaws/client/tests/badpublic.ssl 2011-11-29 18:51:38 +0000 -@@ -0,0 +1,23 @@ -+-----BEGIN CERTIFICATE----- -+MIIDzjCCAzegAwIBAgIJANqT3vXxSVFjMA0GCSqGSIb3DQEBBQUAMIGhMQswCQYD -+VQQGEwJCUjEPMA0GA1UECBMGUGFyYW5hMREwDwYDVQQHEwhDdXJpdGliYTEhMB8G -+A1UEChMYRmFrZSBMYW5kc2NhcGUgKFRlc3RpbmcpMREwDwYDVQQLEwhTZWN1cml0 -+eTESMBAGA1UEAxMJbG9jYWxob3N0MSQwIgYJKoZIhvcNAQkBFhVhbmRyZWFzQGNh -+bm9uaWNhbC5jb20wHhcNMDkwMTA5MTUyNTAwWhcNMTkwMTA3MTUyNTAwWjCBoTEL -+MAkGA1UEBhMCQlIxDzANBgNVBAgTBlBhcmFuYTERMA8GA1UEBxMIQ3VyaXRpYmEx -+ITAfBgNVBAoTGEZha2UgTGFuZHNjYXBlIChUZXN0aW5nKTERMA8GA1UECxMIU2Vj -+dXJpdHkxEjAQBgNVBAMTCWxvY2FsaG9zdDEkMCIGCSqGSIb3DQEJARYVYW5kcmVh -+c0BjYW5vbmljYWwuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDGYFWP -+2Ine2OFIPjX+Tu+S403KW63EWq/I1DYXiezLoUpYPed30tAkAXH1gOwQZbARFlUn -+0LgvXDSpuQLvgKQZwP/e1D8SvZUZ6nexW+aYlPE9kjd1dhK1xpe1h5y09AjCz02x -+xzcFzrJrJ47uU7vV+FGArE8FFh3hO+dz0/PmZQIDAQABo4IBCjCCAQYwHQYDVR0O -+BBYEFF4A8+YHCLAt19OtWTjIjBKzLUokMIHWBgNVHSMEgc4wgcuAFF4A8+YHCLAt -+19OtWTjIjBKzLUokoYGnpIGkMIGhMQswCQYDVQQGEwJCUjEPMA0GA1UECBMGUGFy -+YW5hMREwDwYDVQQHEwhDdXJpdGliYTEhMB8GA1UEChMYRmFrZSBMYW5kc2NhcGUg -+KFRlc3RpbmcpMREwDwYDVQQLEwhTZWN1cml0eTESMBAGA1UEAxMJbG9jYWxob3N0 -+MSQwIgYJKoZIhvcNAQkBFhVhbmRyZWFzQGNhbm9uaWNhbC5jb22CCQDak9718UlR -+YzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GBABszkA3CCzt+nTOX+A7/ -+I98DvI0W1Ss0J+Tq+diLr+kw6Z5ZTj5hrIS/x6XhVHjpim4724UBXA0Sels4JXbw -+hhJovuncExce316gAol/9eEzTffZ9mt1jZQy9LL7IAENiobnsj2F65zNaJzXp5UC -+rE/h/xIxz9rAmXtVOWHqZLcw -+-----END CERTIFICATE----- - -=== added file 'txaws/client/tests/private.ssl' ---- a/txaws/client/tests/private.ssl 1970-01-01 00:00:00 +0000 -+++ b/txaws/client/tests/private.ssl 2011-11-29 18:51:38 +0000 -@@ -0,0 +1,15 @@ -+-----BEGIN RSA PRIVATE KEY----- -+MIICWwIBAAKBgQDX2VNEDZHtl5nimNocshar8pBmjqiGn9olCR2LcKifuJY4bFTg -+qib+Rr3v2DwDTbOMaquRSxFgwLJLCug3WclsGrYSPIsFCx+k3XhqM61JXEwrKuIp -+Js893XHkeg3SEFua/oVfDxNfJttoHW3FbsnDx5964kYwGExjJcH73GInUQIDAQAB -+AoGASiM9NEys6Lx/gJMbp2uL2fdwnak2PTc+iCX/XduOL34JKswawyfuSLwnlO/i -+fQf9OaeR0k/EYkUNeDUA2bIfOj6wWS8tamnX4fxL7A20y5VyqMMah8mcerZgtPdS -+7ZtYCbeijWSKpHgjALc2Hym7R68WZI+IHe0DQkcW6WxOMFkCQQD2jqHZn/Qtd62u -+mWVwIx6G7+Po5vzd86KyWWftdUtVCY9DmiX1rmWXbJhLnmaKCLkmHxyBvw7Biarr -+ZnCAafebAkEA4B2dSpLi7bAzjCa7JBtzV9kr1FVZOl2vA+9BqTAjCQu0b9VDEm8V -+x0061Z8rN7Og3ECGtKH/r3/4RnHUPpwJgwJAdyZQkvHYt4xJc8IPolRmcUFGu4u9 -+Eammq1fHgJqZcBvxjvLUe1jvIXFKW+jNltFGYGTSiuUAxYi4/49+uJ/9FwJAGBB1 -+/DTrcvQxhMH/5C+iYfNqtmD3tMGscjK1jTIjAOyl0kBG9GrDHuRXBesSW+fIxP2U -+uT6P0std4EqGrLZaewJAHT0n/3tXnsPjj+BMlC4ZkRKgPJ4I7zTU1XSlLY5zbMoV -+NvtHLlq7ttiarsH95xyge69uV1/zJVj/IiS71YY9PQ== -+-----END RSA PRIVATE KEY----- - -=== added file 'txaws/client/tests/private_san.ssl' ---- a/txaws/client/tests/private_san.ssl 1970-01-01 00:00:00 +0000 -+++ b/txaws/client/tests/private_san.ssl 2011-11-29 18:51:38 +0000 -@@ -0,0 +1,16 @@ -+-----BEGIN PRIVATE KEY----- -+MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAMzOcwF0HIQcenxS -+RrKT7fOdJYZVPzvGuDiwUpp6WNuyAJ6JVCP83SzTp3yXmKfDFO/m00WdKxF28W3s -+Q/NrVvQQCzfh2NhDtxom7p9JOVW9j7NkO4SFOLHh7JxbLT1j0MGTJX0bxJkro78v -+wB/OAN4/eiQdli2y6D6skqdb9QXHAgMBAAECgYAvSP7+d+tZiSWybGCMPGE03LRc -+NnRZ/cBsvjDkH5lCZ++Cqtw1Tt1VyywhNPL20LCVzuo6aVYXOyn0ohbyLXcuinpE -+rVopV9nUPr0EFOo+yccDNNPQJ2tevlYfEsS6afcfLcUinRUSvVojHDrODADduLR8 -+uA3Le95tChcVwe6NuQJBAOvdRTG858BB9zJdjyd4QoqTA1k0rs+VC3svVUT9l16+ -+gZLZ75wTLbtrkGRN/iiAVPemgqQYmNvuXtUByO3QFmUCQQDeSmt+z2dNCx78mUWQ -+HFcyJP0g2gz/IEnxx/9Rin/9xSo+ycuNvbSwphSHxYl20wVFA72vp/zuOWO3WaXr -+umK7AkB6pDJfe2dRu7sqcCWIk2qeHXVHRDKFc21l3yXKWsYDmLFNR47kq8BCzNpm -+nXtDWf9USjtx0exhp1+eCHCO331VAkALAMwJXuLSIXbLMhsLYxu9067j7WcvSb3f -+RfMRajWjrhrFON/miDlldRMXFWQUiaV9IQ5Gn54ZfKW+8aUQ4gz5AkB+yOVkouwj -+QVngotLjasbgvE8WugbweLInHN1W2ucsLKSpSADoE/djQ5NnwuolmnrhpQT5BWcQ -+j3o7Gf/nXS+r -+-----END PRIVATE KEY----- - -=== added file 'txaws/client/tests/public.ssl' ---- a/txaws/client/tests/public.ssl 1970-01-01 00:00:00 +0000 -+++ b/txaws/client/tests/public.ssl 2011-11-29 18:51:38 +0000 -@@ -0,0 +1,22 @@ -+-----BEGIN CERTIFICATE----- -+MIIDnDCCAwWgAwIBAgIJALPjWsknBC15MA0GCSqGSIb3DQEBBQUAMIGRMQswCQYD -+VQQGEwJCUjEPMA0GA1UECBMGUGFyYW5hMREwDwYDVQQHEwhDdXJpdGliYTESMBAG -+A1UEChMJTGFuZHNjYXBlMRAwDgYDVQQLEwdUZXN0aW5nMRIwEAYDVQQDEwlsb2Nh -+bGhvc3QxJDAiBgkqhkiG9w0BCQEWFWFuZHJlYXNAY2Fub25pY2FsLmNvbTAeFw0w -+OTAxMDgxNjQxMzlaFw0xOTAxMDYxNjQxMzlaMIGRMQswCQYDVQQGEwJCUjEPMA0G -+A1UECBMGUGFyYW5hMREwDwYDVQQHEwhDdXJpdGliYTESMBAGA1UEChMJTGFuZHNj -+YXBlMRAwDgYDVQQLEwdUZXN0aW5nMRIwEAYDVQQDEwlsb2NhbGhvc3QxJDAiBgkq -+hkiG9w0BCQEWFWFuZHJlYXNAY2Fub25pY2FsLmNvbTCBnzANBgkqhkiG9w0BAQEF -+AAOBjQAwgYkCgYEA19lTRA2R7ZeZ4pjaHLIWq/KQZo6ohp/aJQkdi3Con7iWOGxU -+4Kom/ka979g8A02zjGqrkUsRYMCySwroN1nJbBq2EjyLBQsfpN14ajOtSVxMKyri -+KSbPPd1x5HoN0hBbmv6FXw8TXybbaB1txW7Jw8efeuJGMBhMYyXB+9xiJ1ECAwEA -+AaOB+TCB9jAdBgNVHQ4EFgQU3eUz2XxK1J/oavkn/hAvYfGOZM0wgcYGA1UdIwSB -+vjCBu4AU3eUz2XxK1J/oavkn/hAvYfGOZM2hgZekgZQwgZExCzAJBgNVBAYTAkJS -+MQ8wDQYDVQQIEwZQYXJhbmExETAPBgNVBAcTCEN1cml0aWJhMRIwEAYDVQQKEwlM -+YW5kc2NhcGUxEDAOBgNVBAsTB1Rlc3RpbmcxEjAQBgNVBAMTCWxvY2FsaG9zdDEk -+MCIGCSqGSIb3DQEJARYVYW5kcmVhc0BjYW5vbmljYWwuY29tggkAs+NayScELXkw -+DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOBgQBZQcqhHAsasX3WtCXlIKqH -+hE4ZdsvtPHOnoWPxxN4CZEyu2YJ2PMXCkA7yISNokAZgkOpkYGPWwV3CwNCw032u -++ngwIo2sxx7ag8tVrYkIda717oBw7opDMVrjTNhZdak7s+hg+s9ZDPUMMcbJFtlN -+lmayn/uZSyog4Y+yriB1tQ== -+-----END CERTIFICATE----- - -=== added file 'txaws/client/tests/public_san.ssl' ---- a/txaws/client/tests/public_san.ssl 1970-01-01 00:00:00 +0000 -+++ b/txaws/client/tests/public_san.ssl 2011-11-29 18:51:38 +0000 -@@ -0,0 +1,12 @@ -+-----BEGIN CERTIFICATE----- -+MIIB1jCCAT+gAwIBAgIJAMG1W/zdYglWMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV -+BAMTCWxvY2FsaG9zdDAeFw0xMTExMjkxODIxNTdaFw0yMTExMjYxODIxNTdaMBQx -+EjAQBgNVBAMTCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA -+zM5zAXQchBx6fFJGspPt850lhlU/O8a4OLBSmnpY27IAnolUI/zdLNOnfJeYp8MU -+7+bTRZ0rEXbxbexD82tW9BALN+HY2EO3Gibun0k5Vb2Ps2Q7hIU4seHsnFstPWPQ -+wZMlfRvEmSujvy/AH84A3j96JB2WLbLoPqySp1v1BccCAwEAAaMwMC4wLAYDVR0R -+BCUwI4IJMTI3LjAuMC4xgglsb2NhbGhvc3SCCzE5Mi4xNjguMC4xMA0GCSqGSIb3 -+DQEBBQUAA4GBACgPQt3A+kq8Jus+vCIvhbKjU6HaId5gHvRhvM+SnBb/K8llDInh -+vS2bpVasSprTbPQjnqh6vVEj0jB/52p8pliZ5Q0pnaEZRYJtUnyeQCz8mwS16h5o -+KiKfQclPKkM0p0wiQPz1sxju7bbYRm2PoCvoNl08c5RhstKSwF9XmuTx -+-----END CERTIFICATE----- - -=== modified file 'txaws/client/tests/test_client.py' ---- a/txaws/client/tests/test_client.py 2011-04-21 21:16:37 +0000 -+++ b/txaws/client/tests/test_client.py 2011-11-29 18:51:38 +0000 -@@ -1,6 +1,11 @@ - import os - -+from OpenSSL.crypto import load_certificate, FILETYPE_PEM -+from OpenSSL.SSL import Error as SSLError -+from OpenSSL.version import __version__ as pyopenssl_version -+ - from twisted.internet import reactor -+from twisted.internet.ssl import DefaultOpenSSLContextFactory - from twisted.internet.error import ConnectionRefusedError - from twisted.protocols.policies import WrappingFactory - from twisted.python import log -@@ -11,9 +16,23 @@ - from twisted.web.error import Error as TwistedWebError - - from txaws.client.base import BaseClient, BaseQuery, error_wrapper -+from txaws.client.ssl import VerifyingContextFactory -+from txaws.service import AWSServiceEndpoint - from txaws.testing.base import TXAWSTestCase - - -+def sibpath(path): -+ return os.path.join(os.path.dirname(__file__), path) -+ -+ -+PRIVKEY = sibpath("private.ssl") -+PUBKEY = sibpath("public.ssl") -+BADPRIVKEY = sibpath("badprivate.ssl") -+BADPUBKEY = sibpath("badpublic.ssl") -+PRIVSANKEY = sibpath("private_san.ssl") -+PUBSANKEY = sibpath("public_san.ssl") -+ -+ - class ErrorWrapperTestCase(TXAWSTestCase): - - def test_204_no_content(self): -@@ -148,3 +167,135 @@ - d = query.get_page(self._get_url("file")) - d.addCallback(query.get_response_headers) - return d.addCallback(check_results) -+ -+ def test_ssl_hostname_verification(self): -+ """ -+ If the endpoint passed to L{BaseQuery} has C{ssl_hostname_verification} -+ sets to C{True}, a L{VerifyingContextFactory} is passed to -+ C{connectSSL}. -+ """ -+ -+ class FakeReactor(object): -+ -+ def __init__(self): -+ self.connects = [] -+ -+ def connectSSL(self, host, port, client, factory): -+ self.connects.append((host, port, client, factory)) -+ -+ fake_reactor = FakeReactor() -+ endpoint = AWSServiceEndpoint(ssl_hostname_verification=True) -+ query = BaseQuery("an action", "creds", endpoint, fake_reactor) -+ query.get_page("https://example.com/file") -+ [(host, port, client, factory)] = fake_reactor.connects -+ self.assertEqual("example.com", host) -+ self.assertEqual(443, port) -+ self.assertTrue(isinstance(factory, VerifyingContextFactory)) -+ self.assertEqual("example.com", factory.host) -+ self.assertNotEqual([], factory.caCerts) -+ -+ -+class BaseQuerySSLTestCase(TXAWSTestCase): -+ -+ def setUp(self): -+ self.cleanupServerConnections = 0 -+ name = self.mktemp() -+ os.mkdir(name) -+ FilePath(name).child("file").setContent("0123456789") -+ r = static.File(name) -+ self.site = server.Site(r, timeout=None) -+ self.wrapper = WrappingFactory(self.site) -+ from txaws.client import ssl -+ pub_key = file(PUBKEY) -+ pub_key_data = pub_key.read() -+ pub_key.close() -+ pub_key_san = file(PUBSANKEY) -+ pub_key_san_data = pub_key_san.read() -+ pub_key_san.close() -+ ssl._ca_certs = [load_certificate(FILETYPE_PEM, pub_key_data), -+ load_certificate(FILETYPE_PEM, pub_key_san_data)] -+ -+ def tearDown(self): -+ from txaws.client import ssl -+ ssl._ca_certs = None -+ # If the test indicated it might leave some server-side connections -+ # around, clean them up. -+ connections = self.wrapper.protocols.keys() -+ # If there are fewer server-side connections than requested, -+ # that's okay. Some might have noticed that the client closed -+ # the connection and cleaned up after themselves. -+ for n in range(min(len(connections), self.cleanupServerConnections)): -+ proto = connections.pop() -+ log.msg("Closing %r" % (proto,)) -+ proto.transport.loseConnection() -+ if connections: -+ log.msg("Some left-over connections; this test is probably buggy.") -+ return self.port.stopListening() -+ -+ def _get_url(self, path): -+ return "https://localhost:%d/%s" % (self.portno, path) -+ -+ def test_ssl_verification_positive(self): -+ """ -+ The L{VerifyingContextFactory} properly allows to connect to the -+ endpoint if the certificates match. -+ """ -+ context_factory = DefaultOpenSSLContextFactory(PRIVKEY, PUBKEY) -+ self.port = reactor.listenSSL( -+ 0, self.site, context_factory, interface="127.0.0.1") -+ self.portno = self.port.getHost().port -+ -+ endpoint = AWSServiceEndpoint(ssl_hostname_verification=True) -+ query = BaseQuery("an action", "creds", endpoint) -+ d = query.get_page(self._get_url("file")) -+ return d.addCallback(self.assertEquals, "0123456789") -+ -+ def test_ssl_verification_negative(self): -+ """ -+ The L{VerifyingContextFactory} fails with a SSL error the certificates -+ can't be checked. -+ """ -+ context_factory = DefaultOpenSSLContextFactory(BADPRIVKEY, BADPUBKEY) -+ self.port = reactor.listenSSL( -+ 0, self.site, context_factory, interface="127.0.0.1") -+ self.portno = self.port.getHost().port -+ -+ endpoint = AWSServiceEndpoint(ssl_hostname_verification=True) -+ query = BaseQuery("an action", "creds", endpoint) -+ d = query.get_page(self._get_url("file")) -+ return self.assertFailure(d, SSLError) -+ -+ def test_ssl_verification_bypassed(self): -+ """ -+ L{BaseQuery} doesn't use L{VerifyingContextFactory} -+ if C{ssl_hostname_verification} is C{False}, thus allowing to connect -+ to non-secure endpoints. -+ """ -+ context_factory = DefaultOpenSSLContextFactory(BADPRIVKEY, BADPUBKEY) -+ self.port = reactor.listenSSL( -+ 0, self.site, context_factory, interface="127.0.0.1") -+ self.portno = self.port.getHost().port -+ -+ endpoint = AWSServiceEndpoint(ssl_hostname_verification=False) -+ query = BaseQuery("an action", "creds", endpoint) -+ d = query.get_page(self._get_url("file")) -+ return d.addCallback(self.assertEquals, "0123456789") -+ -+ def test_ssl_subject_alt_name(self): -+ """ -+ L{VerifyingContextFactory} supports checking C{subjectAltName} in the -+ certificate if it's available. -+ """ -+ context_factory = DefaultOpenSSLContextFactory(PRIVSANKEY, PUBSANKEY) -+ self.port = reactor.listenSSL( -+ 0, self.site, context_factory, interface="127.0.0.1") -+ self.portno = self.port.getHost().port -+ -+ endpoint = AWSServiceEndpoint(ssl_hostname_verification=True) -+ query = BaseQuery("an action", "creds", endpoint) -+ d = query.get_page("https://127.0.0.1:%d/file" % (self.portno,)) -+ return d.addCallback(self.assertEquals, "0123456789") -+ -+ if pyopenssl_version < "0.12": -+ test_ssl_subject_alt_name.skip = ( -+ "subjectAltName not supported by older PyOpenSSL") - -=== modified file 'txaws/service.py' ---- a/txaws/service.py 2011-08-11 23:10:54 +0000 -+++ b/txaws/service.py 2011-11-29 18:51:38 +0000 -@@ -19,13 +19,16 @@ - """ - @param uri: The URL for the service. - @param method: The HTTP method used when accessing a service. -+ @param ssl_hostname_verification: Whether or not SSL hotname verification -+ will be done when connecting to the endpoint. - """ - -- def __init__(self, uri="", method="GET"): -+ def __init__(self, uri="", method="GET", ssl_hostname_verification=False): - self.host = "" - self.port = None - self.path = "/" - self.method = method -+ self.ssl_hostname_verification = ssl_hostname_verification - self._parse_uri(uri) - if not self.scheme: - self.scheme = "http" - diff -Nru txaws-0.2/debian/patches/drop-epsilon.patch txaws-0.2.3/debian/patches/drop-epsilon.patch --- txaws-0.2/debian/patches/drop-epsilon.patch 2012-01-06 17:34:00.000000000 +0000 +++ txaws-0.2.3/debian/patches/drop-epsilon.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,37 +0,0 @@ -Description: Use dateutil instead of epsilon - Necessary for txaws to enter main, especially since dateutil is already - in main and epsilon seems a bit dead -Author: Clint Byrum -Bug: https://bugs.launchpad.net/txaws/+bug/856067 -Forwarded: yes, merged, https://code.launchpad.net/~clint-fewbar/txaws/drop-epsilon/+merge/87700 - ---- txaws-0.2.orig/txaws/s3/client.py -+++ txaws-0.2/txaws/s3/client.py -@@ -15,7 +15,7 @@ import mimetypes - - from twisted.web.http import datetimeToString - --from epsilon.extime import Time -+from dateutil.parser import parse as parseTime - - from txaws.client.base import BaseClient, BaseQuery, error_wrapper - from txaws.s3.acls import AccessControlPolicy -@@ -96,7 +96,7 @@ class S3Client(BaseClient): - for bucket_data in root.find("Buckets"): - name = bucket_data.findtext("Name") - date_text = bucket_data.findtext("CreationDate") -- date_time = Time.fromISO8601TimeAndDate(date_text).asDatetime() -+ date_time = parseTime(date_text) - bucket = Bucket(name, date_time) - buckets.append(bucket) - return buckets -@@ -143,8 +143,7 @@ class S3Client(BaseClient): - for content_data in root.findall("Contents"): - key = content_data.findtext("Key") - date_text = content_data.findtext("LastModified") -- modification_date = Time.fromISO8601TimeAndDate( -- date_text).asDatetime() -+ modification_date = parseTime(date_text) - etag = content_data.findtext("ETag") - size = content_data.findtext("Size") - storage_class = content_data.findtext("StorageClass") diff -Nru txaws-0.2/debian/patches/drop-pytz.patch txaws-0.2.3/debian/patches/drop-pytz.patch --- txaws-0.2/debian/patches/drop-pytz.patch 2012-01-06 17:34:37.000000000 +0000 +++ txaws-0.2.3/debian/patches/drop-pytz.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,103 +0,0 @@ -Description: Convert pytz calls to dateutil calls. - pytz is not in main, and dateutil is already being used in the s3 - client code so its an easy win to drop the pytz dependency for - dateutil. -Author: Clint Byrum -Bug: https://bugs.launchpad.net/txaws/+bug/912589 -Forwarded: yes, merged, https://code.launchpad.net/~clint-fewbar/txaws/drop-pytz/+merge/87703 - ---- txaws-0.2.orig/txaws/server/resource.py -+++ txaws-0.2/txaws/server/resource.py -@@ -1,6 +1,6 @@ - from datetime import datetime, timedelta - from uuid import uuid4 --from pytz import UTC -+from dateutil.tz import tzutc - - from twisted.python import log - from twisted.internet.defer import maybeDeferred -@@ -122,7 +122,7 @@ class QueryAPI(Resource): - - def get_utc_time(self): - """Return a C{datetime} object with the current time in UTC.""" -- return datetime.now(UTC) -+ return datetime.now(tzutc()) - - def _validate(self, request): - """Validate an L{HTTPRequest} before executing it. ---- txaws-0.2.orig/txaws/server/schema.py -+++ txaws-0.2/txaws/server/schema.py -@@ -1,7 +1,7 @@ - from datetime import datetime - from operator import itemgetter - --from pytz import UTC -+from dateutil.tz import tzutc - - from zope.datetime import parse, SyntaxError - -@@ -234,7 +234,7 @@ class Date(Parameter): - - def parse(self, value): - try: -- return datetime(*parse(value, local=False)[:6], tzinfo=UTC) -+ return datetime(*parse(value, local=False)[:6], tzinfo=tzutc()) - except (TypeError, SyntaxError): - raise ValueError() - ---- txaws-0.2.orig/txaws/server/tests/test_resource.py -+++ txaws-0.2/txaws/server/tests/test_resource.py -@@ -1,6 +1,6 @@ --from pytz import UTC - from cStringIO import StringIO - from datetime import datetime -+from dateutil.tz import tzutc - - from twisted.trial.unittest import TestCase - -@@ -346,7 +346,7 @@ class QueryAPITest(TestCase): - self.assertEqual("data", request.response) - self.assertEqual(200, request.code) - -- now = datetime(2009, 12, 31, tzinfo=UTC) -+ now = datetime(2009, 12, 31, tzinfo=tzutc()) - self.api.get_utc_time = lambda: now - self.api.principal = TestPrincipal(creds) - return self.api.handle(request).addCallback(check) -@@ -370,7 +370,7 @@ class QueryAPITest(TestCase): - " 2010-01-01T12:00:00Z", request.response) - self.assertEqual(400, request.code) - -- now = datetime(2010, 1, 1, 12, 0, 1, tzinfo=UTC) -+ now = datetime(2010, 1, 1, 12, 0, 1, tzinfo=tzutc()) - self.api.get_utc_time = lambda: now - return self.api.handle(request).addCallback(check) - ---- txaws-0.2.orig/txaws/server/tests/test_schema.py -+++ txaws-0.2/txaws/server/tests/test_schema.py -@@ -1,6 +1,6 @@ - from datetime import datetime - --from pytz import UTC, FixedOffset -+from dateutil.tz import tzutc, tzoffset - - from twisted.trial.unittest import TestCase - -@@ -266,7 +266,7 @@ class DateTest(TestCase): - def test_parse(self): - """L{Date.parse checks that the given raw C{value} is a date/time.""" - parameter = Date("Test") -- date = datetime(2010, 9, 15, 23, 59, 59, tzinfo=UTC) -+ date = datetime(2010, 9, 15, 23, 59, 59, tzinfo=tzutc()) - self.assertEqual(date, parameter.parse("2010-09-15T23:59:59Z")) - - def test_format(self): -@@ -276,7 +276,7 @@ class DateTest(TestCase): - """ - parameter = Date("Test") - date = datetime(2010, 9, 15, 23, 59, 59, -- tzinfo=FixedOffset(120)) -+ tzinfo=tzoffset('UTC', 120*60)) - self.assertEqual("2010-09-15T21:59:59Z", parameter.format(date)) - - diff -Nru txaws-0.2/debian/patches/drop-zope-datetime.patch txaws-0.2.3/debian/patches/drop-zope-datetime.patch --- txaws-0.2/debian/patches/drop-zope-datetime.patch 2012-01-06 17:35:31.000000000 +0000 +++ txaws-0.2.3/debian/patches/drop-zope-datetime.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,31 +0,0 @@ -Description: replace zope.datetime.parse with dateutil.parser.parse - zope.datetime is not in main and dateutil.parser.parse works the - same as zope.datetime.parse. -Author: Clint Byrum -Bug: https://bugs.launchpad.net/txaws/+bug/912607 -Forwarded: yes, merged, https://code.launchpad.net/~clint-fewbar/txaws/drop-zope.datetime/+merge/87708 - ---- txaws-0.2.orig/txaws/server/schema.py -+++ txaws-0.2/txaws/server/schema.py -@@ -2,8 +2,7 @@ from datetime import datetime - from operator import itemgetter - - from dateutil.tz import tzutc -- --from zope.datetime import parse, SyntaxError -+from dateutil.parser import parse - - from txaws.server.exception import APIError - -@@ -233,10 +232,7 @@ class Date(Parameter): - kind = "date" - - def parse(self, value): -- try: -- return datetime(*parse(value, local=False)[:6], tzinfo=tzutc()) -- except (TypeError, SyntaxError): -- raise ValueError() -+ return parse(value).replace(tzinfo=tzutc()) - - def format(self, value): - # Convert value to UTC. diff -Nru txaws-0.2/debian/patches/fix-handling-nova-securitygroups.patch txaws-0.2.3/debian/patches/fix-handling-nova-securitygroups.patch --- txaws-0.2/debian/patches/fix-handling-nova-securitygroups.patch 2011-10-12 15:59:20.000000000 +0000 +++ txaws-0.2.3/debian/patches/fix-handling-nova-securitygroups.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,150 +0,0 @@ -From: Kapil Thangavelu -Description: Fixes incompatibilities in txaws when asking for group perms from OpenStack nova -Bug: http://pad.lv/829609 -Forwarded: yes -Origin: https://code.launchpad.net/~hazmat/txaws/fix-s3-port-and-bucket-op/+merge/73293 - -=== modified file 'txaws/ec2/client.py' ---- a/txaws/ec2/client.py 2011-05-12 14:48:26 +0000 -+++ b/txaws/ec2/client.py 2011-08-29 20:27:23 +0000 -@@ -658,9 +658,20 @@ - if ip_permissions is None: - ip_permissions = () - for ip_permission in ip_permissions: -+ -+ # openstack doesn't handle self authorized groups properly -+ # XXX this is an upstream problem and should be addressed there -+ # lp bug #829609 - ip_protocol = ip_permission.findtext("ipProtocol") -- from_port = int(ip_permission.findtext("fromPort")) -- to_port = int(ip_permission.findtext("toPort")) -+ from_port = ip_permission.findtext("fromPort") -+ to_port = ip_permission.findtext("toPort") -+ -+ if from_port: -+ from_port = int(from_port) -+ -+ if to_port: -+ to_port = int(to_port) -+ - for groups in ip_permission.findall("groups/item") or (): - user_id = groups.findtext("userId") - group_name = groups.findtext("groupName") - -=== modified file 'txaws/ec2/tests/test_client.py' ---- a/txaws/ec2/tests/test_client.py 2011-05-12 14:38:37 +0000 -+++ b/txaws/ec2/tests/test_client.py 2011-08-29 20:27:23 +0000 -@@ -522,6 +522,38 @@ - d = ec2.describe_security_groups("WebServers") - return d.addCallback(check_result) - -+ def test_describe_security_groups_with_openstack(self): -+ """ -+ L{EC2Client.describe_security_groups} can work with openstack -+ responses, which may lack proper port information for -+ self-referencing group. Verifying that the response doesn't -+ cause an internal error, workaround for nova launchpad bug -+ #829609. -+ """ -+ class StubQuery(object): -+ -+ def __init__(stub, action="", creds=None, endpoint=None, -+ other_params={}): -+ self.assertEqual(action, "DescribeSecurityGroups") -+ self.assertEqual(creds.access_key, "foo") -+ self.assertEqual(creds.secret_key, "bar") -+ self.assertEqual(other_params, {"GroupName.1": "WebServers"}) -+ -+ def submit(self): -+ return succeed( -+ payload.sample_describe_security_groups_with_openstack) -+ -+ def check_result(security_groups): -+ [security_group] = security_groups -+ self.assertEquals(security_group.name, "WebServers") -+ self.assertEqual( -+ security_group.allowed_groups[0].group_name, "WebServers") -+ -+ creds = AWSCredentials("foo", "bar") -+ ec2 = client.EC2Client(creds, query_factory=StubQuery) -+ d = ec2.describe_security_groups("WebServers") -+ return d.addCallback(check_result) -+ - def test_create_security_group(self): - """ - L{EC2Client.create_security_group} returns a C{Deferred} that - -=== modified file 'txaws/s3/client.py' ---- a/txaws/s3/client.py 2011-08-29 20:27:23 +0000 -+++ b/txaws/s3/client.py 2011-08-29 20:27:23 +0000 -@@ -54,6 +54,8 @@ - if not self.object_name.startswith("/"): - path += "/" - path += self.object_name -+ elif self.bucket is not None and not path.endswith("/"): -+ path += "/" - return path - - def get_url(self): - -=== modified file 'txaws/s3/tests/test_client.py' ---- a/txaws/s3/tests/test_client.py 2011-08-29 20:27:23 +0000 -+++ b/txaws/s3/tests/test_client.py 2011-08-29 20:27:23 +0000 -@@ -34,7 +34,7 @@ - - def test_get_path_with_bucket(self): - url_context = client.URLContext(self.endpoint, bucket="mystuff") -- self.assertEquals(url_context.get_path(), "/mystuff") -+ self.assertEquals(url_context.get_path(), "/mystuff/") - - def test_get_path_with_bucket_and_object(self): - url_context = client.URLContext( - -=== modified file 'txaws/testing/payload.py' ---- a/txaws/testing/payload.py 2011-03-26 12:40:36 +0000 -+++ b/txaws/testing/payload.py 2011-08-29 20:27:23 +0000 -@@ -178,6 +178,43 @@ - """ % (version.ec2_api,) - - -+sample_describe_security_groups_with_openstack = """\ -+ -+ -+ 7d4e4dbd-0a33-4d3a-864a-b5ce0f1c9cbf -+ -+ -+ -+ -+ 22 -+ tcp -+ -+ 0.0.0.0/0 -+ -+ -+ 22 -+ -+ -+ -+ -+ -+ -+ -+ WebServers -+ UYY3TLBUXIEON5NQVUUX6OMPWBZIQNFM -+ -+ -+ -+ -+ -+ WebServers -+ Web servers -+ UYY3TLBUXIEON5NQVUUX6OMPWBZIQNFM -+ -+ -+ -+""" % (version.ec2_api,) -+ - sample_describe_security_groups_result = """\ - - - diff -Nru txaws-0.2/debian/patches/fix-missing-test-ssl-files txaws-0.2.3/debian/patches/fix-missing-test-ssl-files --- txaws-0.2/debian/patches/fix-missing-test-ssl-files 1970-01-01 00:00:00.000000000 +0000 +++ txaws-0.2.3/debian/patches/fix-missing-test-ssl-files 2012-06-23 05:56:21.000000000 +0000 @@ -0,0 +1,128 @@ +Description: Adds files from upstream distribution missing from tarball + These .ssl files were not included in the MANIFEST of the python + package, so they are in lp:txaws, but not the tarball. +Author: Clint Byrum +Bug: http://pad.lv/1004226 +Forwarded: yes, https://code.launchpad.net/~clint-fewbar/txaws/dist-missing-ssl-files/+merge/107300 + +--- /dev/null ++++ txaws-0.2.3/txaws/client/tests/badprivate.ssl +@@ -0,0 +1,15 @@ ++-----BEGIN RSA PRIVATE KEY----- ++MIICXgIBAAKBgQDGYFWP2Ine2OFIPjX+Tu+S403KW63EWq/I1DYXiezLoUpYPed3 ++0tAkAXH1gOwQZbARFlUn0LgvXDSpuQLvgKQZwP/e1D8SvZUZ6nexW+aYlPE9kjd1 ++dhK1xpe1h5y09AjCz02xxzcFzrJrJ47uU7vV+FGArE8FFh3hO+dz0/PmZQIDAQAB ++AoGBAKfv+983yJfgcO9QwzLULlrilQNfk36r6y6QAG7y84T7uU10spSs4kno80mL ++58yF2YTNrC91scdePrMEDikldUVcCqtPYcZKHyw5+4aGaDDO244tznexOQnQcNIe ++2BbLFuh+jmJpoFIY/H7EsLQQzn6+6dGPnYGBQfiyitWfAXRNAkEA/ShQkYCRAHgq ++g6WBIYsw/ISQydhiMiKrL2ZUXERT+pWU9MoSdMskgyMi3S7wzwJQXkrHA36q8QkL +++H8n5K+f5wJBAMiajfEtv0wRW0awX40qJtuqW3cSKeGHBH9mMObcRJd5OcK6giC/ ++Cc5st/ZcuE/8i4r44DfeC+cwY6QdIqI8rdMCQQCKuq78LWJIyZEyt12+ThK4LsVR ++d1zIcKsyvHb6YQ9MQPBx/NKEYlZN7tFKOFEKgBAevAe3aJCwqe5/bN8luQB9AkEA ++uQVD8bR+AgzoIPS/zJWaLXSc09/e3PIJBfAdHnD+mq7mxWH8b3OD+e5wZjvyi2Ok ++2NLfCug0FlGdNVrh/Lz2nQJATdcNvHNzJcWOHe05lo+xAqkjz73FWGpPNrdXRigG ++YnjIsZVy4k48xIxPhT2rC44yo1iPEP5EnHCE2bLyUlTAYA== ++-----END RSA PRIVATE KEY----- +--- /dev/null ++++ txaws-0.2.3/txaws/client/tests/public_san.ssl +@@ -0,0 +1,12 @@ ++-----BEGIN CERTIFICATE----- ++MIIB1jCCAT+gAwIBAgIJAMG1W/zdYglWMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV ++BAMTCWxvY2FsaG9zdDAeFw0xMTExMjkxODIxNTdaFw0yMTExMjYxODIxNTdaMBQx ++EjAQBgNVBAMTCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA ++zM5zAXQchBx6fFJGspPt850lhlU/O8a4OLBSmnpY27IAnolUI/zdLNOnfJeYp8MU ++7+bTRZ0rEXbxbexD82tW9BALN+HY2EO3Gibun0k5Vb2Ps2Q7hIU4seHsnFstPWPQ ++wZMlfRvEmSujvy/AH84A3j96JB2WLbLoPqySp1v1BccCAwEAAaMwMC4wLAYDVR0R ++BCUwI4IJMTI3LjAuMC4xgglsb2NhbGhvc3SCCzE5Mi4xNjguMC4xMA0GCSqGSIb3 ++DQEBBQUAA4GBACgPQt3A+kq8Jus+vCIvhbKjU6HaId5gHvRhvM+SnBb/K8llDInh ++vS2bpVasSprTbPQjnqh6vVEj0jB/52p8pliZ5Q0pnaEZRYJtUnyeQCz8mwS16h5o ++KiKfQclPKkM0p0wiQPz1sxju7bbYRm2PoCvoNl08c5RhstKSwF9XmuTx ++-----END CERTIFICATE----- +--- /dev/null ++++ txaws-0.2.3/txaws/client/tests/public.ssl +@@ -0,0 +1,22 @@ ++-----BEGIN CERTIFICATE----- ++MIIDnDCCAwWgAwIBAgIJALPjWsknBC15MA0GCSqGSIb3DQEBBQUAMIGRMQswCQYD ++VQQGEwJCUjEPMA0GA1UECBMGUGFyYW5hMREwDwYDVQQHEwhDdXJpdGliYTESMBAG ++A1UEChMJTGFuZHNjYXBlMRAwDgYDVQQLEwdUZXN0aW5nMRIwEAYDVQQDEwlsb2Nh ++bGhvc3QxJDAiBgkqhkiG9w0BCQEWFWFuZHJlYXNAY2Fub25pY2FsLmNvbTAeFw0w ++OTAxMDgxNjQxMzlaFw0xOTAxMDYxNjQxMzlaMIGRMQswCQYDVQQGEwJCUjEPMA0G ++A1UECBMGUGFyYW5hMREwDwYDVQQHEwhDdXJpdGliYTESMBAGA1UEChMJTGFuZHNj ++YXBlMRAwDgYDVQQLEwdUZXN0aW5nMRIwEAYDVQQDEwlsb2NhbGhvc3QxJDAiBgkq ++hkiG9w0BCQEWFWFuZHJlYXNAY2Fub25pY2FsLmNvbTCBnzANBgkqhkiG9w0BAQEF ++AAOBjQAwgYkCgYEA19lTRA2R7ZeZ4pjaHLIWq/KQZo6ohp/aJQkdi3Con7iWOGxU ++4Kom/ka979g8A02zjGqrkUsRYMCySwroN1nJbBq2EjyLBQsfpN14ajOtSVxMKyri ++KSbPPd1x5HoN0hBbmv6FXw8TXybbaB1txW7Jw8efeuJGMBhMYyXB+9xiJ1ECAwEA ++AaOB+TCB9jAdBgNVHQ4EFgQU3eUz2XxK1J/oavkn/hAvYfGOZM0wgcYGA1UdIwSB ++vjCBu4AU3eUz2XxK1J/oavkn/hAvYfGOZM2hgZekgZQwgZExCzAJBgNVBAYTAkJS ++MQ8wDQYDVQQIEwZQYXJhbmExETAPBgNVBAcTCEN1cml0aWJhMRIwEAYDVQQKEwlM ++YW5kc2NhcGUxEDAOBgNVBAsTB1Rlc3RpbmcxEjAQBgNVBAMTCWxvY2FsaG9zdDEk ++MCIGCSqGSIb3DQEJARYVYW5kcmVhc0BjYW5vbmljYWwuY29tggkAs+NayScELXkw ++DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOBgQBZQcqhHAsasX3WtCXlIKqH ++hE4ZdsvtPHOnoWPxxN4CZEyu2YJ2PMXCkA7yISNokAZgkOpkYGPWwV3CwNCw032u +++ngwIo2sxx7ag8tVrYkIda717oBw7opDMVrjTNhZdak7s+hg+s9ZDPUMMcbJFtlN ++lmayn/uZSyog4Y+yriB1tQ== ++-----END CERTIFICATE----- +--- /dev/null ++++ txaws-0.2.3/txaws/client/tests/private_san.ssl +@@ -0,0 +1,16 @@ ++-----BEGIN PRIVATE KEY----- ++MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAMzOcwF0HIQcenxS ++RrKT7fOdJYZVPzvGuDiwUpp6WNuyAJ6JVCP83SzTp3yXmKfDFO/m00WdKxF28W3s ++Q/NrVvQQCzfh2NhDtxom7p9JOVW9j7NkO4SFOLHh7JxbLT1j0MGTJX0bxJkro78v ++wB/OAN4/eiQdli2y6D6skqdb9QXHAgMBAAECgYAvSP7+d+tZiSWybGCMPGE03LRc ++NnRZ/cBsvjDkH5lCZ++Cqtw1Tt1VyywhNPL20LCVzuo6aVYXOyn0ohbyLXcuinpE ++rVopV9nUPr0EFOo+yccDNNPQJ2tevlYfEsS6afcfLcUinRUSvVojHDrODADduLR8 ++uA3Le95tChcVwe6NuQJBAOvdRTG858BB9zJdjyd4QoqTA1k0rs+VC3svVUT9l16+ ++gZLZ75wTLbtrkGRN/iiAVPemgqQYmNvuXtUByO3QFmUCQQDeSmt+z2dNCx78mUWQ ++HFcyJP0g2gz/IEnxx/9Rin/9xSo+ycuNvbSwphSHxYl20wVFA72vp/zuOWO3WaXr ++umK7AkB6pDJfe2dRu7sqcCWIk2qeHXVHRDKFc21l3yXKWsYDmLFNR47kq8BCzNpm ++nXtDWf9USjtx0exhp1+eCHCO331VAkALAMwJXuLSIXbLMhsLYxu9067j7WcvSb3f ++RfMRajWjrhrFON/miDlldRMXFWQUiaV9IQ5Gn54ZfKW+8aUQ4gz5AkB+yOVkouwj ++QVngotLjasbgvE8WugbweLInHN1W2ucsLKSpSADoE/djQ5NnwuolmnrhpQT5BWcQ ++j3o7Gf/nXS+r ++-----END PRIVATE KEY----- +--- /dev/null ++++ txaws-0.2.3/txaws/client/tests/badpublic.ssl +@@ -0,0 +1,23 @@ ++-----BEGIN CERTIFICATE----- ++MIIDzjCCAzegAwIBAgIJANqT3vXxSVFjMA0GCSqGSIb3DQEBBQUAMIGhMQswCQYD ++VQQGEwJCUjEPMA0GA1UECBMGUGFyYW5hMREwDwYDVQQHEwhDdXJpdGliYTEhMB8G ++A1UEChMYRmFrZSBMYW5kc2NhcGUgKFRlc3RpbmcpMREwDwYDVQQLEwhTZWN1cml0 ++eTESMBAGA1UEAxMJbG9jYWxob3N0MSQwIgYJKoZIhvcNAQkBFhVhbmRyZWFzQGNh ++bm9uaWNhbC5jb20wHhcNMDkwMTA5MTUyNTAwWhcNMTkwMTA3MTUyNTAwWjCBoTEL ++MAkGA1UEBhMCQlIxDzANBgNVBAgTBlBhcmFuYTERMA8GA1UEBxMIQ3VyaXRpYmEx ++ITAfBgNVBAoTGEZha2UgTGFuZHNjYXBlIChUZXN0aW5nKTERMA8GA1UECxMIU2Vj ++dXJpdHkxEjAQBgNVBAMTCWxvY2FsaG9zdDEkMCIGCSqGSIb3DQEJARYVYW5kcmVh ++c0BjYW5vbmljYWwuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDGYFWP ++2Ine2OFIPjX+Tu+S403KW63EWq/I1DYXiezLoUpYPed30tAkAXH1gOwQZbARFlUn ++0LgvXDSpuQLvgKQZwP/e1D8SvZUZ6nexW+aYlPE9kjd1dhK1xpe1h5y09AjCz02x ++xzcFzrJrJ47uU7vV+FGArE8FFh3hO+dz0/PmZQIDAQABo4IBCjCCAQYwHQYDVR0O ++BBYEFF4A8+YHCLAt19OtWTjIjBKzLUokMIHWBgNVHSMEgc4wgcuAFF4A8+YHCLAt ++19OtWTjIjBKzLUokoYGnpIGkMIGhMQswCQYDVQQGEwJCUjEPMA0GA1UECBMGUGFy ++YW5hMREwDwYDVQQHEwhDdXJpdGliYTEhMB8GA1UEChMYRmFrZSBMYW5kc2NhcGUg ++KFRlc3RpbmcpMREwDwYDVQQLEwhTZWN1cml0eTESMBAGA1UEAxMJbG9jYWxob3N0 ++MSQwIgYJKoZIhvcNAQkBFhVhbmRyZWFzQGNhbm9uaWNhbC5jb22CCQDak9718UlR ++YzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GBABszkA3CCzt+nTOX+A7/ ++I98DvI0W1Ss0J+Tq+diLr+kw6Z5ZTj5hrIS/x6XhVHjpim4724UBXA0Sels4JXbw ++hhJovuncExce316gAol/9eEzTffZ9mt1jZQy9LL7IAENiobnsj2F65zNaJzXp5UC ++rE/h/xIxz9rAmXtVOWHqZLcw ++-----END CERTIFICATE----- +--- /dev/null ++++ txaws-0.2.3/txaws/client/tests/private.ssl +@@ -0,0 +1,15 @@ ++-----BEGIN RSA PRIVATE KEY----- ++MIICWwIBAAKBgQDX2VNEDZHtl5nimNocshar8pBmjqiGn9olCR2LcKifuJY4bFTg ++qib+Rr3v2DwDTbOMaquRSxFgwLJLCug3WclsGrYSPIsFCx+k3XhqM61JXEwrKuIp ++Js893XHkeg3SEFua/oVfDxNfJttoHW3FbsnDx5964kYwGExjJcH73GInUQIDAQAB ++AoGASiM9NEys6Lx/gJMbp2uL2fdwnak2PTc+iCX/XduOL34JKswawyfuSLwnlO/i ++fQf9OaeR0k/EYkUNeDUA2bIfOj6wWS8tamnX4fxL7A20y5VyqMMah8mcerZgtPdS ++7ZtYCbeijWSKpHgjALc2Hym7R68WZI+IHe0DQkcW6WxOMFkCQQD2jqHZn/Qtd62u ++mWVwIx6G7+Po5vzd86KyWWftdUtVCY9DmiX1rmWXbJhLnmaKCLkmHxyBvw7Biarr ++ZnCAafebAkEA4B2dSpLi7bAzjCa7JBtzV9kr1FVZOl2vA+9BqTAjCQu0b9VDEm8V ++x0061Z8rN7Og3ECGtKH/r3/4RnHUPpwJgwJAdyZQkvHYt4xJc8IPolRmcUFGu4u9 ++Eammq1fHgJqZcBvxjvLUe1jvIXFKW+jNltFGYGTSiuUAxYi4/49+uJ/9FwJAGBB1 ++/DTrcvQxhMH/5C+iYfNqtmD3tMGscjK1jTIjAOyl0kBG9GrDHuRXBesSW+fIxP2U ++uT6P0std4EqGrLZaewJAHT0n/3tXnsPjj+BMlC4ZkRKgPJ4I7zTU1XSlLY5zbMoV ++NvtHLlq7ttiarsH95xyge69uV1/zJVj/IiS71YY9PQ== ++-----END RSA PRIVATE KEY----- diff -Nru txaws-0.2/debian/patches/fix-openstack-terminate-instances.patch txaws-0.2.3/debian/patches/fix-openstack-terminate-instances.patch --- txaws-0.2/debian/patches/fix-openstack-terminate-instances.patch 2011-10-12 15:59:20.000000000 +0000 +++ txaws-0.2.3/debian/patches/fix-openstack-terminate-instances.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,80 +0,0 @@ -Author: Clint Byrum -Bug: http://pad.lv/862595 -Description: Fixes txaws's terminate_instances call to allow for Nova's - less than standard response. -Origin: https://code.launchpad.net/~clint-fewbar/txaws/openstack-terminate-instances/+merge/77593 -Forwarded: yes - -=== modified file 'txaws/ec2/client.py' -Index: txaws/txaws/ec2/client.py -=================================================================== ---- txaws.orig/txaws/ec2/client.py 2011-09-29 18:24:31.633198000 -0700 -+++ txaws/txaws/ec2/client.py 2011-09-29 18:25:54.554602018 -0700 -@@ -630,13 +630,15 @@ - root = XML(xml_bytes) - result = [] - # May be a more elegant way to do this: -- for instance in root.find("instancesSet"): -- instanceId = instance.findtext("instanceId") -- previousState = instance.find("previousState").findtext( -- "name") -- shutdownState = instance.find("shutdownState").findtext( -- "name") -- result.append((instanceId, previousState, shutdownState)) -+ instances = root.find("instancesSet") -+ if instances is not None: -+ for instance in instances: -+ instanceId = instance.findtext("instanceId") -+ previousState = instance.find("previousState").findtext( -+ "name") -+ shutdownState = instance.find("shutdownState").findtext( -+ "name") -+ result.append((instanceId, previousState, shutdownState)) - return result - - def describe_security_groups(self, xml_bytes): -Index: txaws/txaws/ec2/tests/test_client.py -=================================================================== ---- txaws.orig/txaws/ec2/tests/test_client.py 2011-09-29 18:24:31.633198000 -0700 -+++ txaws/txaws/ec2/tests/test_client.py 2011-09-29 18:25:54.554602018 -0700 -@@ -1994,3 +1994,40 @@ - d = ec2.disassociate_address("67.202.55.255") - d.addCallback(self.assertTrue) - return d -+ -+class EC2ParserTestCase(TXAWSTestCase): -+ -+ def setUp(self): -+ self.parser = client.Parser() -+ -+ def test_ec2_terminate_instances(self): -+ """ Given a well formed response from EC2, will we parse the correct thing. """ -+ -+ ec2_xml = """ -+ -+ d0adc305-7f97-4652-b7c2-6993b2bb8260 -+ -+ -+ i-cab0c1aa -+ -+ 32 -+ shutting-down -+ -+ -+ 16 -+ running -+ -+ -+ -+""" -+ ec2_response = self.parser.terminate_instances(ec2_xml) -+ self.assertEquals([('i-cab0c1aa','running','shutting-down')], ec2_response) -+ -+ def test_nova_terminate_instances(self): -+ """ Ensure parser can handle the somewhat non-standard response from nova -+ Note that the bug has been reported in nova here: -+ https://launchpad.net/bugs/862680 """ -+ -+ nova_xml = """4fe6643d-2346-4add-adb7-a1f61f37c043true""" -+ nova_response = self.parser.terminate_instances(nova_xml) -+ self.assertEquals([], nova_response) diff -Nru txaws-0.2/debian/patches/fix-s3-alternate-port.patch txaws-0.2.3/debian/patches/fix-s3-alternate-port.patch --- txaws-0.2/debian/patches/fix-s3-alternate-port.patch 2011-10-12 15:59:20.000000000 +0000 +++ txaws-0.2.3/debian/patches/fix-s3-alternate-port.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,59 +0,0 @@ -From: Clint Byrum -Description: Makes txaws use alternate port in S3 URL -Bug: http://pad.lv/824403 -Origin: https://code.launchpad.net/~clint-fewbar/txaws/fix-s3-port/+merge/71289 -Forwarded: yes - -=== modified file 'txaws/s3/client.py' ---- a/txaws/s3/client.py 2011-04-14 19:50:30 +0000 -+++ b/txaws/s3/client.py 2011-08-29 19:06:23 +0000 -@@ -57,8 +57,12 @@ - return path - - def get_url(self): -- return "%s://%s%s" % ( -- self.endpoint.scheme, self.get_host(), self.get_path()) -+ if self.endpoint.port is not None: -+ return "%s://%s:%d%s" % ( -+ self.endpoint.scheme, self.get_host(), self.endpoint.port, self.get_path()) -+ else: -+ return "%s://%s%s" % ( -+ self.endpoint.scheme, self.get_host(), self.get_path()) - - - class S3Client(BaseClient): - -=== modified file 'txaws/s3/tests/test_client.py' ---- a/txaws/s3/tests/test_client.py 2011-04-14 19:50:30 +0000 -+++ b/txaws/s3/tests/test_client.py 2011-08-29 19:06:23 +0000 -@@ -62,6 +62,29 @@ - url_context.get_url(), - "http://localhost/mydocs/notes.txt") - -+ def test_custom_port_endpoint(self): -+ test_uri='http://0.0.0.0:12345/' -+ endpoint = AWSServiceEndpoint(uri=test_uri) -+ self.assertEquals(endpoint.port, 12345) -+ self.assertEquals(endpoint.scheme, 'http') -+ context = client.URLContext(service_endpoint=endpoint, -+ bucket="foo", -+ object_name="bar") -+ self.assertEquals(context.get_host(), '0.0.0.0') -+ self.assertEquals(context.get_url(), test_uri + 'foo/bar') -+ -+ def test_custom_port_endpoint_https(self): -+ test_uri='https://0.0.0.0:12345/' -+ endpoint = AWSServiceEndpoint(uri=test_uri) -+ self.assertEquals(endpoint.port, 12345) -+ self.assertEquals(endpoint.scheme, 'https') -+ context = client.URLContext(service_endpoint=endpoint, -+ bucket="foo", -+ object_name="bar") -+ self.assertEquals(context.get_host(), '0.0.0.0') -+ self.assertEquals(context.get_url(), test_uri + 'foo/bar') -+ -+ - URLContextTestCase.skip = s3clientSkip - - - diff -Nru txaws-0.2/debian/patches/fix-s3-fixes.patch txaws-0.2.3/debian/patches/fix-s3-fixes.patch --- txaws-0.2/debian/patches/fix-s3-fixes.patch 2011-10-12 15:59:20.000000000 +0000 +++ txaws-0.2.3/debian/patches/fix-s3-fixes.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,62 +0,0 @@ -From: Clint Byrum -Author: Jim Baker -Description: Fix regression in creating buckets introduced by - fix-alternative-s3-port and fix-handling-nova-securitygroups patches. This - has been merged into txaws trunk. -Origin: http://bazaar.launchpad.net/~jimbaker/txaws/fix-s3-signing/revision/100 -Forwarded: yes -Bug-Ubuntu: http://pad.lv/856749 - -=== modified file 'txaws/s3/client.py' ---- a/txaws/s3/client.py 2011-08-29 20:01:05 +0000 -+++ b/txaws/s3/client.py 2011-09-22 16:57:38 +0000 -@@ -395,12 +395,16 @@ - """ - Get an S3 resource path. - """ -- resource = "/" -- if self.bucket: -- resource += self.bucket -- if self.object_name: -- resource += "/%s" % self.object_name -- return resource -+ path = "/" -+ if self.bucket is not None: -+ path += self.bucket -+ if self.bucket is not None and self.object_name: -+ if not self.object_name.startswith("/"): -+ path += "/" -+ path += self.object_name -+ elif self.bucket is not None and not path.endswith("/"): -+ path += "/" -+ return path - - def sign(self, headers): - """Sign this query using its built in credentials.""" - -=== modified file 'txaws/s3/tests/test_client.py' ---- a/txaws/s3/tests/test_client.py 2011-08-29 20:14:03 +0000 -+++ b/txaws/s3/tests/test_client.py 2011-09-22 16:57:38 +0000 -@@ -634,7 +634,7 @@ - def test_get_canonicalized_resource(self): - query = client.Query(action="PUT", bucket="images") - result = query.get_canonicalized_resource() -- self.assertEquals(result, "/images") -+ self.assertEquals(result, "/images/") - - def test_get_canonicalized_resource_with_object_name(self): - query = client.Query( -@@ -642,6 +642,12 @@ - result = query.get_canonicalized_resource() - self.assertEquals(result, "/images/advicedog.jpg") - -+ def test_get_canonicalized_resource_with_slashed_object_name(self): -+ query = client.Query( -+ action="PUT", bucket="images", object_name="/advicedog.jpg") -+ result = query.get_canonicalized_resource() -+ self.assertEquals(result, "/images/advicedog.jpg") -+ - def test_sign(self): - query = client.Query(action="PUT", creds=self.creds) - signed = query.sign({}) - diff -Nru txaws-0.2/debian/patches/series txaws-0.2.3/debian/patches/series --- txaws-0.2/debian/patches/series 2012-03-28 05:28:57.000000000 +0000 +++ txaws-0.2.3/debian/patches/series 2012-06-23 05:56:21.000000000 +0000 @@ -1,9 +1,2 @@ -fix-s3-alternate-port.patch -fix-handling-nova-securitygroups.patch -fix-s3-fixes.patch -fix-openstack-terminate-instances.patch -drop-epsilon.patch -drop-pytz.patch -drop-zope-datetime.patch aws-status-add-appindicator.patch -add-ssl-cert-verification.patch +fix-missing-test-ssl-files diff -Nru txaws-0.2/MANIFEST.in txaws-0.2.3/MANIFEST.in --- txaws-0.2/MANIFEST.in 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/MANIFEST.in 1970-01-01 00:00:00.000000000 +0000 @@ -1,5 +0,0 @@ -include README -include LICENSE -include setup.py -recursive-include bin -recursive-include txaws/*.py diff -Nru txaws-0.2/PKG-INFO txaws-0.2.3/PKG-INFO --- txaws-0.2/PKG-INFO 2011-06-14 02:37:48.000000000 +0000 +++ txaws-0.2.3/PKG-INFO 2012-04-11 14:24:21.000000000 +0000 @@ -1,6 +1,6 @@ -Metadata-Version: 1.0 +Metadata-Version: 1.1 Name: txAWS -Version: 0.2 +Version: 0.2.3 Summary: Async library for EC2 and Eucalyptus Home-page: https://launchpad.net/txaws Author: txAWS Developers diff -Nru txaws-0.2/README txaws-0.2.3/README --- txaws-0.2/README 2011-06-14 02:36:14.000000000 +0000 +++ txaws-0.2.3/README 2012-04-11 14:19:09.000000000 +0000 @@ -5,11 +5,9 @@ * The twisted python package (python-twisted on Debian or similar systems) -* The epsilon python package (python-epsilon on Debian or similar systems) - -* The zope.datetime python package if you use the server part - (python-zope.datetime on Debian or similar systems) +* The dateutil python package (python-dateutil on Debian or similar systems) +* lxml (only when using txaws.wsdl) Things present here ------------------- @@ -18,7 +16,6 @@ * bin/aws-status, a GUI status program for aws resources. - License ------- diff -Nru txaws-0.2/setup.cfg txaws-0.2.3/setup.cfg --- txaws-0.2/setup.cfg 2011-06-14 02:37:48.000000000 +0000 +++ txaws-0.2.3/setup.cfg 1970-01-01 00:00:00.000000000 +0000 @@ -1,5 +0,0 @@ -[egg_info] -tag_build = -tag_date = 0 -tag_svn_revision = 0 - diff -Nru txaws-0.2/setup.py txaws-0.2.3/setup.py --- txaws-0.2/setup.py 2011-06-14 02:35:22.000000000 +0000 +++ txaws-0.2.3/setup.py 2012-04-11 14:19:09.000000000 +0000 @@ -5,12 +5,12 @@ from txaws import version # If setuptools is present, use it to find_packages(), and also -# declare our dependency on epsilon. +# declare our dependency on python-dateutil. extra_setup_args = {} try: import setuptools from setuptools import find_packages - extra_setup_args['install_requires'] = ['Epsilon', 'zope.datetime'] + extra_setup_args['install_requires'] = ['python-dateutil<2.0', 'twisted'] except ImportError: def find_packages(): """ diff -Nru txaws-0.2/txaws/client/base.py txaws-0.2.3/txaws/client/base.py --- txaws-0.2/txaws/client/base.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/client/base.py 2012-04-11 14:19:09.000000000 +0000 @@ -3,7 +3,7 @@ except ImportError: from xml.parsers.expat import ExpatError as ParseError -from twisted.internet import reactor, ssl +from twisted.internet.ssl import ClientContextFactory from twisted.web import http from twisted.web.client import HTTPClientFactory from twisted.web.error import Error as TwistedWebError @@ -12,6 +12,7 @@ from txaws.credentials import AWSCredentials from txaws.exception import AWSResponseParseError from txaws.service import AWSServiceEndpoint +from txaws.client.ssl import VerifyingContextFactory def error_wrapper(error, errorClass): @@ -73,13 +74,16 @@ class BaseQuery(object): - def __init__(self, action=None, creds=None, endpoint=None): + def __init__(self, action=None, creds=None, endpoint=None, reactor=None): if not action: raise TypeError("The query requires an action parameter.") self.factory = HTTPClientFactory self.action = action self.creds = creds self.endpoint = endpoint + if reactor is None: + from twisted.internet import reactor + self.reactor = reactor self.client = None def get_page(self, url, *args, **kwds): @@ -92,11 +96,14 @@ contextFactory = None scheme, host, port, path = parse(url) self.client = self.factory(url, *args, **kwds) - if scheme == 'https': - contextFactory = ssl.ClientContextFactory() - reactor.connectSSL(host, port, self.client, contextFactory) + if scheme == "https": + if self.endpoint.ssl_hostname_verification: + contextFactory = VerifyingContextFactory(host) + else: + contextFactory = ClientContextFactory() + self.reactor.connectSSL(host, port, self.client, contextFactory) else: - reactor.connectTCP(host, port, self.client) + self.reactor.connectTCP(host, port, self.client) return self.client.deferred def get_request_headers(self, *args, **kwds): diff -Nru txaws-0.2/txaws/client/discover/command.py txaws-0.2.3/txaws/client/discover/command.py --- txaws-0.2/txaws/client/discover/command.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/client/discover/command.py 2012-04-11 14:19:09.000000000 +0000 @@ -55,11 +55,11 @@ other_params=self.parameters) def write_response(response): - print >>self.output, "URL: %s" % query.client.url - print >>self.output - print >>self.output, "HTTP status code: %s" % query.client.status - print >>self.output - print >>self.output, response + print >> self.output, "URL: %s" % query.client.url + print >> self.output + print >> self.output, "HTTP status code: %s" % query.client.status + print >> self.output + print >> self.output, response def write_error(failure): if failure.check(AWSError): @@ -69,11 +69,17 @@ if message.startswith("Error Message: "): message = message[len("Error Message: "):] - print >>self.output, "URL: %s" % query.client.url - print >>self.output - print >>self.output, "HTTP status code: %s" % query.client.status - print >>self.output - print >>self.output, message + print >> self.output, "URL: %s" % query.client.url + print >> self.output + if getattr(query.client, "status", None) is not None: + print >> self.output, "HTTP status code: %s" % ( + query.client.status,) + print >> self.output + print >> self.output, message + + if getattr(failure.value, "response", None) is not None: + print >> self.output + print >> self.output, failure.value.response deferred = query.submit() deferred.addCallback(write_response) diff -Nru txaws-0.2/txaws/client/discover/tests/test_command.py txaws-0.2.3/txaws/client/discover/tests/test_command.py --- txaws-0.2/txaws/client/discover/tests/test_command.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/client/discover/tests/test_command.py 2012-04-11 14:19:09.000000000 +0000 @@ -6,6 +6,7 @@ from cStringIO import StringIO from twisted.internet.defer import succeed, fail +from twisted.web.error import Error from txaws.client.discover.command import Command from txaws.ec2.client import Query @@ -19,13 +20,14 @@ self.url = url -class CommandTest(TXAWSTestCase): +class CommandTestCase(TXAWSTestCase): def prepare_command(self, response, status, action, parameters={}, - get_page=None): + get_page=None, error=None): """Prepare a L{Command} for testing.""" self.url = None self.method = None + self.error = error self.response = response self.status = status self.output = StringIO() @@ -59,7 +61,7 @@ self.url = url self.method = method self.query.client = FakeHTTPClient(self.status, url) - return fail(Exception(self.response)) + return fail(self.error or Exception(self.response)) def test_run(self): """ @@ -72,9 +74,9 @@ url = ( "http://endpoint?AWSAccessKeyId=key&" "Action=DescribeRegions&" - "Signature=uAlV2ALkp7qTxZrTNNuJhHl0i9xiTK5faZOhJTgGS1E%3D&" + "Signature=3%2BHSkQQosF1Sr9AL3kdY31tEfTWQ2whjJOUSc3kvc2c%3D&" "SignatureMethod=HmacSHA256&SignatureVersion=2&" - "Timestamp=2010-06-04T23%3A40%3A00Z&Version=2008-12-01") + "Timestamp=2010-06-04T23%3A40%3A00Z&Version=2009-11-30") self.assertEqual("GET", self.method) self.assertEqual(url, self.url) self.assertEqual("URL: %s\n" @@ -97,9 +99,9 @@ url = ( "http://endpoint?AWSAccessKeyId=key&" "Action=DescribeRegions&RegionName.0=us-west-1&" - "Signature=P6C7cQJ7j93uIJyv2dTbpQG3EI7ArGBJT%2FzVH%2BDFhyY%3D&" + "Signature=6D8aCgSPQOYixowRHy26aRFzK2Vwgixl9uwegYX9nLA%3D&" "SignatureMethod=HmacSHA256&SignatureVersion=2&" - "Timestamp=2010-06-04T23%3A40%3A00Z&Version=2008-12-01") + "Timestamp=2010-06-04T23%3A40%3A00Z&Version=2009-11-30") self.assertEqual("GET", self.method) self.assertEqual(url, self.url) self.assertEqual("URL: %s\n" @@ -126,9 +128,9 @@ url = ( "http://endpoint?AWSAccessKeyId=key&" "Action=DescribeRegions&RegionName.0=us-west-1&" - "Signature=P6C7cQJ7j93uIJyv2dTbpQG3EI7ArGBJT%2FzVH%2BDFhyY%3D&" + "Signature=6D8aCgSPQOYixowRHy26aRFzK2Vwgixl9uwegYX9nLA%3D&" "SignatureMethod=HmacSHA256&SignatureVersion=2&" - "Timestamp=2010-06-04T23%3A40%3A00Z&Version=2008-12-01") + "Timestamp=2010-06-04T23%3A40%3A00Z&Version=2009-11-30") self.assertEqual("GET", self.method) self.assertEqual(url, self.url) self.assertEqual("URL: %s\n" @@ -157,7 +159,7 @@ "Action=DescribeRegions&RegionName.0=us-west-1&" "Signature=P6C7cQJ7j93uIJyv2dTbpQG3EI7ArGBJT%2FzVH%2BDFhyY%3D&" "SignatureMethod=HmacSHA256&SignatureVersion=2&" - "Timestamp=2010-06-04T23%3A40%3A00Z&Version=2008-12-01") + "Timestamp=2010-06-04T23%3A40%3A00Z&Version=2009-11-30") self.assertEqual("GET", self.method) self.assertEqual(url, self.url) self.assertEqual("URL: %s\n" @@ -170,3 +172,33 @@ deferred = self.command.run() deferred.addErrback(check) return deferred + + def test_run_with_error_payload(self): + """ + If the returned HTTP error contains a payload, it's printed out. + """ + self.prepare_command("Bad Request", 400, + "DescribeRegions", {"RegionName.0": "us-west-1"}, + self.get_error_page, Error(400, None, "bar")) + + def check(result): + url = ( + "http://endpoint?AWSAccessKeyId=key&" + "Action=DescribeRegions&RegionName.0=us-west-1&" + "Signature=6D8aCgSPQOYixowRHy26aRFzK2Vwgixl9uwegYX9nLA%3D&" + "SignatureMethod=HmacSHA256&SignatureVersion=2&" + "Timestamp=2010-06-04T23%3A40%3A00Z&Version=2009-11-30") + self.assertEqual("GET", self.method) + self.assertEqual(url, self.url) + self.assertEqual("URL: %s\n" + "\n" + "HTTP status code: 400\n" + "\n" + "400 Bad Request\n" + "\n" + "bar\n" % url, + self.output.getvalue()) + + deferred = self.command.run() + deferred.addCallback(check) + return deferred diff -Nru txaws-0.2/txaws/client/discover/tests/test_entry_point.py txaws-0.2.3/txaws/client/discover/tests/test_entry_point.py --- txaws-0.2/txaws/client/discover/tests/test_entry_point.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/client/discover/tests/test_entry_point.py 2012-04-11 14:19:09.000000000 +0000 @@ -12,7 +12,7 @@ from txaws.testing.base import TXAWSTestCase -class ParseOptionsTest(TXAWSTestCase): +class ParseOptionsTestCase(TXAWSTestCase): def test_parse_options(self): """ @@ -165,7 +165,7 @@ "--action", "action", "--help"]) -class GetCommandTest(TXAWSTestCase): +class GetCommandTestCase(TXAWSTestCase): def test_get_command_without_arguments(self): """An L{OptionError} is raised if no arguments are provided.""" @@ -223,7 +223,7 @@ self.assertEqual({"Region.Name.0": "us-west-1"}, command.parameters) -class MainTest(TXAWSTestCase): +class MainTestCase(TXAWSTestCase): def test_usage_message(self): """ diff -Nru txaws-0.2/txaws/client/gui/gtk.py txaws-0.2.3/txaws/client/gui/gtk.py --- txaws-0.2/txaws/client/gui/gtk.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/client/gui/gtk.py 2012-04-11 14:19:09.000000000 +0000 @@ -63,7 +63,7 @@ def set_region(self, creds): from txaws.service import AWSServiceRegion - self.region = AWSServiceRegion(creds) + self.region = AWSServiceRegion(creds) def create_client(self, creds): if creds is not None: @@ -98,6 +98,7 @@ gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT)) content = self.password_dialog.get_content_area() + def add_entry(name): box = gtk.HBox() box.show() diff -Nru txaws-0.2/txaws/client/gui/tests/test_gtk.py txaws-0.2.3/txaws/client/gui/tests/test_gtk.py --- txaws-0.2/txaws/client/gui/tests/test_gtk.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/client/gui/tests/test_gtk.py 2012-04-11 14:19:09.000000000 +0000 @@ -4,8 +4,7 @@ from twisted.trial.unittest import TestCase -class UITests(TestCase): +class UITestCase(TestCase): pass # Really need some, but UI testing hurts my brain. - diff -Nru txaws-0.2/txaws/client/ssl.py txaws-0.2.3/txaws/client/ssl.py --- txaws-0.2/txaws/client/ssl.py 1970-01-01 00:00:00.000000000 +0000 +++ txaws-0.2.3/txaws/client/ssl.py 2012-04-11 14:19:09.000000000 +0000 @@ -0,0 +1,128 @@ +from glob import glob +import os +import re +import sys + +from OpenSSL import SSL +from OpenSSL.crypto import load_certificate, FILETYPE_PEM + +from twisted.internet.ssl import CertificateOptions + +from txaws import exception + + +__all__ = ["VerifyingContextFactory", "get_ca_certs"] + + +# Multiple defaults are supported; just add more paths, separated by colons. +if sys.platform == "darwin": + DEFAULT_CERTS_PATH = "/System/Library/OpenSSL/certs/:" +# XXX Windows users can file a bug to add theirs, since we don't know what +# the right path is +else: + DEFAULT_CERTS_PATH = "/etc/ssl/certs/:" + + +class VerifyingContextFactory(CertificateOptions): + """ + A SSL context factory to pass to C{connectSSL} to check for hostname + validity. + """ + + def __init__(self, host, caCerts=None): + if caCerts is None: + caCerts = get_global_ca_certs() + CertificateOptions.__init__(self, verify=True, caCerts=caCerts) + self.host = host + + def _dnsname_match(self, dn, host): + pats = [] + for frag in dn.split(r"."): + if frag == "*": + pats.append("[^.]+") + else: + frag = re.escape(frag) + pats.append(frag.replace(r"\*", "[^.]*")) + + rx = re.compile(r"\A" + r"\.".join(pats) + r"\Z", re.IGNORECASE) + return bool(rx.match(host)) + + def verify_callback(self, connection, x509, errno, depth, preverifyOK): + # Only check depth == 0 on chained certificates. + if depth == 0: + dns_found = False + if getattr(x509, "get_extension", None) is not None: + for index in range(x509.get_extension_count()): + extension = x509.get_extension(index) + if extension.get_short_name() != "subjectAltName": + continue + data = str(extension) + for element in data.split(", "): + key, value = element.split(":") + if key != "DNS": + continue + if self._dnsname_match(value, self.host): + return preverifyOK + dns_found = True + break + if not dns_found: + commonName = x509.get_subject().commonName + if commonName is None: + return False + if not self._dnsname_match(commonName, self.host): + return False + else: + return False + return preverifyOK + + def _makeContext(self): + context = CertificateOptions._makeContext(self) + context.set_verify( + SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT, + self.verify_callback) + return context + + +def get_ca_certs(): + """ + Retrieve a list of CAs at either the DEFAULT_CERTS_PATH or the env + override, TXAWS_CERTS_PATH. + + In order to find .pem files, this function checks first for presence of the + TXAWS_CERTS_PATH environment variable that should point to a directory + containing cert files. In the absense of this variable, the module-level + DEFAULT_CERTS_PATH will be used instead. + + Note that both of these variables have have multiple paths in them, just + like the familiar PATH environment variable (separated by colons). + """ + cert_paths = os.getenv("TXAWS_CERTS_PATH", DEFAULT_CERTS_PATH).split(":") + certificate_authority_map = {} + for path in cert_paths: + for cert_file_name in glob(os.path.join(path, "*.pem")): + # There might be some dead symlinks in there, so let's make sure + # it's real. + if not os.path.exists(cert_file_name): + continue + cert_file = open(cert_file_name) + data = cert_file.read() + cert_file.close() + x509 = load_certificate(FILETYPE_PEM, data) + digest = x509.digest("sha1") + # Now, de-duplicate in case the same cert has multiple names. + certificate_authority_map[digest] = x509 + values = certificate_authority_map.values() + if len(values) == 0: + raise exception.CertsNotFoundError("Could not find any .pem files.") + return values + + +_ca_certs = None + + +def get_global_ca_certs(): + """Retrieve a singleton of CA certificates.""" + global _ca_certs + if _ca_certs is None: + _ca_certs = get_ca_certs() + return _ca_certs diff -Nru txaws-0.2/txaws/client/tests/test_base.py txaws-0.2.3/txaws/client/tests/test_base.py --- txaws-0.2/txaws/client/tests/test_base.py 1970-01-01 00:00:00.000000000 +0000 +++ txaws-0.2.3/txaws/client/tests/test_base.py 2012-04-11 14:19:09.000000000 +0000 @@ -0,0 +1,184 @@ +import os + +from twisted.internet import reactor +from twisted.internet.error import ConnectionRefusedError +from twisted.protocols.policies import WrappingFactory +from twisted.python import log +from twisted.python.filepath import FilePath +from twisted.python.failure import Failure +from twisted.test.test_sslverify import makeCertificate +from twisted.web import server, static +from twisted.web.client import HTTPClientFactory +from twisted.web.error import Error as TwistedWebError + +from txaws.client import ssl +from txaws.client.base import BaseClient, BaseQuery, error_wrapper +from txaws.service import AWSServiceEndpoint +from txaws.testing.base import TXAWSTestCase + + +class ErrorWrapperTestCase(TXAWSTestCase): + + def test_204_no_content(self): + failure = Failure(TwistedWebError(204, "No content")) + wrapped = error_wrapper(failure, None) + self.assertEquals(wrapped, "204 No content") + + def test_302_found(self): + # XXX I'm not sure we want to raise for 300s... + failure = Failure(TwistedWebError(302, "found")) + error = self.assertRaises( + Exception, error_wrapper, failure, None) + self.assertEquals(failure.type, type(error)) + self.assertTrue(isinstance(error, TwistedWebError)) + self.assertEquals(str(error), "302 found") + + def test_500(self): + failure = Failure(TwistedWebError(500, "internal error")) + error = self.assertRaises( + Exception, error_wrapper, failure, None) + self.assertTrue(isinstance(error, TwistedWebError)) + self.assertEquals(str(error), "500 internal error") + + def test_timeout_error(self): + failure = Failure(Exception("timeout")) + error = self.assertRaises(Exception, error_wrapper, failure, None) + self.assertTrue(isinstance(error, Exception)) + self.assertEquals(str(error), "timeout") + + def test_connection_error(self): + failure = Failure(ConnectionRefusedError("timeout")) + error = self.assertRaises( + Exception, error_wrapper, failure, ConnectionRefusedError) + self.assertTrue(isinstance(error, ConnectionRefusedError)) + + +class BaseClientTestCase(TXAWSTestCase): + + def test_creation(self): + client = BaseClient("creds", "endpoint", "query factory", "parser") + self.assertEquals(client.creds, "creds") + self.assertEquals(client.endpoint, "endpoint") + self.assertEquals(client.query_factory, "query factory") + self.assertEquals(client.parser, "parser") + + +class BaseQueryTestCase(TXAWSTestCase): + + def setUp(self): + self.cleanupServerConnections = 0 + name = self.mktemp() + os.mkdir(name) + FilePath(name).child("file").setContent("0123456789") + r = static.File(name) + self.site = server.Site(r, timeout=None) + self.wrapper = WrappingFactory(self.site) + self.port = self._listen(self.wrapper) + self.portno = self.port.getHost().port + + def tearDown(self): + # If the test indicated it might leave some server-side connections + # around, clean them up. + connections = self.wrapper.protocols.keys() + # If there are fewer server-side connections than requested, + # that's okay. Some might have noticed that the client closed + # the connection and cleaned up after themselves. + for n in range(min(len(connections), self.cleanupServerConnections)): + proto = connections.pop() + log.msg("Closing %r" % (proto,)) + proto.transport.loseConnection() + if connections: + log.msg("Some left-over connections; this test is probably buggy.") + return self.port.stopListening() + + def _listen(self, site): + return reactor.listenTCP(0, site, interface="127.0.0.1") + + def _get_url(self, path): + return "http://127.0.0.1:%d/%s" % (self.portno, path) + + def test_creation(self): + query = BaseQuery("an action", "creds", "http://endpoint") + self.assertEquals(query.factory, HTTPClientFactory) + self.assertEquals(query.action, "an action") + self.assertEquals(query.creds, "creds") + self.assertEquals(query.endpoint, "http://endpoint") + + def test_init_requires_action(self): + self.assertRaises(TypeError, BaseQuery) + + def test_init_requires_creds(self): + self.assertRaises(TypeError, BaseQuery, None) + + def test_get_page(self): + query = BaseQuery("an action", "creds", "http://endpoint") + d = query.get_page(self._get_url("file")) + d.addCallback(self.assertEquals, "0123456789") + return d + + def test_get_request_headers_no_client(self): + + query = BaseQuery("an action", "creds", "http://endpoint") + results = query.get_request_headers() + self.assertEquals(results, None) + + def test_get_request_headers_with_client(self): + + def check_results(results): + self.assertEquals(results.keys(), []) + self.assertEquals(results.values(), []) + + query = BaseQuery("an action", "creds", "http://endpoint") + d = query.get_page(self._get_url("file")) + d.addCallback(query.get_request_headers) + return d.addCallback(check_results) + + def test_get_response_headers_no_client(self): + + query = BaseQuery("an action", "creds", "http://endpoint") + results = query.get_response_headers() + self.assertEquals(results, None) + + def test_get_response_headers_with_client(self): + + def check_results(results): + self.assertEquals(sorted(results.keys()), [ + "accept-ranges", "content-length", "content-type", "date", + "last-modified", "server"]) + self.assertEquals(len(results.values()), 6) + + query = BaseQuery("an action", "creds", "http://endpoint") + d = query.get_page(self._get_url("file")) + d.addCallback(query.get_response_headers) + return d.addCallback(check_results) + + # XXX for systems that don't have certs in the DEFAULT_CERT_PATH, this test + # will fail; instead, let's create some certs in a temp directory and set + # the DEFAULT_CERT_PATH to point there. + def test_ssl_hostname_verification(self): + """ + If the endpoint passed to L{BaseQuery} has C{ssl_hostname_verification} + sets to C{True}, a L{VerifyingContextFactory} is passed to + C{connectSSL}. + """ + + class FakeReactor(object): + + def __init__(self): + self.connects = [] + + def connectSSL(self, host, port, client, factory): + self.connects.append((host, port, client, factory)) + + certs = makeCertificate(O="Test Certificate", CN="something")[1] + self.patch(ssl, "_ca_certs", certs) + fake_reactor = FakeReactor() + endpoint = AWSServiceEndpoint(ssl_hostname_verification=True) + query = BaseQuery("an action", "creds", endpoint, fake_reactor) + query.get_page("https://example.com/file") + [(host, port, client, factory)] = fake_reactor.connects + self.assertEqual("example.com", host) + self.assertEqual(443, port) + self.assertTrue(isinstance(factory, ssl.VerifyingContextFactory)) + self.assertEqual("example.com", factory.host) + self.assertNotEqual([], factory.caCerts) diff -Nru txaws-0.2/txaws/client/tests/test_client.py txaws-0.2.3/txaws/client/tests/test_client.py --- txaws-0.2/txaws/client/tests/test_client.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/client/tests/test_client.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,150 +0,0 @@ -import os - -from twisted.internet import reactor -from twisted.internet.error import ConnectionRefusedError -from twisted.protocols.policies import WrappingFactory -from twisted.python import log -from twisted.python.filepath import FilePath -from twisted.python.failure import Failure -from twisted.web import server, static -from twisted.web.client import HTTPClientFactory -from twisted.web.error import Error as TwistedWebError - -from txaws.client.base import BaseClient, BaseQuery, error_wrapper -from txaws.testing.base import TXAWSTestCase - - -class ErrorWrapperTestCase(TXAWSTestCase): - - def test_204_no_content(self): - failure = Failure(TwistedWebError(204, "No content")) - wrapped = error_wrapper(failure, None) - self.assertEquals(wrapped, "204 No content") - - def test_302_found(self): - # XXX I'm not sure we want to raise for 300s... - failure = Failure(TwistedWebError(302, "found")) - error = self.assertRaises( - Exception, error_wrapper, failure, None) - self.assertEquals(failure.type, type(error)) - self.assertTrue(isinstance(error, TwistedWebError)) - self.assertEquals(str(error), "302 found") - - def test_500(self): - failure = Failure(TwistedWebError(500, "internal error")) - error = self.assertRaises( - Exception, error_wrapper, failure, None) - self.assertTrue(isinstance(error, TwistedWebError)) - self.assertEquals(str(error), "500 internal error") - - def test_timeout_error(self): - failure = Failure(Exception("timeout")) - error = self.assertRaises(Exception, error_wrapper, failure, None) - self.assertTrue(isinstance(error, Exception)) - self.assertEquals(str(error), "timeout") - - def test_connection_error(self): - failure = Failure(ConnectionRefusedError("timeout")) - error = self.assertRaises( - Exception, error_wrapper, failure, ConnectionRefusedError) - self.assertTrue(isinstance(error, ConnectionRefusedError)) - - -class BaseClientTestCase(TXAWSTestCase): - - def test_creation(self): - client = BaseClient("creds", "endpoint", "query factory", "parser") - self.assertEquals(client.creds, "creds") - self.assertEquals(client.endpoint, "endpoint") - self.assertEquals(client.query_factory, "query factory") - self.assertEquals(client.parser, "parser") - - -class BaseQueryTestCase(TXAWSTestCase): - - def setUp(self): - self.cleanupServerConnections = 0 - name = self.mktemp() - os.mkdir(name) - FilePath(name).child("file").setContent("0123456789") - r = static.File(name) - self.site = server.Site(r, timeout=None) - self.wrapper = WrappingFactory(self.site) - self.port = self._listen(self.wrapper) - self.portno = self.port.getHost().port - - def tearDown(self): - # If the test indicated it might leave some server-side connections - # around, clean them up. - connections = self.wrapper.protocols.keys() - # If there are fewer server-side connections than requested, - # that's okay. Some might have noticed that the client closed - # the connection and cleaned up after themselves. - for n in range(min(len(connections), self.cleanupServerConnections)): - proto = connections.pop() - log.msg("Closing %r" % (proto,)) - proto.transport.loseConnection() - if connections: - log.msg("Some left-over connections; this test is probably buggy.") - return self.port.stopListening() - - def _listen(self, site): - return reactor.listenTCP(0, site, interface="127.0.0.1") - - def _get_url(self, path): - return "http://127.0.0.1:%d/%s" % (self.portno, path) - - def test_creation(self): - query = BaseQuery("an action", "creds", "http://endpoint") - self.assertEquals(query.factory, HTTPClientFactory) - self.assertEquals(query.action, "an action") - self.assertEquals(query.creds, "creds") - self.assertEquals(query.endpoint, "http://endpoint") - - def test_init_requires_action(self): - self.assertRaises(TypeError, BaseQuery) - - def test_init_requires_creds(self): - self.assertRaises(TypeError, BaseQuery, None) - - def test_get_page(self): - query = BaseQuery("an action", "creds", "http://endpoint") - d = query.get_page(self._get_url("file")) - d.addCallback(self.assertEquals, "0123456789") - return d - - def test_get_request_headers_no_client(self): - - query = BaseQuery("an action", "creds", "http://endpoint") - results = query.get_request_headers() - self.assertEquals(results, None) - - def test_get_request_headers_with_client(self): - - def check_results(results): - self.assertEquals(results.keys(), []) - self.assertEquals(results.values(), []) - - query = BaseQuery("an action", "creds", "http://endpoint") - d = query.get_page(self._get_url("file")) - d.addCallback(query.get_request_headers) - return d.addCallback(check_results) - - def test_get_response_headers_no_client(self): - - query = BaseQuery("an action", "creds", "http://endpoint") - results = query.get_response_headers() - self.assertEquals(results, None) - - def test_get_response_headers_with_client(self): - - def check_results(results): - self.assertEquals(sorted(results.keys()), [ - "accept-ranges", "content-length", "content-type", "date", - "last-modified", "server"]) - self.assertEquals(len(results.values()), 6) - - query = BaseQuery("an action", "creds", "http://endpoint") - d = query.get_page(self._get_url("file")) - d.addCallback(query.get_response_headers) - return d.addCallback(check_results) diff -Nru txaws-0.2/txaws/client/tests/test_ssl.py txaws-0.2.3/txaws/client/tests/test_ssl.py --- txaws-0.2/txaws/client/tests/test_ssl.py 1970-01-01 00:00:00.000000000 +0000 +++ txaws-0.2.3/txaws/client/tests/test_ssl.py 2012-04-11 14:19:09.000000000 +0000 @@ -0,0 +1,199 @@ +import os +import tempfile + +from OpenSSL.crypto import dump_certificate, load_certificate, FILETYPE_PEM +from OpenSSL.SSL import Error as SSLError +from OpenSSL.version import __version__ as pyopenssl_version + +from twisted.internet import reactor +from twisted.internet.ssl import DefaultOpenSSLContextFactory +from twisted.protocols.policies import WrappingFactory +from twisted.python import log +from twisted.python.filepath import FilePath +from twisted.test.test_sslverify import makeCertificate +from twisted.web import server, static + +from txaws import exception +from txaws.client import ssl +from txaws.client.base import BaseQuery +from txaws.service import AWSServiceEndpoint +from txaws.testing.base import TXAWSTestCase + + +def sibpath(path): + return os.path.join(os.path.dirname(__file__), path) + + +PRIVKEY = sibpath("private.ssl") +PUBKEY = sibpath("public.ssl") +BADPRIVKEY = sibpath("badprivate.ssl") +BADPUBKEY = sibpath("badpublic.ssl") +PRIVSANKEY = sibpath("private_san.ssl") +PUBSANKEY = sibpath("public_san.ssl") + + +class BaseQuerySSLTestCase(TXAWSTestCase): + + def setUp(self): + self.cleanupServerConnections = 0 + name = self.mktemp() + os.mkdir(name) + FilePath(name).child("file").setContent("0123456789") + r = static.File(name) + self.site = server.Site(r, timeout=None) + self.wrapper = WrappingFactory(self.site) + pub_key = file(PUBKEY) + pub_key_data = pub_key.read() + pub_key.close() + pub_key_san = file(PUBSANKEY) + pub_key_san_data = pub_key_san.read() + pub_key_san.close() + ssl._ca_certs = [load_certificate(FILETYPE_PEM, pub_key_data), + load_certificate(FILETYPE_PEM, pub_key_san_data)] + + def tearDown(self): + ssl._ca_certs = None + # If the test indicated it might leave some server-side connections + # around, clean them up. + connections = self.wrapper.protocols.keys() + # If there are fewer server-side connections than requested, + # that's okay. Some might have noticed that the client closed + # the connection and cleaned up after themselves. + for n in range(min(len(connections), self.cleanupServerConnections)): + proto = connections.pop() + log.msg("Closing %r" % (proto,)) + proto.transport.loseConnection() + if connections: + log.msg("Some left-over connections; this test is probably buggy.") + return self.port.stopListening() + + def _get_url(self, path): + return "https://localhost:%d/%s" % (self.portno, path) + + def test_ssl_verification_positive(self): + """ + The L{VerifyingContextFactory} properly allows to connect to the + endpoint if the certificates match. + """ + context_factory = DefaultOpenSSLContextFactory(PRIVKEY, PUBKEY) + self.port = reactor.listenSSL( + 0, self.site, context_factory, interface="127.0.0.1") + self.portno = self.port.getHost().port + + endpoint = AWSServiceEndpoint(ssl_hostname_verification=True) + query = BaseQuery("an action", "creds", endpoint) + d = query.get_page(self._get_url("file")) + return d.addCallback(self.assertEquals, "0123456789") + + def test_ssl_verification_negative(self): + """ + The L{VerifyingContextFactory} fails with a SSL error the certificates + can't be checked. + """ + context_factory = DefaultOpenSSLContextFactory(BADPRIVKEY, BADPUBKEY) + self.port = reactor.listenSSL( + 0, self.site, context_factory, interface="127.0.0.1") + self.portno = self.port.getHost().port + + endpoint = AWSServiceEndpoint(ssl_hostname_verification=True) + query = BaseQuery("an action", "creds", endpoint) + d = query.get_page(self._get_url("file")) + return self.assertFailure(d, SSLError) + + def test_ssl_verification_bypassed(self): + """ + L{BaseQuery} doesn't use L{VerifyingContextFactory} + if C{ssl_hostname_verification} is C{False}, thus allowing to connect + to non-secure endpoints. + """ + context_factory = DefaultOpenSSLContextFactory(BADPRIVKEY, BADPUBKEY) + self.port = reactor.listenSSL( + 0, self.site, context_factory, interface="127.0.0.1") + self.portno = self.port.getHost().port + + endpoint = AWSServiceEndpoint(ssl_hostname_verification=False) + query = BaseQuery("an action", "creds", endpoint) + d = query.get_page(self._get_url("file")) + return d.addCallback(self.assertEquals, "0123456789") + + def test_ssl_subject_alt_name(self): + """ + L{VerifyingContextFactory} supports checking C{subjectAltName} in the + certificate if it's available. + """ + context_factory = DefaultOpenSSLContextFactory(PRIVSANKEY, PUBSANKEY) + self.port = reactor.listenSSL( + 0, self.site, context_factory, interface="127.0.0.1") + self.portno = self.port.getHost().port + + endpoint = AWSServiceEndpoint(ssl_hostname_verification=True) + query = BaseQuery("an action", "creds", endpoint) + d = query.get_page("https://127.0.0.1:%d/file" % (self.portno,)) + return d.addCallback(self.assertEquals, "0123456789") + + if pyopenssl_version < "0.12": + test_ssl_subject_alt_name.skip = ( + "subjectAltName not supported by older PyOpenSSL") + + +class CertsFilesTestCase(TXAWSTestCase): + + def setUp(self): + super(CertsFilesTestCase, self).setUp() + # set up temp dir with no certs + self.no_certs_dir = tempfile.mkdtemp() + # create certs + cert1 = makeCertificate(O="Server Certificate 1", CN="cn1") + cert2 = makeCertificate(O="Server Certificate 2", CN="cn2") + cert3 = makeCertificate(O="Server Certificate 3", CN="cn3") + # set up temp dir with one cert + self.one_cert_dir = tempfile.mkdtemp() + self.cert1 = self._write_pem(cert1, self.one_cert_dir, "cert1.pem") + # set up temp dir with two certs + self.two_certs_dir = tempfile.mkdtemp() + self.cert2 = self._write_pem(cert2, self.two_certs_dir, "cert2.pem") + self.cert3 = self._write_pem(cert3, self.two_certs_dir, "cert3.pem") + + def tearDown(self): + super(CertsFilesTestCase, self).tearDown() + os.unlink(self.cert1) + os.unlink(self.cert2) + os.unlink(self.cert3) + os.removedirs(self.no_certs_dir) + os.removedirs(self.one_cert_dir) + os.removedirs(self.two_certs_dir) + + def _write_pem(self, cert, dir, filename): + data = dump_certificate(FILETYPE_PEM, cert[1]) + full_path = os.path.join(dir, filename) + fh = open(full_path, "w") + fh.write(data) + fh.close() + return full_path + + def test_get_ca_certs_no_certs(self): + os.environ["TXAWS_CERTS_PATH"] = self.no_certs_dir + self.patch(ssl, "DEFAULT_CERTS_PATH", self.no_certs_dir) + self.assertRaises(exception.CertsNotFoundError, ssl.get_ca_certs) + + def test_get_ca_certs_with_default_path(self): + self.patch(ssl, "DEFAULT_CERTS_PATH", self.two_certs_dir) + certs = ssl.get_ca_certs() + self.assertEqual(len(certs), 2) + + def test_get_ca_certs_with_env_path(self): + os.environ["TXAWS_CERTS_PATH"] = self.one_cert_dir + certs = ssl.get_ca_certs() + self.assertEqual(len(certs), 1) + + def test_get_ca_certs_multiple_paths(self): + os.environ["TXAWS_CERTS_PATH"] = "%s:%s" % ( + self.one_cert_dir, self.two_certs_dir) + certs = ssl.get_ca_certs() + self.assertEqual(len(certs), 3) + + def test_get_ca_certs_one_empty_path(self): + os.environ["TXAWS_CERTS_PATH"] = "%s:%s" % ( + self.no_certs_dir, self.one_cert_dir) + certs = ssl.get_ca_certs() + self.assertEqual(len(certs), 1) diff -Nru txaws-0.2/txaws/ec2/client.py txaws-0.2.3/txaws/ec2/client.py --- txaws-0.2/txaws/ec2/client.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/ec2/client.py 2012-04-11 14:19:09.000000000 +0000 @@ -49,14 +49,17 @@ security_groups=None, key_name=None, instance_type=None, user_data=None, availability_zone=None, kernel_id=None, ramdisk_id=None): - """Run new instances.""" + """Run new instances. + + TODO: blockDeviceMapping, monitoring, subnetId + """ params = {"ImageId": image_id, "MinCount": str(min_count), "MaxCount": str(max_count)} + if key_name is not None: + params["KeyName"] = key_name if security_groups is not None: for i, name in enumerate(security_groups): params["SecurityGroup.%d" % (i + 1)] = name - if key_name is not None: - params["KeyName"] = key_name if user_data is not None: params["UserData"] = b64encode(user_data) if instance_type is not None: @@ -340,7 +343,10 @@ return d.addCallback(self.parser.truth_return) def describe_snapshots(self, *snapshot_ids): - """Describe available snapshots.""" + """Describe available snapshots. + + TODO: ownerSet, restorableBySet + """ snapshot_set = {} for pos, snapshot_id in enumerate(snapshot_ids): snapshot_set["SnapshotId.%d" % (pos + 1)] = snapshot_id @@ -351,7 +357,10 @@ return d.addCallback(self.parser.snapshots) def create_snapshot(self, volume_id): - """Create a new snapshot of an existing volume.""" + """Create a new snapshot of an existing volume. + + TODO: description + """ query = self.query_factory( action="CreateSnapshot", creds=self.creds, endpoint=self.endpoint, other_params={"VolumeId": volume_id}) @@ -379,7 +388,7 @@ """Returns information about key pairs available.""" keypairs = {} for index, keypair_name in enumerate(keypair_names): - keypairs["KeyPair.%d" % (index + 1)] = keypair_name + keypairs["KeyName.%d" % (index + 1)] = keypair_name query = self.query_factory( action="DescribeKeyPairs", creds=self.creds, endpoint=self.endpoint, other_params=keypairs) @@ -418,6 +427,9 @@ @return: A L{Deferred} firing with a L{model.Keypair} instance if successful. + + TODO: there is no corresponding method in the 2009-11-30 version + of the ec2 wsdl. Delete this? """ query = self.query_factory( action="ImportKeyPair", creds=self.creds, endpoint=self.endpoint, @@ -530,31 +542,37 @@ @param instance_data: An XML node containing instance data. @param reservation: The L{Reservation} associated with the instance. @return: An L{Instance}. + + TODO: reason, platform, monitoring, subnetId, vpcId, privateIpAddress, + ipAddress, stateReason, architecture, rootDeviceName, + blockDeviceMapping, instanceLifecycle, spotInstanceRequestId. """ instance_id = instance_data.findtext("instanceId") instance_state = instance_data.find( "instanceState").findtext("name") - instance_type = instance_data.findtext("instanceType") - image_id = instance_data.findtext("imageId") private_dns_name = instance_data.findtext("privateDnsName") dns_name = instance_data.findtext("dnsName") + private_ip_address = instance_data.findtext("privateIpAddress") + ip_address = instance_data.findtext("ipAddress") key_name = instance_data.findtext("keyName") ami_launch_index = instance_data.findtext("amiLaunchIndex") - launch_time = instance_data.findtext("launchTime") - placement = instance_data.find("placement").findtext( - "availabilityZone") products = [] product_codes = instance_data.find("productCodes") if product_codes is not None: for product_data in instance_data.find("productCodes"): products.append(product_data.text) + instance_type = instance_data.findtext("instanceType") + launch_time = instance_data.findtext("launchTime") + placement = instance_data.find("placement").findtext( + "availabilityZone") kernel_id = instance_data.findtext("kernelId") ramdisk_id = instance_data.findtext("ramdiskId") + image_id = instance_data.findtext("imageId") instance = model.Instance( instance_id, instance_state, instance_type, image_id, - private_dns_name, dns_name, key_name, ami_launch_index, - launch_time, placement, products, kernel_id, ramdisk_id, - reservation=reservation) + private_dns_name, dns_name, private_ip_address, ip_address, + key_name, ami_launch_index, launch_time, placement, products, + kernel_id, ramdisk_id, reservation=reservation) return instance def describe_instances(self, xml_bytes): @@ -602,7 +620,8 @@ Parse the reservations XML payload that is returned from an AWS RunInstances API call. - @param xml_bytes: raw XML payload from AWS. + @param xml_bytes: raw XML bytes with a C{RunInstancesResponse} root + element. """ root = XML(xml_bytes) # Get the security group information. @@ -625,18 +644,20 @@ @param xml_bytes: XML bytes with a C{TerminateInstancesResponse} root element. @return: An iterable of C{tuple} of (instanceId, previousState, - shutdownState) for the ec2 instances that where terminated. + currentState) for the ec2 instances that where terminated. """ root = XML(xml_bytes) result = [] # May be a more elegant way to do this: - for instance in root.find("instancesSet"): - instanceId = instance.findtext("instanceId") - previousState = instance.find("previousState").findtext( - "name") - shutdownState = instance.find("shutdownState").findtext( - "name") - result.append((instanceId, previousState, shutdownState)) + instances = root.find("instancesSet") + if instances is not None: + for instance in instances: + instanceId = instance.findtext("instanceId") + previousState = instance.find("previousState").findtext( + "name") + currentState = instance.find("currentState").findtext( + "name") + result.append((instanceId, previousState, currentState)) return result def describe_security_groups(self, xml_bytes): @@ -658,9 +679,20 @@ if ip_permissions is None: ip_permissions = () for ip_permission in ip_permissions: + + # openstack doesn't handle self authorized groups properly + # XXX this is an upstream problem and should be addressed there + # lp bug #829609 ip_protocol = ip_permission.findtext("ipProtocol") - from_port = int(ip_permission.findtext("fromPort")) - to_port = int(ip_permission.findtext("toPort")) + from_port = ip_permission.findtext("fromPort") + to_port = ip_permission.findtext("toPort") + + if from_port: + from_port = int(from_port) + + if to_port: + to_port = int(to_port) + for groups in ip_permission.findall("groups/item") or (): user_id = groups.findtext("userId") group_name = groups.findtext("groupName") @@ -697,15 +729,17 @@ @param xml_bytes: XML bytes with a C{DescribeVolumesResponse} root element. @return: A list of L{Volume} instances. + + TODO: attachementSetItemResponseType#deleteOnTermination """ root = XML(xml_bytes) result = [] for volume_data in root.find("volumeSet"): volume_id = volume_data.findtext("volumeId") size = int(volume_data.findtext("size")) - status = volume_data.findtext("status") - availability_zone = volume_data.findtext("availabilityZone") snapshot_id = volume_data.findtext("snapshotId") + availability_zone = volume_data.findtext("availabilityZone") + status = volume_data.findtext("status") create_time = volume_data.findtext("createTime") create_time = datetime.strptime( create_time[:19], "%Y-%m-%dT%H:%M:%S") @@ -715,8 +749,8 @@ result.append(volume) for attachment_data in volume_data.find("attachmentSet"): instance_id = attachment_data.findtext("instanceId") - status = attachment_data.findtext("status") device = attachment_data.findtext("device") + status = attachment_data.findtext("status") attach_time = attachment_data.findtext("attachTime") attach_time = datetime.strptime( attach_time[:19], "%Y-%m-%dT%H:%M:%S") @@ -735,10 +769,10 @@ root = XML(xml_bytes) volume_id = root.findtext("volumeId") size = int(root.findtext("size")) + snapshot_id = root.findtext("snapshotId") + availability_zone = root.findtext("availabilityZone") status = root.findtext("status") create_time = root.findtext("createTime") - availability_zone = root.findtext("availabilityZone") - snapshot_id = root.findtext("snapshotId") create_time = datetime.strptime( create_time[:19], "%Y-%m-%dT%H:%M:%S") volume = model.Volume( @@ -752,6 +786,9 @@ @param xml_bytes: XML bytes with a C{DescribeSnapshotsResponse} root element. @return: A list of L{Snapshot} instances. + + TODO: ownersSet, restorableBySet, ownerId, volumeSize, description, + ownerAlias. """ root = XML(xml_bytes) result = [] @@ -775,6 +812,8 @@ @param xml_bytes: XML bytes with a C{CreateSnapshotResponse} root element. @return: The L{Snapshot} instance created. + + TODO: ownerId, volumeSize, description. """ root = XML(xml_bytes) snapshot_id = root.findtext("snapshotId") @@ -794,6 +833,8 @@ @param xml_bytes: XML bytes with a C{AttachVolumeResponse} root element. @return: a C{dict} with status and attach_time keys. + + TODO: volumeId, instanceId, device """ root = XML(xml_bytes) status = root.findtext("status") @@ -834,7 +875,11 @@ return model.Keypair(key_name, key_fingerprint, key_material) def import_keypair(self, xml_bytes, key_material): - """Extract the key name and the fingerprint from the result.""" + """Extract the key name and the fingerprint from the result. + + TODO: there is no corresponding method in the 2009-11-30 version + of the ec2 wsdl. Delete this? + """ keypair_data = XML(xml_bytes) key_name = keypair_data.findtext("keyName") key_fingerprint = keypair_data.findtext("keyFingerprint") @@ -870,6 +915,8 @@ @param xml_bytes: XML bytes with a C{DescribeAvailibilityZonesResponse} root element. @return: a C{list} of L{AvailabilityZone}. + + TODO: regionName, messageSet """ results = [] root = XML(xml_bytes) @@ -889,7 +936,7 @@ *args, **kwargs): """Create a Query to submit to EC2.""" super(Query, self).__init__(*args, **kwargs) - # Currently, txAWS only supports version 2008-12-01 + # Currently, txAWS only supports version 2009-11-30 if api_version is None: api_version = version.ec2_api self.params = { @@ -952,7 +999,9 @@ @ivar creds: The L{AWSCredentials} to use to compute the signature. @ivar endpoint: The {AWSServiceEndpoint} to consider. - @ivar params: A C{dict} of parameters to consider. + @ivar params: A C{dict} of parameters to consider. They should be byte + strings, but unicode strings are supported and will be encoded in + UTF-8. """ def __init__(self, creds, endpoint, params): @@ -1002,9 +1051,11 @@ def encode(self, string): """Encode a_string as per the canonicalisation encoding rules. - See the AWS dev reference page 90 (2008-12-01 version). + See the AWS dev reference page 186 (2009-11-30 version). @return: a_string encoded. """ + if isinstance(string, unicode): + string = string.encode("utf-8") return quote(string, safe="~") def sorted_params(self): diff -Nru txaws-0.2/txaws/ec2/exception.py txaws-0.2.3/txaws/ec2/exception.py --- txaws-0.2/txaws/ec2/exception.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/ec2/exception.py 2012-04-11 14:19:09.000000000 +0000 @@ -15,6 +15,3 @@ data = self._node_to_dict(error) if data: self.errors.append(data) - - - diff -Nru txaws-0.2/txaws/ec2/model.py txaws-0.2.3/txaws/ec2/model.py --- txaws-0.2/txaws/ec2/model.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/ec2/model.py 2012-04-11 14:19:09.000000000 +0000 @@ -32,6 +32,9 @@ @attrib dns_name: The public DNS name assigned to the instance. This DNS name is contactable from outside the Amazon EC2 network. This element remains empty until the instance enters a running state. + @attrib private_ip_address: The private IP address assigned to the + instance. + @attrib ip_address: The IP address of the instance. @attrib key_name: If this instance was launched with an associated key pair, this displays the key pair name. @attrib ami_launch_index: The AMI launch index, which can be used to find @@ -43,7 +46,8 @@ @attrib ramdisk_id: Optional. RAM disk associated with this instance. """ def __init__(self, instance_id, instance_state, instance_type="", - image_id="", private_dns_name="", dns_name="", key_name="", + image_id="", private_dns_name="", dns_name="", + private_ip_address="", ip_address="", key_name="", ami_launch_index="", launch_time="", placement="", product_codes=[], kernel_id=None, ramdisk_id=None, reservation=None): @@ -53,6 +57,8 @@ self.image_id = image_id self.private_dns_name = private_dns_name self.dns_name = dns_name + self.private_ip_address = private_ip_address + self.ip_address = ip_address self.key_name = key_name self.ami_launch_index = ami_launch_index self.launch_time = launch_time diff -Nru txaws-0.2/txaws/ec2/tests/test_client.py txaws-0.2.3/txaws/ec2/tests/test_client.py --- txaws-0.2/txaws/ec2/tests/test_client.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/ec2/tests/test_client.py 2012-04-11 14:19:09.000000000 +0000 @@ -40,14 +40,17 @@ def test_instance_creation(self): instance = model.Instance( - "id1", "running", "type", "id2", "dns1", "dns2", "key", "ami", - "time", "placement", ["prod1", "prod2"], "id3", "id4") + "id1", "running", "type", "id2", "dns1", "dns2", "ip1", + "ip2", "key", "ami", "time", "placement", + ["prod1", "prod2"], "id3", "id4") self.assertEquals(instance.instance_id, "id1") self.assertEquals(instance.instance_state, "running") self.assertEquals(instance.instance_type, "type") self.assertEquals(instance.image_id, "id2") self.assertEquals(instance.private_dns_name, "dns1") self.assertEquals(instance.dns_name, "dns2") + self.assertEquals(instance.private_ip_address, "ip1") + self.assertEquals(instance.ip_address, "ip2") self.assertEquals(instance.key_name, "key") self.assertEquals(instance.ami_launch_index, "ami") self.assertEquals(instance.launch_time, "time") @@ -139,7 +142,8 @@ def submit(self): return succeed( - payload.sample_describe_availability_zones_multiple_results) + payload. + sample_describe_availability_zones_multiple_results) def check_parsed_availability_zones(results): self.assertEquals(len(results), 3) @@ -179,6 +183,8 @@ self.assertEquals( instance.dns_name, "ec2-75-101-245-65.compute-1.amazonaws.com") + self.assertEquals(instance.private_ip_address, "10.0.0.1") + self.assertEquals(instance.ip_address, "75.101.245.65") self.assertEquals(instance.key_name, "keyname") self.assertEquals(instance.ami_launch_index, "0") self.assertEquals(instance.launch_time, "2009-04-27T02:23:18.000Z") @@ -207,6 +213,8 @@ self.assertEquals( instance.dns_name, "ec2-75-101-245-65.compute-1.amazonaws.com") + self.assertEquals(instance.private_ip_address, "10.0.0.1") + self.assertEquals(instance.ip_address, "75.101.245.65") self.assertEquals(instance.key_name, None) self.assertEquals(instance.ami_launch_index, None) self.assertEquals(instance.launch_time, "2009-04-27T02:23:18.000Z") @@ -522,6 +530,38 @@ d = ec2.describe_security_groups("WebServers") return d.addCallback(check_result) + def test_describe_security_groups_with_openstack(self): + """ + L{EC2Client.describe_security_groups} can work with openstack + responses, which may lack proper port information for + self-referencing group. Verifying that the response doesn't + cause an internal error, workaround for nova launchpad bug + #829609. + """ + class StubQuery(object): + + def __init__(stub, action="", creds=None, endpoint=None, + other_params={}): + self.assertEqual(action, "DescribeSecurityGroups") + self.assertEqual(creds.access_key, "foo") + self.assertEqual(creds.secret_key, "bar") + self.assertEqual(other_params, {"GroupName.1": "WebServers"}) + + def submit(self): + return succeed( + payload.sample_describe_security_groups_with_openstack) + + def check_result(security_groups): + [security_group] = security_groups + self.assertEquals(security_group.name, "WebServers") + self.assertEqual( + security_group.allowed_groups[0].group_name, "WebServers") + + creds = AWSCredentials("foo", "bar") + ec2 = client.EC2Client(creds, query_factory=StubQuery) + d = ec2.describe_security_groups("WebServers") + return d.addCallback(check_result) + def test_create_security_group(self): """ L{EC2Client.create_security_group} returns a C{Deferred} that @@ -1256,7 +1296,7 @@ self.assertEqual("foo", creds) self.assertEquals( other_params, - {"KeyPair.1": "gsg-keypair"}) + {"KeyName.1": "gsg-keypair"}) def submit(self): return succeed(payload.sample_single_describe_keypairs_result) @@ -1521,7 +1561,7 @@ {"AWSAccessKeyId": "foo", "Action": "DescribeInstances", "SignatureVersion": "2", - "Version": "2008-12-01"}) + "Version": "2009-11-30"}) def test_init_other_args_are_params(self): query = client.Query( @@ -1535,7 +1575,7 @@ "InstanceId.0": "12345", "SignatureVersion": "2", "Timestamp": "2007-11-12T13:14:15Z", - "Version": "2008-12-01"}) + "Version": "2009-11-30"}) def test_no_timestamp_if_expires_in_other_params(self): """ @@ -1553,7 +1593,7 @@ "Action": "DescribeInstances", "SignatureVersion": "2", "Expires": "2007-11-12T13:14:15Z", - "Version": "2008-12-01"}) + "Version": "2009-11-30"}) def test_sign(self): query = client.Query( @@ -1561,7 +1601,7 @@ endpoint=self.endpoint, time_tuple=(2007, 11, 12, 13, 14, 15, 0, 0, 0)) query.sign() - self.assertEqual("aDmLr0Ktjsmt17UJD/EZf6DrfKWT1JW0fq2FDUCOPic=", + self.assertEqual("G4c2NtQaFNhWWT8EWPVIIOpHVr0mGUYwJVYss9krsMU=", query.params["Signature"]) def test_old_sign(self): @@ -1572,7 +1612,7 @@ other_params={"SignatureVersion": "1"}) query.sign() self.assertEqual( - "MBKyHoxqCd/lBQLVkCZYpwAtNJg=", query.params["Signature"]) + "9xP+PIs/3QXW+4mWX6WGR4nGqfE=", query.params["Signature"]) def test_unsupported_sign(self): query = client.Query( @@ -1703,6 +1743,15 @@ signature = client.Signature(self.creds, self.endpoint, self.params) self.assertEqual("a%20space", signature.encode("a space")) + def test_encode_unicode(self): + """ + L{Signature.encode} accepts unicode strings and encode them un UTF-8. + """ + signature = client.Signature(self.creds, self.endpoint, self.params) + self.assertEqual( + "f%C3%A9e", + signature.encode(u"f\N{LATIN SMALL LETTER E WITH ACUTE}e")) + def test_canonical_query(self): signature = client.Signature(self.creds, self.endpoint, self.params) time_tuple = (2007, 11, 12, 13, 14, 15, 0, 0, 0) @@ -1711,15 +1760,16 @@ "argwithnovalue": "", "SignatureVersion": "2", "Timestamp": iso8601time(time_tuple), - "Version": "2008-12-01", + "Version": "2009-11-30", "Action": "DescribeInstances", "InstanceId.1": "i-1234"}) expected_params = ("AWSAccessKeyId=foo&Action=DescribeInstances" "&InstanceId.1=i-1234" "&SignatureVersion=2&" - "Timestamp=2007-11-12T13%3A14%3A15Z&Version=2008-12-01&" + "Timestamp=2007-11-12T13%3A14%3A15Z&Version=2009-11-30&" "argwithnovalue=&fu%20n=g%2Fames") - self.assertEqual(expected_params, signature.get_canonical_query_params()) + self.assertEqual( + expected_params, signature.get_canonical_query_params()) def test_signing_text(self): signature = client.Signature(self.creds, self.endpoint, self.params) @@ -1763,14 +1813,14 @@ self.params.update({"AWSAccessKeyId": "foo", "fun": "games", "SignatureVersion": "2", - "Version": "2008-12-01", + "Version": "2009-11-30", "Action": "DescribeInstances"}) self.assertEqual([ ("AWSAccessKeyId", "foo"), ("Action", "DescribeInstances"), ("SignatureVersion", "2"), - ("Version", "2008-12-01"), + ("Version", "2009-11-30"), ("fun", "games"), ], signature.sorted_params()) @@ -1962,3 +2012,49 @@ d = ec2.disassociate_address("67.202.55.255") d.addCallback(self.assertTrue) return d + + +class EC2ParserTestCase(TXAWSTestCase): + + def setUp(self): + self.parser = client.Parser() + + def test_ec2_terminate_instances(self): + """ + Given a well formed response from EC2, parse the correct thing. + """ + ec2_xml = """ + + d0adc305-7f97-4652-b7c2-6993b2bb8260 + + + i-cab0c1aa + + 32 + shutting-down + + + 16 + running + + + +""" + ec2_response = self.parser.terminate_instances(ec2_xml) + self.assertEquals( + [('i-cab0c1aa', 'running', 'shutting-down')], ec2_response) + + def test_nova_terminate_instances(self): + """ + Ensure parser can handle the somewhat non-standard response from nova + Note that the bug has been reported in nova here: + https://launchpad.net/bugs/862680 + """ + + nova_xml = ( + '' + '4fe6643d-2346-4add-adb7-a1f61f37c043' + 'true') + nova_response = self.parser.terminate_instances(nova_xml) + self.assertEquals([], nova_response) diff -Nru txaws-0.2/txaws/ec2/tests/test_model.py txaws-0.2.3/txaws/ec2/tests/test_model.py --- txaws-0.2/txaws/ec2/tests/test_model.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/ec2/tests/test_model.py 2012-04-11 14:19:09.000000000 +0000 @@ -28,12 +28,12 @@ "name", "desc", owner_id="me", groups=user_group_pairs, ips=ips) self.assertEquals(group.name, "name") self.assertEquals(group.description, "desc") - self.assertEquals(group.owner_id, "me") - self.assertEquals(group.allowed_groups[0].user_id, "somegal24") - self.assertEquals(group.allowed_groups[0].group_name, "other1") - self.assertEquals(group.allowed_groups[1].user_id, "somegal24") - self.assertEquals(group.allowed_groups[1].group_name, "other2") - self.assertEquals(group.allowed_ips[0].cidr_ip, "10.0.1.0/24") + self.assertEquals(group.owner_id, "me") + self.assertEquals(group.allowed_groups[0].user_id, "somegal24") + self.assertEquals(group.allowed_groups[0].group_name, "other1") + self.assertEquals(group.allowed_groups[1].user_id, "somegal24") + self.assertEquals(group.allowed_groups[1].group_name, "other2") + self.assertEquals(group.allowed_ips[0].cidr_ip, "10.0.1.0/24") class UserIDGroupPairTestCase(TXAWSTestCase): diff -Nru txaws-0.2/txaws/exception.py txaws-0.2.3/txaws/exception.py --- txaws-0.2/txaws/exception.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/exception.py 2012-04-11 14:19:09.000000000 +0000 @@ -122,8 +122,13 @@ return self.errors[0]["Message"] - class AWSResponseParseError(Exception): """ txAWS was unable to parse the server response. """ + + +class CertsNotFoundError(Exception): + """ + txAWS was not able to find any SSL certificates. + """ diff -Nru txaws-0.2/txaws/meta.py txaws-0.2.3/txaws/meta.py --- txaws-0.2/txaws/meta.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/meta.py 2012-04-11 14:19:09.000000000 +0000 @@ -7,4 +7,3 @@ description = """ Twisted-based Asynchronous Libraries for Amazon Web Services """ - diff -Nru txaws-0.2/txaws/reactor.py txaws-0.2.3/txaws/reactor.py --- txaws-0.2/txaws/reactor.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/reactor.py 2012-04-11 14:19:09.000000000 +0000 @@ -1,5 +1,6 @@ '''Reactor utilities.''' + def get_exitcode_reactor(): """ This is only neccesary until a fix like the one outlined here is diff -Nru txaws-0.2/txaws/regions.py txaws-0.2.3/txaws/regions.py --- txaws-0.2/txaws/regions.py 1970-01-01 00:00:00.000000000 +0000 +++ txaws-0.2.3/txaws/regions.py 2012-04-11 14:19:09.000000000 +0000 @@ -0,0 +1,67 @@ +# Copyright (C) 2009 Duncan McGreggor +# Copyright (C) 2009 Robert Collins +# Copyright (C) 2012 New Dream Network, LLC (DreamHost) +# Licenced under the txaws licence available at /LICENSE in the txaws source. + +__all__ = ["REGION_US", "REGION_EU", "EC2_US_EAST", "EC2_US_WEST", +"EC2_ASIA_PACIFIC", "EC2_EU_WEST", "EC2_SOUTH_AMERICA_EAST", "EC2_ALL_REGIONS"] + + +# These old EC2 variable names are maintained for backwards compatibility. +REGION_US = "US" +REGION_EU = "EU" +EC2_ENDPOINT_US = "https://us-east-1.ec2.amazonaws.com/" +EC2_ENDPOINT_EU = "https://eu-west-1.ec2.amazonaws.com/" + +# These are the new EC2 variables. +EC2_US_EAST = [ + {"region": "US East (Northern Virginia) Region", + "endpoint": "https://ec2.us-east-1.amazonaws.com"}] +EC2_US_WEST = [ + {"region": "US West (Oregon) Region", + "endpoint": "https://ec2.us-west-2.amazonaws.com"}, + {"region": "US West (Northern California) Region", + "endpoint": "https://ec2.us-west-1.amazonaws.com"}] +EC2_US = EC2_US_EAST + EC2_US_WEST +EC2_ASIA_PACIFIC = [ + {"region": "Asia Pacific (Singapore) Region", + "endpoint": "https://ec2.ap-southeast-1.amazonaws.com"}, + {"region": "Asia Pacific (Tokyo) Region", + "endpoint": "https://ec2.ap-northeast-1.amazonaws.com"}] +EC2_EU_WEST = [ + {"region": "EU (Ireland) Region", + "endpoint": "https://ec2.eu-west-1.amazonaws.com"}] +EC2_EU = EC2_EU_WEST +EC2_SOUTH_AMERICA_EAST = [ + {"region": "South America (Sao Paulo) Region", + "endpoint": "https://ec2.sa-east-1.amazonaws.com"}] +EC2_SOUTH_AMERICA = EC2_SOUTH_AMERICA_EAST +EC2_ALL_REGIONS = EC2_US + EC2_ASIA_PACIFIC + EC2_EU + EC2_SOUTH_AMERICA + +# This old S3 variable is maintained for backwards compatibility. +S3_ENDPOINT = "https://s3.amazonaws.com/" + +# These are the new S3 variables. +S3_US_DEFAULT = [ + {"region": "US Standard *", + "endpoint": "https://s3.amazonaws.com"}] +S3_US_WEST = [ + {"region": "US West (Oregon) Region", + "endpoint": "https://s3-us-west-2.amazonaws.com"}, + {"region": "US West (Northern California) Region", + "endpoint": "https://s3-us-west-1.amazonaws.com"}] +S3_ASIA_PACIFIC = [ + {"region": "Asia Pacific (Singapore) Region", + "endpoint": "https://s3-ap-southeast-1.amazonaws.com"}, + {"region": "Asia Pacific (Tokyo) Region", + "endpoint": "https://s3-ap-northeast-1.amazonaws.com"}] +S3_US = S3_US_DEFAULT + S3_US_WEST +S3_EU_WEST = [ + {"region": "EU (Ireland) Region", + "endpoint": "https://s3-eu-west-1.amazonaws.com"}] +S3_EU = S3_EU_WEST +S3_SOUTH_AMERICA_EAST = [ + {"region": "South America (Sao Paulo) Region", + "endpoint": "s3-sa-east-1.amazonaws.com"}] +S3_SOUTH_AMERICA = S3_SOUTH_AMERICA_EAST +S3_ALL_REGIONS = S3_US + S3_ASIA_PACIFIC + S3_EU + S3_SOUTH_AMERICA diff -Nru txaws-0.2/txaws/s3/acls.py txaws-0.2.3/txaws/s3/acls.py --- txaws-0.2/txaws/s3/acls.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/s3/acls.py 2012-04-11 14:19:09.000000000 +0000 @@ -99,17 +99,36 @@ return buffer -class Grantee(Owner): +class Grantee(XMLMixin): + + def __init__(self, id="", display_name="", email_address="", uri=""): + if id or display_name: + msg = "Both 'id' and 'display_name' must be provided." + if not (id and display_name): + raise ValueError(msg) + self.id = id + self.display_name = display_name + self.email_address = email_address + self.uri = uri def _to_xml(self, buffer=None, indent=0): if buffer is None: buffer = [] ws = " " * (indent * 2) + if self.id and self.display_name: + xsi_type = "CanonicalUser" + value = ("%s %s\n" + "%s %s\n" % ( + ws, self.id, ws, self.display_name)) + elif self.email_address: + xsi_type = "AmazonCustomerByEmail" + value = "%s %s\n" % ( + ws, self.email_address) + elif self.uri: + xsi_type = "Group" + value = "%s %s\n" % (ws, self.uri) buffer.append("%s\n' - "%s %s\n" - "%s %s\n" - "%s\n" % (ws, ws, self.id, ws, - self.display_name, ws)) + ' xsi:type="%s">\n' + "%s%s\n" % (ws, xsi_type, value, ws)) return buffer diff -Nru txaws-0.2/txaws/s3/client.py txaws-0.2.3/txaws/s3/client.py --- txaws-0.2/txaws/s3/client.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/s3/client.py 2012-04-11 14:19:09.000000000 +0000 @@ -1,6 +1,7 @@ # Copyright (C) 2008 Tristan Seligmann # Copyright (C) 2009 Canonical Ltd # Copyright (C) 2009 Duncan McGreggor +# Copyright (C) 2012 New Dream Network (DreamHost) # Licenced under the txaws licence available at /LICENSE in the txaws source. """ @@ -15,12 +16,14 @@ from twisted.web.http import datetimeToString -from epsilon.extime import Time +from dateutil.parser import parse as parseTime from txaws.client.base import BaseClient, BaseQuery, error_wrapper from txaws.s3.acls import AccessControlPolicy from txaws.s3.model import ( - Bucket, BucketItem, BucketListing, ItemOwner, RequestPayment) + Bucket, BucketItem, BucketListing, ItemOwner, LifecycleConfiguration, + LifecycleConfigurationRule, NotificationConfiguration, RequestPayment, + VersioningConfiguration, WebsiteConfiguration) from txaws.s3.exception import S3Error from txaws.service import AWSServiceEndpoint, S3_ENDPOINT from txaws.util import XML, calculate_md5 @@ -54,11 +57,18 @@ if not self.object_name.startswith("/"): path += "/" path += self.object_name + elif self.bucket is not None and not path.endswith("/"): + path += "/" return path def get_url(self): - return "%s://%s%s" % ( - self.endpoint.scheme, self.get_host(), self.get_path()) + if self.endpoint.port is not None: + return "%s://%s:%d%s" % ( + self.endpoint.scheme, self.get_host(), self.endpoint.port, + self.get_path()) + else: + return "%s://%s%s" % ( + self.endpoint.scheme, self.get_host(), self.get_path()) class S3Client(BaseClient): @@ -90,7 +100,7 @@ for bucket_data in root.find("Buckets"): name = bucket_data.findtext("Name") date_text = bucket_data.findtext("CreationDate") - date_time = Time.fromISO8601TimeAndDate(date_text).asDatetime() + date_time = parseTime(date_text) bucket = Bucket(name, date_time) buckets.append(bucket) return buckets @@ -137,8 +147,7 @@ for content_data in root.findall("Contents"): key = content_data.findtext("Key") date_text = content_data.findtext("LastModified") - modification_date = Time.fromISO8601TimeAndDate( - date_text).asDatetime() + modification_date = parseTime(date_text) etag = content_data.findtext("ETag") size = content_data.findtext("Size") storage_class = content_data.findtext("StorageClass") @@ -174,6 +183,96 @@ root = XML(xml_bytes) return root.text or "" + def get_bucket_lifecycle(self, bucket): + """ + Get the lifecycle configuration of a bucket. + + @param bucket: The name of the bucket. + @return: A C{Deferred} that will fire with the bucket's lifecycle + configuration. + """ + query = self.query_factory( + action='GET', creds=self.creds, endpoint=self.endpoint, + bucket=bucket, object_name='?lifecycle') + return query.submit().addCallback(self._parse_lifecycle_config) + + def _parse_lifecycle_config(self, xml_bytes): + """Parse a C{LifecycleConfiguration} XML document.""" + root = XML(xml_bytes) + rules = [] + + for content_data in root.findall("Rule"): + id = content_data.findtext("ID") + prefix = content_data.findtext("Prefix") + status = content_data.findtext("Status") + expiration = int(content_data.findtext("Expiration/Days")) + rules.append( + LifecycleConfigurationRule(id, prefix, status, expiration)) + + return LifecycleConfiguration(rules) + + def get_bucket_website_config(self, bucket): + """ + Get the website configuration of a bucket. + + @param bucket: The name of the bucket. + @return: A C{Deferred} that will fire with the bucket's website + configuration. + """ + query = self.query_factory( + action='GET', creds=self.creds, endpoint=self.endpoint, + bucket=bucket, object_name='?website') + return query.submit().addCallback(self._parse_website_config) + + def _parse_website_config(self, xml_bytes): + """Parse a C{WebsiteConfiguration} XML document.""" + root = XML(xml_bytes) + index_suffix = root.findtext("IndexDocument/Suffix") + error_key = root.findtext("ErrorDocument/Key") + + return WebsiteConfiguration(index_suffix, error_key) + + def get_bucket_notification_config(self, bucket): + """ + Get the notification configuration of a bucket. + + @param bucket: The name of the bucket. + @return: A C{Deferred} that will request the bucket's notification + configuration. + """ + query = self.query_factory( + action='GET', creds=self.creds, endpoint=self.endpoint, + bucket=bucket, object_name='?notification') + return query.submit().addCallback(self._parse_notification_config) + + def _parse_notification_config(self, xml_bytes): + """Parse a C{NotificationConfiguration} XML document.""" + root = XML(xml_bytes) + topic = root.findtext("TopicConfiguration/Topic") + event = root.findtext("TopicConfiguration/Event") + + return NotificationConfiguration(topic, event) + + def get_bucket_versioning_config(self, bucket): + """ + Get the versioning configuration of a bucket. + + @param bucket: The name of the bucket. @return: A C{Deferred} that + will request the bucket's versioning configuration. + """ + query = self.query_factory( + action='GET', creds=self.creds, endpoint=self.endpoint, + bucket=bucket, object_name='?versioning') + return query.submit().addCallback(self._parse_versioning_config) + + def _parse_versioning_config(self, xml_bytes): + """Parse a C{VersioningConfiguration} XML document.""" + root = XML(xml_bytes) + mfa_delete = root.findtext("MfaDelete") + status = root.findtext("Status") + + return VersioningConfiguration(mfa_delete=mfa_delete, status=status) + def get_bucket_acl(self, bucket): """ Get the access control policy for a bucket. @@ -278,6 +377,16 @@ bucket=bucket, object_name=object_name) return query.submit() + def put_object_acl(self, bucket, object_name, access_control_policy): + """ + Set access control policy on an object. + """ + data = access_control_policy.to_xml() + query = self.query_factory( + action='PUT', creds=self.creds, endpoint=self.endpoint, + bucket=bucket, object_name='%s?acl' % object_name, data=data) + return query.submit().addCallback(self._parse_acl) + def get_object_acl(self, bucket, object_name): """ Get the access control policy for an object. @@ -389,12 +498,16 @@ """ Get an S3 resource path. """ - resource = "/" - if self.bucket: - resource += self.bucket - if self.object_name: - resource += "/%s" % self.object_name - return resource + path = "/" + if self.bucket is not None: + path += self.bucket + if self.bucket is not None and self.object_name: + if not self.object_name.startswith("/"): + path += "/" + path += self.object_name + elif self.bucket is not None and not path.endswith("/"): + path += "/" + return path def sign(self, headers): """Sign this query using its built in credentials.""" diff -Nru txaws-0.2/txaws/s3/model.py txaws-0.2.3/txaws/s3/model.py --- txaws-0.2/txaws/s3/model.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/s3/model.py 2012-04-11 14:19:09.000000000 +0000 @@ -1,3 +1,8 @@ +# Copyright (C) 2009 Canonical Ltd +# Copyright (C) 2009 Duncan McGreggor +# Copyright (C) 2011 Drew Smathers +# Copyright (C) 2012 New Dream Network (DreamHost) +# Licenced under the txaws licence available at /LICENSE in the txaws source. from txaws.util import XML @@ -34,6 +39,9 @@ class BucketListing(object): + """ + A mapping for the data in a bucket listing. + """ def __init__(self, name, prefix, marker, max_keys, is_truncated, contents=None, common_prefixes=None): self.name = name @@ -45,6 +53,62 @@ self.common_prefixes = common_prefixes +class LifecycleConfiguration(object): + """ + Returns the lifecycle configuration information set on the bucket. + """ + def __init__(self, rules): + self.rules = rules + + +class LifecycleConfigurationRule(object): + """ + Container for elements that describe a lifecycle rule. + """ + def __init__(self, id, prefix, status, expiration): + self.id = id + self.prefix = prefix + self.status = status + self.expiration = expiration + + +class WebsiteConfiguration(object): + """ + A mapping for the data in a bucket website configuration. + """ + def __init__(self, index_suffix, error_key=None): + self.index_suffix = index_suffix + self.error_key = error_key + + +class NotificationConfiguration(object): + """ + A mapping for the data in a bucket notification configuration. + """ + def __init__(self, topic=None, event=None): + self.topic = topic + self.event = event + + +class VersioningConfiguration(object): + """ + Container for the bucket versioning configuration. + + According to Amazon: + + C{MfaDelete}: This element is only returned if the bucket has been + configured with C{MfaDelete}. If the bucket has never been so configured, + this element is not returned. The possible values are None, "Disabled" or + "Enabled". + + C{Status}: If the bucket has never been so configured, this element is not + returned. The possible values are None, "Suspended" or "Enabled". + """ + def __init__(self, mfa_delete=None, status=None): + self.mfa_delete = mfa_delete + self.status = status + + class FileChunk(object): """ An Amazon S3 file chunk. diff -Nru txaws-0.2/txaws/s3/tests/test_acls.py txaws-0.2.3/txaws/s3/tests/test_acls.py --- txaws-0.2/txaws/s3/tests/test_acls.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/s3/tests/test_acls.py 2012-04-11 14:19:09.000000000 +0000 @@ -4,7 +4,7 @@ from txaws.s3 import acls -class ACLTests(TestCase): +class ACLTestCase(TestCase): def test_owner_to_xml(self): owner = acls.Owner(id='8a6925ce4adf588a4f21c32aa379004fef', @@ -17,17 +17,47 @@ """) - def test_grantee_to_xml(self): + def test_grantee_canonical_missing_parameter(self): + self.assertRaises( + ValueError, acls.Grantee, + {'id': '8a6925ce4adf588a4f21c32aa379004fef'}) + self.assertRaises( + ValueError, acls.Grantee, + {'display_name': 'BucketOwnersEmail@amazon.com'}) + + def test_grantee_canonical_to_xml(self): grantee = acls.Grantee(id='8a6925ce4adf588a4f21c32aa379004fef', display_name='BucketOwnersEmail@amazon.com') xml_bytes = grantee.to_xml() self.assertEquals(xml_bytes, """\ - + 8a6925ce4adf588a4f21c32aa379004fef BucketOwnersEmail@amazon.com """) + def test_grantee_email_to_xml(self): + grantee = acls.Grantee(email_address="BucketOwnersEmail@amazon.com") + xml_bytes = grantee.to_xml() + self.assertEquals(xml_bytes, """\ + + BucketOwnersEmail@amazon.com + +""") + + def test_grantee_uri_to_xml(self): + grantee = acls.Grantee( + uri='http://acs.amazonaws.com/groups/global/AuthenticatedUsers') + xml_bytes = grantee.to_xml() + self.assertEquals(xml_bytes, """\ + + http://acs.amazonaws.com/groups/global/AuthenticatedUsers + +""") + def test_grant_to_xml(self): grantee = acls.Grantee(id='8a6925ce4adf588a4f21c32aa379004fef', display_name='BucketOwnersEmail@amazon.com') @@ -35,7 +65,8 @@ xml_bytes = grant.to_xml() self.assertEquals(xml_bytes, """\ - + 8a6925ce4adf588a4f21c32aa379004fef BucketOwnersEmail@amazon.com diff -Nru txaws-0.2/txaws/s3/tests/test_client.py txaws-0.2.3/txaws/s3/tests/test_client.py --- txaws-0.2/txaws/s3/tests/test_client.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/s3/tests/test_client.py 2012-04-11 14:19:09.000000000 +0000 @@ -4,7 +4,7 @@ try: from txaws.s3 import client except ImportError: - s3clientSkip = ("S3Client couldn't be imported (perhaps because epsilon, " + s3clientSkip = ("S3Client couldn't be imported (perhaps because dateutil, " "on which it depends, isn't present)") else: s3clientSkip = None @@ -34,7 +34,7 @@ def test_get_path_with_bucket(self): url_context = client.URLContext(self.endpoint, bucket="mystuff") - self.assertEquals(url_context.get_path(), "/mystuff") + self.assertEquals(url_context.get_path(), "/mystuff/") def test_get_path_with_bucket_and_object(self): url_context = client.URLContext( @@ -62,6 +62,29 @@ url_context.get_url(), "http://localhost/mydocs/notes.txt") + def test_custom_port_endpoint(self): + test_uri = 'http://0.0.0.0:12345/' + endpoint = AWSServiceEndpoint(uri=test_uri) + self.assertEquals(endpoint.port, 12345) + self.assertEquals(endpoint.scheme, 'http') + context = client.URLContext(service_endpoint=endpoint, + bucket="foo", + object_name="bar") + self.assertEquals(context.get_host(), '0.0.0.0') + self.assertEquals(context.get_url(), test_uri + 'foo/bar') + + def test_custom_port_endpoint_https(self): + test_uri = 'https://0.0.0.0:12345/' + endpoint = AWSServiceEndpoint(uri=test_uri) + self.assertEquals(endpoint.port, 12345) + self.assertEquals(endpoint.scheme, 'https') + context = client.URLContext(service_endpoint=endpoint, + bucket="foo", + object_name="bar") + self.assertEquals(context.get_host(), '0.0.0.0') + self.assertEquals(context.get_url(), test_uri + 'foo/bar') + + URLContextTestCase.skip = s3clientSkip @@ -178,7 +201,8 @@ """ L{S3Client.get_bucket_location} creates a L{Query} to get a bucket's location. It parses the returned C{LocationConstraint} XML document - and returns a C{Deferred} that fires with the bucket's region. + and returns a C{Deferred} that requests the bucket's location + constraint. """ class StubQuery(client.Query): @@ -208,6 +232,346 @@ d = s3.get_bucket_location("mybucket") return d.addCallback(check_results) + def test_get_bucket_lifecycle_multiple_rules(self): + """ + L{S3Client.get_bucket_lifecycle} creates a L{Query} to get a bucket's + lifecycle. It parses the returned C{LifecycleConfiguration} XML + document and returns a C{Deferred} that requests the bucket's lifecycle + configuration with multiple rules. + """ + + class StubQuery(client.Query): + + def __init__(query, action, creds, endpoint, bucket=None, + object_name=None): + super(StubQuery, query).__init__(action=action, creds=creds, + bucket=bucket, + object_name=object_name) + self.assertEquals(action, "GET") + self.assertEqual(creds.access_key, "foo") + self.assertEqual(creds.secret_key, "bar") + self.assertEqual(query.bucket, "mybucket") + self.assertEqual(query.object_name, "?lifecycle") + self.assertEqual(query.data, "") + self.assertEqual(query.metadata, {}) + self.assertEqual(query.amz_headers, {}) + + def submit(query, url_context=None): + return succeed(payload. + sample_s3_get_bucket_lifecycle_multiple_rules_result) + + def check_results(lifecycle_config): + self.assertTrue(len(lifecycle_config.rules) == 2) + rule = lifecycle_config.rules[1] + self.assertEquals(rule.id, 'another-id') + self.assertEquals(rule.prefix, 'another-logs') + self.assertEquals(rule.status, 'Disabled') + self.assertEquals(rule.expiration, 37) + + creds = AWSCredentials("foo", "bar") + s3 = client.S3Client(creds, query_factory=StubQuery) + d = s3.get_bucket_lifecycle("mybucket") + return d.addCallback(check_results) + + def test_get_bucket_lifecycle(self): + """ + L{S3Client.get_bucket_lifecycle} creates a L{Query} to get a bucket's + lifecycle. It parses the returned C{LifecycleConfiguration} XML + document and returns a C{Deferred} that requests the bucket's lifecycle + configuration. + """ + + class StubQuery(client.Query): + + def __init__(query, action, creds, endpoint, bucket=None, + object_name=None): + super(StubQuery, query).__init__(action=action, creds=creds, + bucket=bucket, + object_name=object_name) + self.assertEquals(action, "GET") + self.assertEqual(creds.access_key, "foo") + self.assertEqual(creds.secret_key, "bar") + self.assertEqual(query.bucket, "mybucket") + self.assertEqual(query.object_name, "?lifecycle") + self.assertEqual(query.data, "") + self.assertEqual(query.metadata, {}) + self.assertEqual(query.amz_headers, {}) + + def submit(query, url_context=None): + return succeed(payload.sample_s3_get_bucket_lifecycle_result) + + def check_results(lifecycle_config): + rule = lifecycle_config.rules[0] + self.assertEquals(rule.id, '30-day-log-deletion-rule') + self.assertEquals(rule.prefix, 'logs') + self.assertEquals(rule.status, 'Enabled') + self.assertEquals(rule.expiration, 30) + + creds = AWSCredentials("foo", "bar") + s3 = client.S3Client(creds, query_factory=StubQuery) + d = s3.get_bucket_lifecycle("mybucket") + return d.addCallback(check_results) + + def test_get_bucket_website_config(self): + """ + L{S3Client.get_bucket_website_config} creates a L{Query} to get a + bucket's website configurtion. It parses the returned + C{WebsiteConfiguration} XML document and returns a C{Deferred} that + requests the bucket's website configuration. + """ + + class StubQuery(client.Query): + + def __init__(query, action, creds, endpoint, bucket=None, + object_name=None): + super(StubQuery, query).__init__(action=action, creds=creds, + bucket=bucket, + object_name=object_name) + self.assertEquals(action, "GET") + self.assertEqual(creds.access_key, "foo") + self.assertEqual(creds.secret_key, "bar") + self.assertEqual(query.bucket, "mybucket") + self.assertEqual(query.object_name, "?website") + self.assertEqual(query.data, "") + self.assertEqual(query.metadata, {}) + self.assertEqual(query.amz_headers, {}) + + def submit(query, url_context=None): + return succeed(payload. + sample_s3_get_bucket_website_no_error_result) + + def check_results(website_config): + self.assertEquals(website_config.index_suffix, "index.html") + self.assertEquals(website_config.error_key, None) + + creds = AWSCredentials("foo", "bar") + s3 = client.S3Client(creds, query_factory=StubQuery) + d = s3.get_bucket_website_config("mybucket") + return d.addCallback(check_results) + + def test_get_bucket_website_config_with_error_doc(self): + """ + L{S3Client.get_bucket_website_config} creates a L{Query} to get a + bucket's website configurtion. It parses the returned + C{WebsiteConfiguration} XML document and returns a C{Deferred} that + requests the bucket's website configuration with the error document. + """ + + class StubQuery(client.Query): + + def __init__(query, action, creds, endpoint, bucket=None, + object_name=None): + super(StubQuery, query).__init__(action=action, creds=creds, + bucket=bucket, + object_name=object_name) + self.assertEquals(action, "GET") + self.assertEqual(creds.access_key, "foo") + self.assertEqual(creds.secret_key, "bar") + self.assertEqual(query.bucket, "mybucket") + self.assertEqual(query.object_name, "?website") + self.assertEqual(query.data, "") + self.assertEqual(query.metadata, {}) + self.assertEqual(query.amz_headers, {}) + + def submit(query, url_context=None): + return succeed(payload.sample_s3_get_bucket_website_result) + + def check_results(website_config): + self.assertEquals(website_config.index_suffix, "index.html") + self.assertEquals(website_config.error_key, "404.html") + + creds = AWSCredentials("foo", "bar") + s3 = client.S3Client(creds, query_factory=StubQuery) + d = s3.get_bucket_website_config("mybucket") + return d.addCallback(check_results) + + def test_get_bucket_notification_config(self): + """ + L{S3Client.get_bucket_notification_config} creates a L{Query} to get a + bucket's notification configuration. It parses the returned + C{NotificationConfiguration} XML document and returns a C{Deferred} + that requests the bucket's notification configuration. + """ + + class StubQuery(client.Query): + + def __init__(query, action, creds, endpoint, bucket=None, + object_name=None): + super(StubQuery, query).__init__(action=action, creds=creds, + bucket=bucket, + object_name=object_name) + self.assertEquals(action, "GET") + self.assertEqual(creds.access_key, "foo") + self.assertEqual(creds.secret_key, "bar") + self.assertEqual(query.bucket, "mybucket") + self.assertEqual(query.object_name, "?notification") + self.assertEqual(query.data, "") + self.assertEqual(query.metadata, {}) + self.assertEqual(query.amz_headers, {}) + + def submit(query, url_context=None): + return succeed(payload. + sample_s3_get_bucket_notification_result) + + def check_results(notification_config): + self.assertEquals(notification_config.topic, None) + self.assertEquals(notification_config.event, None) + + creds = AWSCredentials("foo", "bar") + s3 = client.S3Client(creds, query_factory=StubQuery) + d = s3.get_bucket_notification_config("mybucket") + return d.addCallback(check_results) + + def test_get_bucket_notification_config_with_topic(self): + """ + L{S3Client.get_bucket_notification_config} creates a L{Query} to get a + bucket's notification configuration. It parses the returned + C{NotificationConfiguration} XML document and returns a C{Deferred} + that requests the bucket's notification configuration with a topic. + """ + + class StubQuery(client.Query): + + def __init__(query, action, creds, endpoint, bucket=None, + object_name=None): + super(StubQuery, query).__init__(action=action, creds=creds, + bucket=bucket, + object_name=object_name) + self.assertEquals(action, "GET") + self.assertEqual(creds.access_key, "foo") + self.assertEqual(creds.secret_key, "bar") + self.assertEqual(query.bucket, "mybucket") + self.assertEqual(query.object_name, "?notification") + self.assertEqual(query.data, "") + self.assertEqual(query.metadata, {}) + self.assertEqual(query.amz_headers, {}) + + def submit(query, url_context=None): + return succeed( + payload. + sample_s3_get_bucket_notification_with_topic_result) + + def check_results(notification_config): + self.assertEquals(notification_config.topic, + "arn:aws:sns:us-east-1:123456789012:myTopic") + self.assertEquals(notification_config.event, + "s3:ReducedRedundancyLostObject") + + creds = AWSCredentials("foo", "bar") + s3 = client.S3Client(creds, query_factory=StubQuery) + d = s3.get_bucket_notification_config("mybucket") + return d.addCallback(check_results) + + def test_get_bucket_versioning_config(self): + """ + L{S3Client.get_bucket_versioning_configuration} creates a L{Query} to + get a bucket's versioning status. It parses the returned + C{VersioningConfiguration} XML document and returns a C{Deferred} that + requests the bucket's versioning configuration. + """ + + class StubQuery(client.Query): + + def __init__(query, action, creds, endpoint, bucket=None, + object_name=None): + super(StubQuery, query).__init__(action=action, creds=creds, + bucket=bucket, + object_name=object_name) + self.assertEquals(action, "GET") + self.assertEqual(creds.access_key, "foo") + self.assertEqual(creds.secret_key, "bar") + self.assertEqual(query.bucket, "mybucket") + self.assertEqual(query.object_name, "?versioning") + self.assertEqual(query.data, "") + self.assertEqual(query.metadata, {}) + self.assertEqual(query.amz_headers, {}) + + def submit(query, url_context=None): + return succeed(payload.sample_s3_get_bucket_versioning_result) + + def check_results(versioning_config): + self.assertEquals(versioning_config.status, None) + + creds = AWSCredentials("foo", "bar") + s3 = client.S3Client(creds, query_factory=StubQuery) + d = s3.get_bucket_versioning_config("mybucket") + return d.addCallback(check_results) + + def test_get_bucket_versioning_config_enabled(self): + """ + L{S3Client.get_bucket_versioning_config} creates a L{Query} to get a + bucket's versioning configuration. It parses the returned + C{VersioningConfiguration} XML document and returns a C{Deferred} that + requests the bucket's versioning configuration that has a enabled + C{Status}. + """ + + class StubQuery(client.Query): + + def __init__(query, action, creds, endpoint, bucket=None, + object_name=None): + super(StubQuery, query).__init__(action=action, creds=creds, + bucket=bucket, + object_name=object_name) + self.assertEquals(action, "GET") + self.assertEqual(creds.access_key, "foo") + self.assertEqual(creds.secret_key, "bar") + self.assertEqual(query.bucket, "mybucket") + self.assertEqual(query.object_name, "?versioning") + self.assertEqual(query.data, "") + self.assertEqual(query.metadata, {}) + self.assertEqual(query.amz_headers, {}) + + def submit(query, url_context=None): + return succeed(payload. + sample_s3_get_bucket_versioning_enabled_result) + + def check_results(versioning_config): + self.assertEquals(versioning_config.status, 'Enabled') + + creds = AWSCredentials("foo", "bar") + s3 = client.S3Client(creds, query_factory=StubQuery) + d = s3.get_bucket_versioning_config("mybucket") + return d.addCallback(check_results) + + def test_get_bucket_versioning_config_mfa_disabled(self): + """ + L{S3Client.get_bucket_versioning_config} creates a L{Query} to get a + bucket's versioning configuration. It parses the returned + C{VersioningConfiguration} XML document and returns a C{Deferred} that + requests the bucket's versioning configuration that has a disabled + C{MfaDelete}. + """ + + class StubQuery(client.Query): + + def __init__(query, action, creds, endpoint, bucket=None, + object_name=None): + super(StubQuery, query).__init__(action=action, creds=creds, + bucket=bucket, + object_name=object_name) + self.assertEquals(action, "GET") + self.assertEqual(creds.access_key, "foo") + self.assertEqual(creds.secret_key, "bar") + self.assertEqual(query.bucket, "mybucket") + self.assertEqual(query.object_name, "?versioning") + self.assertEqual(query.data, "") + self.assertEqual(query.metadata, {}) + self.assertEqual(query.amz_headers, {}) + + def submit(query, url_context=None): + return succeed( + payload. + sample_s3_get_bucket_versioning_mfa_disabled_result) + + def check_results(versioning_config): + self.assertEquals(versioning_config.mfa_delete, 'Disabled') + + creds = AWSCredentials("foo", "bar") + s3 = client.S3Client(creds, query_factory=StubQuery) + d = s3.get_bucket_versioning_config("mybucket") + return d.addCallback(check_results) + def test_delete_bucket(self): class StubQuery(client.Query): @@ -500,6 +864,39 @@ s3 = client.S3Client(creds, query_factory=StubQuery) return s3.delete_object("mybucket", "objectname") + def test_put_object_acl(self): + + class StubQuery(client.Query): + + def __init__(query, action, creds, endpoint, bucket=None, + object_name=None, data=""): + super(StubQuery, query).__init__(action=action, creds=creds, + bucket=bucket, + object_name=object_name, + data=data) + self.assertEquals(action, "PUT") + self.assertEqual(creds.access_key, "foo") + self.assertEqual(creds.secret_key, "bar") + self.assertEqual(query.bucket, "mybucket") + self.assertEqual(query.object_name, "myobject?acl") + self.assertEqual(query.data, + payload.sample_access_control_policy_result) + self.assertEqual(query.metadata, {}) + self.assertEqual(query.metadata, {}) + + def submit(query, url_context=None): + return succeed(payload.sample_access_control_policy_result) + + def check_result(result): + self.assert_(isinstance(result, AccessControlPolicy)) + + creds = AWSCredentials("foo", "bar") + s3 = client.S3Client(creds, query_factory=StubQuery) + policy = AccessControlPolicy.from_xml( + payload.sample_access_control_policy_result) + deferred = s3.put_object_acl("mybucket", "myobject", policy) + return deferred.addCallback(check_result) + def test_get_object_acl(self): class StubQuery(client.Query): @@ -611,7 +1008,7 @@ def test_get_canonicalized_resource(self): query = client.Query(action="PUT", bucket="images") result = query.get_canonicalized_resource() - self.assertEquals(result, "/images") + self.assertEquals(result, "/images/") def test_get_canonicalized_resource_with_object_name(self): query = client.Query( @@ -619,6 +1016,12 @@ result = query.get_canonicalized_resource() self.assertEquals(result, "/images/advicedog.jpg") + def test_get_canonicalized_resource_with_slashed_object_name(self): + query = client.Query( + action="PUT", bucket="images", object_name="/advicedog.jpg") + result = query.get_canonicalized_resource() + self.assertEquals(result, "/images/advicedog.jpg") + def test_sign(self): query = client.Query(action="PUT", creds=self.creds) signed = query.sign({}) @@ -706,7 +1109,7 @@ QueryTestCase.skip = s3clientSkip -class MiscellaneousTests(TXAWSTestCase): +class MiscellaneousTestCase(TXAWSTestCase): def test_content_md5(self): self.assertEqual(calculate_md5("somedata"), "rvr3UC1SmUw7AZV2NqPN0g==") diff -Nru txaws-0.2/txaws/server/call.py txaws-0.2.3/txaws/server/call.py --- txaws-0.2/txaws/server/call.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/server/call.py 2012-04-11 14:19:09.000000000 +0000 @@ -20,7 +20,7 @@ calling the C{parse} method. @ivar rest: Extra parameters not included in the given arguments schema, it will be available after calling the L{parse} method. - @ivar version: The version of the API call. Defaults to 2008-12-01. + @ivar version: The version of the API call. Defaults to 2009-11-30. """ def __init__(self, raw_params=None, principal=None, action=None, diff -Nru txaws-0.2/txaws/server/exception.py txaws-0.2.3/txaws/server/exception.py --- txaws-0.2/txaws/server/exception.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/server/exception.py 2012-04-11 14:19:09.000000000 +0000 @@ -23,3 +23,10 @@ if self.code is not None or self.message is not None: raise RuntimeError("If the full response payload is passed, " "code and message must not be set.") + + def __str__(self): + # This avoids an exception when twisted logger logs the message, as it + # currently doesn't support unicode. + if self.message is not None: + return self.message.encode("ascii", "replace") + return "" diff -Nru txaws-0.2/txaws/server/method.py txaws-0.2.3/txaws/server/method.py --- txaws-0.2/txaws/server/method.py 1970-01-01 00:00:00.000000000 +0000 +++ txaws-0.2.3/txaws/server/method.py 2012-04-11 14:19:09.000000000 +0000 @@ -0,0 +1,53 @@ +def method(method_class): + """Decorator to use to mark an API method. + + When invoking L{Registry.scan} the classes marked with this decorator + will be added to the registry. + + @param method_class: The L{Method} class to register. + """ + + def callback(scanner, name, method_class): + if method_class.actions is not None: + actions = method_class.actions + else: + actions = [name] + + if method_class.versions is not None: + versions = method_class.versions + else: + versions = [None] + + for action in actions: + for version in versions: + scanner.registry.add(method_class, + action=action, + version=version) + + from venusian import attach + attach(method_class, callback, category="method") + return method_class + + +class Method(object): + """Handle a single HTTP request to an API resource. + + @cvar actions: List of actions that the Method can handle, if C{None} + the class name will be used as only supported action. + @cvar versions: List of versions that the Method can handle, if C{None} + all versions will be supported. + """ + actions = None + versions = None + + def invoke(self, call): + """Invoke this method for executing the given C{call}.""" + raise NotImplemented("Sub-classes have to implement the invoke method") + + def is_available(self): + """Return a boolean indicating wether this method is available. + + Override this to dynamically decide at run-time whether specific + methods are available or not. + """ + return True diff -Nru txaws-0.2/txaws/server/registry.py txaws-0.2.3/txaws/server/registry.py --- txaws-0.2/txaws/server/registry.py 1970-01-01 00:00:00.000000000 +0000 +++ txaws-0.2.3/txaws/server/registry.py 2012-04-11 14:19:09.000000000 +0000 @@ -0,0 +1,54 @@ +from txaws.server.exception import APIError + + +class Registry(object): + """Register API L{Method}s. for handling specific actions and versions""" + + def __init__(self): + self._by_action = {} + + def add(self, method_class, action, version=None): + """Add a method class to the regitry. + + @param method_class: The method class to add + @param action: The action that the method class can handle + @param version: The version that the method class can handle + """ + by_version = self._by_action.setdefault(action, {}) + if version in by_version: + raise RuntimeError("A method was already registered for action" + " %s in version %s" % (action, version)) + by_version[version] = method_class + + def check(self, action, version=None): + """Check if the given action is supported in the given version. + + @raises APIError: If there's no method class registered for handling + the given action or version. + """ + if action not in self._by_action: + raise APIError(400, "InvalidAction", "The action %s is not valid " + "for this web service." % action) + by_version = self._by_action[action] + if None not in by_version: + # There's no catch-all method, let's try the version-specific one + if version not in by_version: + raise APIError(400, "InvalidVersion", "Invalid API version.") + + def get(self, action, version=None): + """Get the method class handing the given action and version.""" + by_version = self._by_action[action] + if version in by_version: + return by_version[version] + else: + return by_version[None] + + def scan(self, module, onerror=None, ignore=None): + """Scan the given module object for L{Method}s and register them.""" + from venusian import Scanner + scanner = Scanner(registry=self) + kwargs = {"onerror": onerror, "categories": ["method"]} + if ignore is not None: + # Only pass it if specified, for backward compatibility + kwargs["ignore"] = ignore + scanner.scan(module, **kwargs) diff -Nru txaws-0.2/txaws/server/resource.py txaws-0.2.3/txaws/server/resource.py --- txaws-0.2/txaws/server/resource.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/server/resource.py 2012-04-11 14:19:09.000000000 +0000 @@ -1,8 +1,9 @@ from datetime import datetime, timedelta from uuid import uuid4 -from pytz import UTC +from dateutil.tz import tzutc from twisted.python import log +from twisted.python.reflect import safe_str from twisted.internet.defer import maybeDeferred from twisted.web.resource import Resource from twisted.web.server import NOT_DONE_YET @@ -19,6 +20,8 @@ class QueryAPI(Resource): """Base class for EC2-like query APIs. + @param registry: The L{Registry} to use to look up L{Method}s for handling + the API requests. @param path: Optionally, the actual resource path the clients are using when sending HTTP requests to this API, to take into account when validating the signature. This can differ from the one in the HTTP @@ -29,8 +32,6 @@ The following class variables must be defined by sub-classes: - @ivar actions: The actions that the API supports. The 'Action' field of - the request must contain one of these. @ivar signature_versions: A list of allowed values for 'SignatureVersion'. @cvar content_type: The content type to set the 'Content-Type' header to. """ @@ -42,15 +43,30 @@ RawStr("AWSAccessKeyId"), Date("Timestamp", optional=True), Date("Expires", optional=True), - Unicode("Version", optional=True), + RawStr("Version", optional=True), Enum("SignatureMethod", {"HmacSHA256": "sha256", "HmacSHA1": "sha1"}, optional=True, default="HmacSHA256"), Unicode("Signature"), Integer("SignatureVersion", optional=True, default=2)) - def __init__(self, path=None): + def __init__(self, registry=None, path=None): Resource.__init__(self) self.path = path + self.registry = registry + + def get_method(self, call, *args, **kwargs): + """Return the L{Method} instance to invoke for the given L{Call}. + + @param args: Positional arguments to pass to the method constructor. + @param kwargs: Keyword arguments to pass to the method constructor. + """ + method_class = self.registry.get(call.action, call.version) + method = method_class(*args, **kwargs) + if not method.is_available(): + raise APIError(400, "InvalidAction", "The action %s is not " + "valid for this web service." % call.action) + else: + return method def get_principal(self, access_key): """Return a principal object by access key. @@ -83,14 +99,22 @@ return response def write_error(failure): - log.err(failure) if failure.check(APIError): status = failure.value.status + + # Don't log the stack traces for 4xx responses. + if status < 400 or status >= 500: + log.err(failure) + else: + log.msg("status: %s message: %s" % ( + status, safe_str(failure.value))) + bytes = failure.value.response if bytes is None: bytes = self.dump_error(failure.value, request) else: - bytes = str(failure.value) + log.err(failure) + bytes = safe_str(failure.value) status = 500 request.setResponseCode(status) request.write(bytes) @@ -108,6 +132,16 @@ """ raise NotImplementedError("Must be implemented by subclass.") + def dump_result(self, result): + """Serialize the result of the method invokation. + + @param result: The L{Method} result to serialize. + """ + return result + + def authorize(self, method, call): + """Authorize to invoke the given L{Method} with the given L{Call}.""" + def execute(self, call): """Execute an API L{Call}. @@ -118,11 +152,14 @@ @raises: An L{APIError} in case the execution fails, sporting an error message the HTTP status code to return. """ - raise NotImplementedError() + method = self.get_method(call) + deferred = maybeDeferred(self.authorize, method, call) + deferred.addCallback(lambda _: method.invoke(call)) + return deferred.addCallback(self.dump_result) def get_utc_time(self): """Return a C{datetime} object with the current time in UTC.""" - return datetime.now(UTC) + return datetime.now(tzutc()) def _validate(self, request): """Validate an L{HTTPRequest} before executing it. @@ -142,7 +179,7 @@ params = dict((k, v[-1]) for k, v in request.args.iteritems()) args, rest = self.schema.extract(params) - self._validate_generic_parameters(args, self.get_utc_time()) + self._validate_generic_parameters(args) def create_call(principal): self._validate_principal(principal, args) @@ -157,11 +194,10 @@ deferred.addCallback(create_call) return deferred - def _validate_generic_parameters(self, args, utc_now): + def _validate_generic_parameters(self, args): """Validate the generic request parameters. @param args: Parsed schema arguments. - @param utc_now: The current UTC time in datetime format. @raises APIError: In the following cases: - Action is not included in C{self.actions} - SignatureVersion is not included in C{self.signature_versions} @@ -169,9 +205,15 @@ - Expires is before the current time - Timestamp is older than 15 minutes. """ - if not args.Action in self.actions: - raise APIError(400, "InvalidAction", "The action %s is not valid " - "for this web service." % args.Action) + utc_now = self.get_utc_time() + + if getattr(self, "actions", None) is not None: + # Check the deprecated 'actions' attribute + if not args.Action in self.actions: + raise APIError(400, "InvalidAction", "The action %s is not " + "valid for this web service." % args.Action) + else: + self.registry.check(args.Action, args.Version) if not args.SignatureVersion in self.signature_versions: raise APIError(403, "InvalidSignature", "SignatureVersion '%s' " diff -Nru txaws-0.2/txaws/server/schema.py txaws-0.2.3/txaws/server/schema.py --- txaws-0.2/txaws/server/schema.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/server/schema.py 2012-04-11 14:19:09.000000000 +0000 @@ -1,9 +1,8 @@ from datetime import datetime from operator import itemgetter -from pytz import UTC - -from zope.datetime import parse, SyntaxError +from dateutil.tz import tzutc +from dateutil.parser import parse from txaws.server.exception import APIError @@ -65,16 +64,18 @@ @param min: Minimum value for a parameter. @param max: Maximum value for a parameter. @param allow_none: Whether the parameter may be C{None}. + @param validator: A callable to validate the parameter, returning a bool. """ def __init__(self, name, optional=False, default=None, - min=None, max=None, allow_none=False): + min=None, max=None, allow_none=False, validator=None): self.name = name self.optional = optional self.default = default self.min = min self.max = max self.allow_none = allow_none + self.validator = validator def coerce(self, value): """Coerce a single value according to this parameter's settings. @@ -92,12 +93,19 @@ if not self.allow_none: raise MissingParameterError(self.name) return self.default - self._check_range(value) try: - return self.parse(value) + self._check_range(value) + parsed = self.parse(value) + if self.validator and not self.validator(parsed): + raise ValueError(value) + return parsed except ValueError: - raise InvalidParameterValueError("Invalid %s value %s" % - (self.kind, value)) + try: + value = value.decode("utf-8") + message = "Invalid %s value %s" % (self.kind, value) + except UnicodeDecodeError: + message = "Invalid %s value" % self.kind + raise InvalidParameterValueError(message) def _check_range(self, value): """Check that the given C{value} is in the expected range.""" @@ -171,15 +179,23 @@ kind = "integer" + lower_than_min_template = "Value must be at least %s." + greater_than_max_template = "Value exceeds maximum of %s." + + def __init__(self, name, optional=False, default=None, + min=0, max=None, allow_none=False, validator=None): + super(Integer, self).__init__(name, optional, default, min, max, + allow_none, validator) + def parse(self, value): - number = int(value) - if number < 0: - raise ValueError() - return number + return int(value) def format(self, value): return str(value) + def measure(self, value): + return int(value) + class Bool(Parameter): """A parameter that must be a C{bool}.""" @@ -233,10 +249,7 @@ kind = "date" def parse(self, value): - try: - return datetime(*parse(value, local=False)[:6], tzinfo=UTC) - except (TypeError, SyntaxError): - raise ValueError() + return parse(value).replace(tzinfo=tzutc()) def format(self, value): # Convert value to UTC. @@ -488,3 +501,15 @@ else: # None is discarded. pass + + def extend(self, *schema_items): + """ + Add any number of schema items to a new schema. + """ + parameters = self._parameters.values() + for item in schema_items: + if isinstance(item, Parameter): + parameters.append(item) + else: + raise TypeError("Illegal argument %s" % item) + return Schema(*parameters) diff -Nru txaws-0.2/txaws/server/tests/fixtures/amodule.py txaws-0.2.3/txaws/server/tests/fixtures/amodule.py --- txaws-0.2/txaws/server/tests/fixtures/amodule.py 1970-01-01 00:00:00.000000000 +0000 +++ txaws-0.2.3/txaws/server/tests/fixtures/amodule.py 2012-04-11 14:19:09.000000000 +0000 @@ -0,0 +1,7 @@ +from txaws.server.tests.fixtures import method +from txaws.server.method import Method + + +@method +class TestMethod(Method): + pass diff -Nru txaws-0.2/txaws/server/tests/fixtures/importerror/amodule.py txaws-0.2.3/txaws/server/tests/fixtures/importerror/amodule.py --- txaws-0.2/txaws/server/tests/fixtures/importerror/amodule.py 1970-01-01 00:00:00.000000000 +0000 +++ txaws-0.2.3/txaws/server/tests/fixtures/importerror/amodule.py 2012-04-11 14:19:09.000000000 +0000 @@ -0,0 +1,7 @@ +from txaws.server.method import Method +from txaws.server.tests.fixtures import method + + +@method +class TestMethod(Method): + pass diff -Nru txaws-0.2/txaws/server/tests/fixtures/importerror/submodule/will_raise_import_error.py txaws-0.2.3/txaws/server/tests/fixtures/importerror/submodule/will_raise_import_error.py --- txaws-0.2/txaws/server/tests/fixtures/importerror/submodule/will_raise_import_error.py 1970-01-01 00:00:00.000000000 +0000 +++ txaws-0.2.3/txaws/server/tests/fixtures/importerror/submodule/will_raise_import_error.py 2012-04-11 14:19:09.000000000 +0000 @@ -0,0 +1 @@ +import doesnt.exist diff -Nru txaws-0.2/txaws/server/tests/fixtures/__init__.py txaws-0.2.3/txaws/server/tests/fixtures/__init__.py --- txaws-0.2/txaws/server/tests/fixtures/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ txaws-0.2.3/txaws/server/tests/fixtures/__init__.py 2012-04-11 14:19:09.000000000 +0000 @@ -0,0 +1,8 @@ +try: + import venusian +except ImportError: + method = lambda function: function + has_venusian = False +else: + from txaws.server.method import method + has_venusian = True diff -Nru txaws-0.2/txaws/server/tests/test_call.py txaws-0.2.3/txaws/server/tests/test_call.py --- txaws-0.2/txaws/server/tests/test_call.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/server/tests/test_call.py 2012-04-11 14:19:09.000000000 +0000 @@ -3,12 +3,12 @@ from txaws.server.call import Call -class CallTest(TestCase): +class CallTestCase(TestCase): def test_default_version(self): """ If no version is explicitly requested, C{version} is set to - 2008-12-01, which is the earliest version we support. + 2009-11-30, which is the earliest version we support. """ call = Call() - self.assertEqual(call.version, "2008-12-01") + self.assertEqual(call.version, "2009-11-30") diff -Nru txaws-0.2/txaws/server/tests/test_exception.py txaws-0.2.3/txaws/server/tests/test_exception.py --- txaws-0.2/txaws/server/tests/test_exception.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/server/tests/test_exception.py 2012-04-11 14:19:09.000000000 +0000 @@ -1,9 +1,11 @@ +# -*- coding: utf-8 -*- + from unittest import TestCase from txaws.server.exception import APIError -class APIErrorTest(TestCase): +class APIErrorTestCase(TestCase): def test_with_no_parameters(self): """ @@ -49,3 +51,11 @@ """ error = APIError("200", response="noes") self.assertEqual(200, error.status) + + def test_with_unicode_message(self): + """ + L{APIError} will convert message to plain ASCII if converted to string. + """ + error = APIError(400, code="APIError", message=u"cittá") + self.assertEqual(u"cittá", error.message) + self.assertEqual("citt?", str(error)) diff -Nru txaws-0.2/txaws/server/tests/test_method.py txaws-0.2.3/txaws/server/tests/test_method.py --- txaws-0.2/txaws/server/tests/test_method.py 1970-01-01 00:00:00.000000000 +0000 +++ txaws-0.2.3/txaws/server/tests/test_method.py 2012-04-11 14:19:09.000000000 +0000 @@ -0,0 +1,18 @@ +from twisted.trial.unittest import TestCase + +from txaws.server.method import Method + + +class MethodTestCase(TestCase): + + def setUp(self): + super(MethodTestCase, self).setUp() + self.method = Method() + + def test_defaults(self): + """ + By default a L{Method} applies to all API versions and handles a + single action matching its class name. + """ + self.assertIdentical(None, self.method.actions) + self.assertIdentical(None, self.method.versions) diff -Nru txaws-0.2/txaws/server/tests/test_registry.py txaws-0.2.3/txaws/server/tests/test_registry.py --- txaws-0.2/txaws/server/tests/test_registry.py 1970-01-01 00:00:00.000000000 +0000 +++ txaws-0.2.3/txaws/server/tests/test_registry.py 2012-04-11 14:19:09.000000000 +0000 @@ -0,0 +1,122 @@ +from twisted.trial.unittest import TestCase + +from txaws.server.method import Method +from txaws.server.registry import Registry +from txaws.server.exception import APIError + +try: + from txaws.server.tests.fixtures import ( + has_venusian, importerror, amodule) + from txaws.server.tests.fixtures.amodule import TestMethod + from txaws.server.tests.fixtures.importerror.amodule import ( + TestMethod as testmethod) + no_class_decorators = False +except SyntaxError: + no_class_decorators = True + has_venusian = False + + +class RegistryTestCase(TestCase): + + if no_class_decorators: + skip = ("Your version of Python doesn't seem to support class " + "decorators.") + + def setUp(self): + super(RegistryTestCase, self).setUp() + self.registry = Registry() + + def test_add(self): + """ + L{MethodRegistry.add} registers a method class for the given action + and version. + """ + self.registry.add(TestMethod, "test", "1.0") + self.registry.add(TestMethod, "test", "2.0") + self.registry.check("test", "1.0") + self.registry.check("test", "2.0") + self.assertIdentical(TestMethod, self.registry.get("test", "1.0")) + self.assertIdentical(TestMethod, self.registry.get("test", "2.0")) + + def test_add_duplicate_method(self): + """ + L{MethodRegistry.add} fails if a method class for the given action + and version was already registered. + """ + + class TestMethod2(Method): + pass + + self.registry.add(TestMethod, "test", "1.0") + self.assertRaises(RuntimeError, self.registry.add, TestMethod2, + "test", "1.0") + + def test_get(self): + """ + L{MethodRegistry.get} returns the method class registered for the + given action and version. + """ + + class TestMethod2(Method): + pass + + self.registry.add(TestMethod, "test", "1.0") + self.registry.add(TestMethod, "test", "2.0") + self.registry.add(TestMethod2, "test", "3.0") + self.assertIdentical(TestMethod, self.registry.get("test", "1.0")) + self.assertIdentical(TestMethod, self.registry.get("test", "2.0")) + self.assertIdentical(TestMethod2, self.registry.get("test", "3.0")) + + def test_check_with_missing_action(self): + """ + L{MethodRegistry.get} fails if the given action is not registered. + """ + error = self.assertRaises(APIError, self.registry.check, "boom", "1.0") + self.assertEqual(400, error.status) + self.assertEqual("InvalidAction", error.code) + self.assertEqual("The action boom is not valid for this web service.", + error.message) + + def test_check_with_missing_version(self): + """ + L{MethodRegistry.get} fails if the given action is not registered. + """ + self.registry.add(TestMethod, "test", "1.0") + error = self.assertRaises(APIError, self.registry.check, "test", "2.0") + self.assertEqual(400, error.status) + self.assertEqual("InvalidVersion", error.code) + self.assertEqual("Invalid API version.", error.message) + + def test_scan(self): + """ + L{MethodRegistry.scan} registers the L{Method}s decorated with L{api}. + """ + self.registry.scan(amodule) + self.assertIdentical(TestMethod, self.registry.get("TestMethod", None)) + + def test_scan_raises_error_on_importerror(self): + """ + L{MethodRegistry.scan} raises an error by default when an error happens + and there is no onerror callback is passed. + """ + self.assertRaises(ImportError, self.registry.scan, importerror) + + def test_scan_swallows_with_onerror(self): + """ + L{MethodRegistry.scan} accepts an onerror callback that can be used to + deal with scanning errors. + """ + swallowed = [] + + def swallow(error): + swallowed.append(error) + + self.registry.scan(importerror, onerror=swallow) + self.assertEqual(1, len(swallowed)) + self.assertEqual(testmethod, self.registry.get("TestMethod")) + + if not has_venusian: + test_scan.skip = "venusian module not available" + test_scan_raises_error_on_importerror.skip = ( + "venusian module not available") + test_scan_swallows_with_onerror.skip = "venusian module not available" diff -Nru txaws-0.2/txaws/server/tests/test_resource.py txaws-0.2.3/txaws/server/tests/test_resource.py --- txaws-0.2/txaws/server/tests/test_resource.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/server/tests/test_resource.py 2012-04-11 14:19:09.000000000 +0000 @@ -1,13 +1,23 @@ -from pytz import UTC from cStringIO import StringIO from datetime import datetime +from dateutil.tz import tzutc + +try: + import json +except ImportError: + import simplejson as json + from twisted.trial.unittest import TestCase +from twisted.python.reflect import safe_str from txaws.credentials import AWSCredentials from txaws.service import AWSServiceEndpoint from txaws.ec2.client import Query +from txaws.server.method import Method +from txaws.server.registry import Registry from txaws.server.resource import QueryAPI +from txaws.server.exception import APIError class FakeRequest(object): @@ -55,6 +65,12 @@ return self.written.getvalue() +class TestMethod(Method): + + def invoke(self, call): + return "data" + + class TestPrincipal(object): def __init__(self, creds): @@ -71,7 +87,6 @@ class TestQueryAPI(QueryAPI): - actions = ["SomeAction"] signature_versions = (1, 2) content_type = "text/plain" @@ -79,22 +94,21 @@ QueryAPI.__init__(self, *args, **kwargs) self.principal = None - def execute(self, call): - return "data" - def get_principal(self, access_key): if self.principal and self.principal.access_key == access_key: return self.principal def dump_error(self, error, request): - return str("%s - %s" % (error.code, error.message)) + return str("%s - %s" % (error.code, safe_str(error.message))) -class QueryAPITest(TestCase): +class QueryAPITestCase(TestCase): def setUp(self): - super(QueryAPITest, self).setUp() - self.api = TestQueryAPI() + super(QueryAPITestCase, self).setUp() + self.registry = Registry() + self.registry.add(TestMethod, action="SomeAction", version=None) + self.api = TestQueryAPI(registry=self.registry) def test_handle(self): """ @@ -116,11 +130,46 @@ self.api.principal = TestPrincipal(creds) return self.api.handle(request).addCallback(check) + def test_handle_with_dump_result(self): + """ + L{QueryAPI.handle} serializes the action result with C{dump_result}. + """ + creds = AWSCredentials("access", "secret") + endpoint = AWSServiceEndpoint("http://uri") + query = Query(action="SomeAction", creds=creds, endpoint=endpoint) + query.sign() + request = FakeRequest(query.params, endpoint) + + def check(ignored): + self.assertEqual("data", json.loads(request.response)) + + self.api.dump_result = json.dumps + self.api.principal = TestPrincipal(creds) + return self.api.handle(request).addCallback(check) + + def test_handle_with_deprecated_actions(self): + """ + L{QueryAPI.handle} supports the legacy 'actions' attribute. + """ + self.api.actions = ["SomeAction"] + creds = AWSCredentials("access", "secret") + endpoint = AWSServiceEndpoint("http://uri") + query = Query(action="SomeAction", creds=creds, endpoint=endpoint) + query.sign() + request = FakeRequest(query.params, endpoint) + + def check(ignored): + self.assertEqual("data", request.response) + + self.api.principal = TestPrincipal(creds) + return self.api.handle(request).addCallback(check) + def test_handle_pass_params_to_call(self): """ L{QueryAPI.handle} creates a L{Call} object with the correct parameters. """ + self.registry.add(TestMethod, "SomeAction", "1.2.3") creds = AWSCredentials("access", "secret") endpoint = AWSServiceEndpoint("http://uri") query = Query(action="SomeAction", creds=creds, endpoint=endpoint, @@ -144,6 +193,29 @@ self.api.principal = TestPrincipal(creds) return self.api.handle(request).addCallback(check) + def test_handle_ensures_version_is_str(self): + """ + L{QueryAPI.schema} coerces the Version parameter to a str, in order + to let URLs built with it be str, as required by urllib.quote in + python 2.7. + """ + self.registry.add(TestMethod, "SomeAction", "1.2.3") + creds = AWSCredentials("access", "secret") + endpoint = AWSServiceEndpoint("http://uri") + query = Query(action="SomeAction", creds=creds, endpoint=endpoint, + other_params={"Version": u"1.2.3"}) + query.sign() + request = FakeRequest(query.params, endpoint) + + def execute(call): + self.assertEqual("1.2.3", call.version) + self.assertIsInstance(call.version, str) + return "ok" + + self.api.execute = execute + self.api.principal = TestPrincipal(creds) + return self.api.handle(request) + def test_handle_empty_request(self): """ If an empty request is received a message describing the API is @@ -198,7 +270,8 @@ request = FakeRequest(query.params, endpoint) def check(ignored): - self.flushLoggedErrors() + errors = self.flushLoggedErrors() + self.assertEquals(0, len(errors)) self.assertEqual("InvalidSignature - SignatureVersion '2' " "not supported", request.response) self.assertEqual(403, request.code) @@ -220,7 +293,8 @@ self.api.execute = lambda call: 1 / 0 def check(ignored): - self.flushLoggedErrors() + errors = self.flushLoggedErrors() + self.assertEquals(1, len(errors)) self.assertTrue(request.finished) self.assertEqual("integer division or modulo by zero", request.response) @@ -229,6 +303,31 @@ self.api.principal = TestPrincipal(creds) return self.api.handle(request).addCallback(check) + def test_handle_500_api_error(self): + """ + If an L{APIError} is raised with a status code superior or equal to + 500, the error is logged on the server side. + """ + creds = AWSCredentials("access", "secret") + endpoint = AWSServiceEndpoint("http://uri") + query = Query(action="SomeAction", creds=creds, endpoint=endpoint) + query.sign() + request = FakeRequest(query.params, endpoint) + + def fail_execute(call): + raise APIError(500, response="oops") + self.api.execute = fail_execute + + def check(ignored): + errors = self.flushLoggedErrors() + self.assertEquals(1, len(errors)) + self.assertTrue(request.finished) + self.assertEqual("oops", request.response) + self.assertEqual(500, request.code) + + self.api.principal = TestPrincipal(creds) + return self.api.handle(request).addCallback(check) + def test_handle_with_parameter_error(self): """ If an error occurs while parsing the parameters, L{QueryAPI.handle} @@ -242,15 +341,113 @@ request = FakeRequest(query.params, endpoint) def check(ignored): - self.flushLoggedErrors() + errors = self.flushLoggedErrors() + self.assertEquals(0, len(errors)) self.assertEqual("MissingParameter - The request must contain " "the parameter Action", request.response) self.assertEqual(400, request.code) return self.api.handle(request).addCallback(check) + def test_handle_unicode_api_error(self): + """ + If an L{APIError} contains a unicode message, L{QueryAPI} is able to + protect itself from it. + """ + creds = AWSCredentials("access", "secret") + endpoint = AWSServiceEndpoint("http://uri") + query = Query(action="SomeAction", creds=creds, endpoint=endpoint) + query.sign() + request = FakeRequest(query.params, endpoint) + + def fail_execute(call): + raise APIError(400, code="LangError", + message=u"\N{HIRAGANA LETTER A}dvanced") + self.api.execute = fail_execute + + def check(ignored): + errors = self.flushLoggedErrors() + self.assertEquals(0, len(errors)) + self.assertTrue(request.finished) + self.assertTrue(request.response.startswith("LangError")) + self.assertEqual(400, request.code) + + self.api.principal = TestPrincipal(creds) + return self.api.handle(request).addCallback(check) + + def test_handle_unicode_error(self): + """ + If an arbitrary error raised by an API method contains a unicode + message, L{QueryAPI} is able to protect itself from it. + """ + creds = AWSCredentials("access", "secret") + endpoint = AWSServiceEndpoint("http://uri") + query = Query(action="SomeAction", creds=creds, endpoint=endpoint) + query.sign() + request = FakeRequest(query.params, endpoint) + + def fail_execute(call): + raise ValueError(u"\N{HIRAGANA LETTER A}dvanced") + self.api.execute = fail_execute + + def check(ignored): + errors = self.flushLoggedErrors() + self.assertEquals(1, len(errors)) + self.assertTrue(request.finished) + self.assertIn("ValueError", request.response) + self.assertEqual(500, request.code) + + self.api.principal = TestPrincipal(creds) + return self.api.handle(request).addCallback(check) + def test_handle_with_unsupported_action(self): - """Only actions listed in L{QueryAPI.actions} are supported.""" + """Only actions registered in the L{Registry} are supported.""" + creds = AWSCredentials("access", "secret") + endpoint = AWSServiceEndpoint("http://uri") + query = Query(action="FooBar", creds=creds, endpoint=endpoint) + query.sign() + request = FakeRequest(query.params, endpoint) + + def check(ignored): + errors = self.flushLoggedErrors() + self.assertEquals(0, len(errors)) + self.assertEqual("InvalidAction - The action FooBar is not valid" + " for this web service.", request.response) + self.assertEqual(400, request.code) + + return self.api.handle(request).addCallback(check) + + def test_handle_non_available_method(self): + """Only actions registered in the L{Registry} are supported.""" + + class NonAvailableMethod(Method): + + def is_available(self): + return False + + self.registry.add(NonAvailableMethod, action="CantDoIt") + creds = AWSCredentials("access", "secret") + endpoint = AWSServiceEndpoint("http://uri") + query = Query(action="CantDoIt", creds=creds, endpoint=endpoint) + query.sign() + request = FakeRequest(query.params, endpoint) + + def check(ignored): + errors = self.flushLoggedErrors() + self.assertEquals(0, len(errors)) + self.assertEqual("InvalidAction - The action CantDoIt is not " + "valid for this web service.", request.response) + self.assertEqual(400, request.code) + + self.api.principal = TestPrincipal(creds) + return self.api.handle(request).addCallback(check) + + def test_handle_with_deprecated_actions_and_unsupported_action(self): + """ + If the deprecated L{QueryAPI.actions} attribute is set, it will be + used for looking up supported actions. + """ + self.api.actions = ["SomeAction"] creds = AWSCredentials("access", "secret") endpoint = AWSServiceEndpoint("http://uri") query = Query(action="FooBar", creds=creds, endpoint=endpoint) @@ -258,7 +455,8 @@ request = FakeRequest(query.params, endpoint) def check(ignored): - self.flushLoggedErrors() + errors = self.flushLoggedErrors() + self.assertEquals(0, len(errors)) self.assertEqual("InvalidAction - The action FooBar is not valid" " for this web service.", request.response) self.assertEqual(400, request.code) @@ -277,7 +475,8 @@ request = FakeRequest(query.params, endpoint) def check(ignored): - self.flushLoggedErrors() + errors = self.flushLoggedErrors() + self.assertEquals(0, len(errors)) self.assertEqual("AuthFailure - No user with access key 'access'", request.response) self.assertEqual(401, request.code) @@ -297,7 +496,8 @@ request = FakeRequest(query.params, endpoint) def check(ignored): - self.flushLoggedErrors() + errors = self.flushLoggedErrors() + self.assertEquals(0, len(errors)) self.assertEqual("SignatureDoesNotMatch - The request signature " "we calculated does not match the signature you " "provided. Check your key and signing method.", @@ -321,7 +521,8 @@ request = FakeRequest(query.params, endpoint) def check(ignored): - self.flushLoggedErrors() + errors = self.flushLoggedErrors() + self.assertEquals(0, len(errors)) self.assertEqual( "InvalidParameterCombination - The parameter Timestamp" " cannot be used with the parameter Expires", @@ -346,7 +547,7 @@ self.assertEqual("data", request.response) self.assertEqual(200, request.code) - now = datetime(2009, 12, 31, tzinfo=UTC) + now = datetime(2009, 12, 31, tzinfo=tzutc()) self.api.get_utc_time = lambda: now self.api.principal = TestPrincipal(creds) return self.api.handle(request).addCallback(check) @@ -364,13 +565,14 @@ request = FakeRequest(query.params, endpoint) def check(ignored): - self.flushLoggedErrors() + errors = self.flushLoggedErrors() + self.assertEquals(0, len(errors)) self.assertEqual( "RequestExpired - Request has expired. Expires date is" " 2010-01-01T12:00:00Z", request.response) self.assertEqual(400, request.code) - now = datetime(2010, 1, 1, 12, 0, 1, tzinfo=UTC) + now = datetime(2010, 1, 1, 12, 0, 1, tzinfo=tzutc()) self.api.get_utc_time = lambda: now return self.api.handle(request).addCallback(check) diff -Nru txaws-0.2/txaws/server/tests/test_schema.py txaws-0.2.3/txaws/server/tests/test_schema.py --- txaws-0.2/txaws/server/tests/test_schema.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/server/tests/test_schema.py 2012-04-11 14:19:09.000000000 +0000 @@ -1,6 +1,8 @@ +# -*- coding: utf-8 -*- + from datetime import datetime -from pytz import UTC, FixedOffset +from dateutil.tz import tzutc, tzoffset from twisted.trial.unittest import TestCase @@ -9,7 +11,7 @@ Arguments, Bool, Date, Enum, Integer, Parameter, RawStr, Schema, Unicode) -class ArgumentsTest(TestCase): +class ArgumentsTestCase(TestCase): def test_instantiate_empty(self): """Creating an L{Arguments} object.""" @@ -54,7 +56,7 @@ self.assertEqual("egg", arguments.foo[0]) -class ParameterTest(TestCase): +class ParameterTestCase(TestCase): def test_coerce(self): """ @@ -102,6 +104,19 @@ self.assertEqual("InvalidParameterValue", error.code) self.assertEqual("Invalid integer value foo", error.message) + def test_coerce_with_parameter_error_unicode(self): + """ + L{Parameter.coerce} raises an L{APIError} if an invalid value is + passed as request argument and parameter value is unicode. + """ + parameter = Parameter("Test") + parameter.parse = lambda value: int(value) + parameter.kind = "integer" + error = self.assertRaises(APIError, parameter.coerce, "citt\xc3\xa1") + self.assertEqual(400, error.status) + self.assertEqual("InvalidParameterValue", error.code) + self.assertEqual(u"Invalid integer value cittá", error.message) + def test_coerce_with_empty_strings(self): """ L{Parameter.coerce} returns C{None} if the value is an empty string and @@ -150,14 +165,41 @@ self.assertEqual("Value (longish) for parameter Test is invalid. " "3 should be enough for anybody", error.message) + def test_validator_invalid(self): + """ + L{Parameter.coerce} raises an error if the validator returns False. + """ + parameter = Parameter("Test", validator=lambda _: False) + parameter.parse = lambda value: value + parameter.kind = "test_parameter" + error = self.assertRaises(APIError, parameter.coerce, "foo") + self.assertEqual(400, error.status) + self.assertEqual("InvalidParameterValue", error.code) + self.assertEqual("Invalid test_parameter value foo", error.message) + + def test_validator_valid(self): + """ + L{Parameter.coerce} returns the correct value if validator returns + True. + """ + parameter = Parameter("Test", validator=lambda _: True) + parameter.parse = lambda value: value + parameter.kind = "test_parameter" + self.assertEqual("foo", parameter.coerce("foo")) + -class UnicodeTest(TestCase): +class UnicodeTestCase(TestCase): def test_parse(self): """L{Unicode.parse} converts the given raw C{value} to C{unicode}.""" parameter = Unicode("Test") self.assertEqual(u"foo", parameter.parse("foo")) + def test_parse_unicode(self): + """L{Unicode.parse} works with unicode input.""" + parameter = Unicode("Test") + self.assertEqual(u"cittá", parameter.parse("citt\xc3\xa1")) + def test_format(self): """L{Unicode.format} encodes the given C{unicode} with utf-8.""" parameter = Unicode("Test") @@ -179,8 +221,18 @@ self.assertEqual(400, error.status) self.assertEqual("InvalidParameterValue", error.code) + def test_invalid_unicode(self): + """ + The L{Unicode} parameter returns an error with invalid unicode data. + """ + parameter = Unicode("Test") + error = self.assertRaises(APIError, parameter.coerce, "Test\x95Error") + self.assertIn(u"Invalid unicode value", error.message) + self.assertEqual(400, error.status) + self.assertEqual("InvalidParameterValue", error.code) + -class RawStrTest(TestCase): +class RawStrTestCase(TestCase): def test_parse(self): """L{RawStr.parse} checks that the given raw C{value} is a string.""" @@ -195,7 +247,7 @@ self.assertTrue(isinstance(value, str)) -class IntegerTest(TestCase): +class IntegerTestCase(TestCase): def test_parse(self): """L{Integer.parse} converts the given raw C{value} to C{int}.""" @@ -205,15 +257,44 @@ def test_parse_with_negative(self): """L{Integer.parse} converts the given raw C{value} to C{int}.""" parameter = Integer("Test") - self.assertRaises(ValueError, parameter.parse, "-1") + error = self.assertRaises(APIError, parameter.coerce, "-1") + self.assertEqual(400, error.status) + self.assertEqual("InvalidParameterValue", error.code) + self.assertIn("Value must be at least 0.", error.message) def test_format(self): """L{Integer.format} converts the given integer to a string.""" parameter = Integer("Test") self.assertEqual("123", parameter.format(123)) + def test_min_and_max(self): + """The L{Integer} parameter properly supports ranges.""" + parameter = Integer("Test", min=2, max=4) + + error = self.assertRaises(APIError, parameter.coerce, "1") + self.assertEqual(400, error.status) + self.assertEqual("InvalidParameterValue", error.code) + self.assertIn("Value must be at least 2.", error.message) + + error = self.assertRaises(APIError, parameter.coerce, "5") + self.assertIn("Value exceeds maximum of 4.", error.message) + self.assertEqual(400, error.status) + self.assertEqual("InvalidParameterValue", error.code) + + def test_non_integer_string(self): + """ + The L{Integer} parameter raises an L{APIError} when passed non-int + values (in this case, a string). + """ + garbage = "blah" + parameter = Integer("Test") + error = self.assertRaises(APIError, parameter.coerce, garbage) + self.assertEqual(400, error.status) + self.assertEqual("InvalidParameterValue", error.code) + self.assertIn("Invalid integer value %s" % garbage, error.message) + -class BoolTest(TestCase): +class BoolTestCase(TestCase): def test_parse(self): """L{Bool.parse} converts 'true' to C{True}.""" @@ -240,7 +321,7 @@ self.assertEqual("false", parameter.format(False)) -class EnumTest(TestCase): +class EnumTestCase(TestCase): def test_parse(self): """L{Enum.parse} accepts a map for translating values.""" @@ -261,12 +342,12 @@ self.assertEqual("foo", parameter.format("bar")) -class DateTest(TestCase): +class DateTestCase(TestCase): def test_parse(self): """L{Date.parse checks that the given raw C{value} is a date/time.""" parameter = Date("Test") - date = datetime(2010, 9, 15, 23, 59, 59, tzinfo=UTC) + date = datetime(2010, 9, 15, 23, 59, 59, tzinfo=tzutc()) self.assertEqual(date, parameter.parse("2010-09-15T23:59:59Z")) def test_format(self): @@ -276,11 +357,11 @@ """ parameter = Date("Test") date = datetime(2010, 9, 15, 23, 59, 59, - tzinfo=FixedOffset(120)) + tzinfo=tzoffset('UTC', 120 * 60)) self.assertEqual("2010-09-15T21:59:59Z", parameter.format(date)) -class SchemaTest(TestCase): +class SchemaTestCase(TestCase): def test_extract(self): """ @@ -491,3 +572,21 @@ """ schema = Schema(Integer("count")) self.assertRaises(RuntimeError, schema.bundle, name="foo") + + def test_add_single_extra_schema_item(self): + """New Parameters can be added to the Schema.""" + schema = Schema(Unicode("name")) + schema = schema.extend(Unicode("computer")) + arguments, _ = schema.extract({"name": "value", "computer": "testing"}) + self.assertEqual(u"value", arguments.name) + self.assertEqual("testing", arguments.computer) + + def test_add_extra_schema_items(self): + """A list of new Parameters can be added to the Schema.""" + schema = Schema(Unicode("name")) + schema = schema.extend(Unicode("computer"), Integer("count")) + arguments, _ = schema.extract({"name": "value", "computer": "testing", + "count": "5"}) + self.assertEqual(u"value", arguments.name) + self.assertEqual("testing", arguments.computer) + self.assertEqual(5, arguments.count) diff -Nru txaws-0.2/txaws/service.py txaws-0.2.3/txaws/service.py --- txaws-0.2/txaws/service.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/service.py 2012-04-11 14:19:09.000000000 +0000 @@ -3,29 +3,35 @@ # Licenced under the txaws licence available at /LICENSE in the txaws source. from txaws.credentials import AWSCredentials +from txaws import regions from txaws.util import parse + __all__ = ["AWSServiceEndpoint", "AWSServiceRegion", "REGION_US", "REGION_EU"] -REGION_US = "US" -REGION_EU = "EU" -EC2_ENDPOINT_US = "https://us-east-1.ec2.amazonaws.com/" -EC2_ENDPOINT_EU = "https://eu-west-1.ec2.amazonaws.com/" -S3_ENDPOINT = "https://s3.amazonaws.com/" +# These old variable names are maintained for backwards compatibility. +REGION_US = regions.REGION_US +REGION_EU = regions.REGION_EU +EC2_ENDPOINT_US = regions.EC2_ENDPOINT_US +EC2_ENDPOINT_EU = regions.EC2_ENDPOINT_EU +S3_ENDPOINT = regions.S3_ENDPOINT class AWSServiceEndpoint(object): """ @param uri: The URL for the service. @param method: The HTTP method used when accessing a service. + @param ssl_hostname_verification: Whether or not SSL hotname verification + will be done when connecting to the endpoint. """ - def __init__(self, uri="", method="GET"): + def __init__(self, uri="", method="GET", ssl_hostname_verification=False): self.host = "" self.port = None self.path = "/" self.method = method + self.ssl_hostname_verification = ssl_hostname_verification self._parse_uri(uri) if not self.scheme: self.scheme = "http" diff -Nru txaws-0.2/txaws/testing/base.py txaws-0.2.3/txaws/testing/base.py --- txaws-0.2/txaws/testing/base.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/testing/base.py 2012-04-11 14:19:09.000000000 +0000 @@ -24,4 +24,3 @@ for key in set(os.environ) - set(self.orig_environ): del os.environ[key] os.environ.update(self.orig_environ) - diff -Nru txaws-0.2/txaws/testing/payload.py txaws-0.2.3/txaws/testing/payload.py --- txaws-0.2/txaws/testing/payload.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/testing/payload.py 2012-04-11 14:19:09.000000000 +0000 @@ -22,8 +22,11 @@ 16 running - domU-12-31-39-03-15-11.compute-1.internal + domU-12-31-39-03-15-11.compute-1.internal\ + ec2-75-101-245-65.compute-1.amazonaws.com + 10.0.0.1 + 75.101.245.65 c1.xlarge 2009-04-27T02:23:18.000Z @@ -58,8 +61,11 @@ 16 running - domU-12-31-39-03-15-11.compute-1.internal + domU-12-31-39-03-15-11.compute-1.internal\ + ec2-75-101-245-65.compute-1.amazonaws.com + 10.0.0.1 + 75.101.245.65 keyname 0 @@ -153,10 +159,10 @@ i-1234 - + 32 shutting-down - + 16 running @@ -164,10 +170,10 @@ i-5678 - + 32 shutting-down - + 32 shutting-down @@ -178,6 +184,43 @@ """ % (version.ec2_api,) +sample_describe_security_groups_with_openstack = """\ + + + 7d4e4dbd-0a33-4d3a-864a-b5ce0f1c9cbf + + + + + 22 + tcp + + 0.0.0.0/0 + + + 22 + + + + + + + + WebServers + UYY3TLBUXIEON5NQVUUX6OMPWBZIQNFM + + + + + + WebServers + Web servers + UYY3TLBUXIEON5NQVUUX6OMPWBZIQNFM + + + +""" % (version.ec2_api,) + sample_describe_security_groups_result = """\ @@ -457,7 +500,8 @@ InvalidGroup.InUse - Group groupID1:GroupReferredTo is used by groups: groupID2:UsingGroup + Group groupID1:GroupReferredTo is used by groups: \ +groupID2:UsingGroup 9a6df05f-9c27-47aa-81d8-6619689210cc @@ -466,7 +510,8 @@ sample_authorize_security_group = """\ - + true """ % (version.ec2_api,) @@ -613,7 +658,8 @@ gsg-keypair - 1f:51:ae:28:bf:89:e9:d8:1f:25:5d:37:2d:7d:b8:ca:9f:f5:f1:6f + 1f:51:ae:28:bf:89:e9:d8:1f:25:5d:37:2d:7d:b8:\ +ca:9f:f5:f1:6f @@ -626,11 +672,13 @@ gsg-keypair-1 - 1f:51:ae:28:bf:89:e9:d8:1f:25:5d:37:2d:7d:b8:ca:9f:f5:f1:6f + 1f:51:ae:28:bf:89:e9:d8:1f:25:5d:37:2d:7d:b8:\ +ca:9f:f5:f1:6f gsg-keypair-2 - 1f:51:ae:28:bf:89:e9:d8:1f:25:5d:37:2d:7d:b8:ca:9f:f5:f1:70 + 1f:51:ae:28:bf:89:e9:d8:1f:25:5d:37:2d:7d:b8:\ +ca:9f:f5:f1:70 @@ -641,7 +689,8 @@ example-key-name - 1f:51:ae:28:bf:89:e9:d8:1f:25:5d:37:2d:7d:b8:ca:9f:f5:f1:6f + 1f:51:ae:28:bf:89:e9:d8:1f:25:5d:37:2d:7d:b8:\ +ca:9f:f5:f1:6f -----BEGIN RSA PRIVATE KEY----- MIIEoQIBAAKCAQBuLFg5ujHrtm1jnutSuoO8Xe56LlT+HM8v/xkaa39EstM3/aFxTHgElQiJLChp HungXQ29VTc8rc1bW0lkdi23OH5eqkMHGhvEwqa0HWASUMll4o3o/IX+0f2UcPoKCOVUR+jx71Sg @@ -710,7 +759,8 @@ example-key-name - 1f:51:ae:28:bf:89:e9:d8:1f:25:5d:37:2d:7d:b8:ca:9f:f5:f1:6f + 1f:51:ae:28:bf:89:e9:d8:1f:25:5d:37:2d:7d:b8:\ +ca:9f:f5:f1:6f """ % (version.ec2_api,) @@ -800,7 +850,8 @@ InvalidClientTokenId - The AWS Access Key Id you provided does not exist in our records. + The AWS Access Key Id you provided does not exist in our\ + records. 47bfd77d-78d6-446d-be0d-f7621795dded @@ -828,7 +879,8 @@ InternalError We encountered an internal error. Please try again. A2A7E5395E27DFBB - f691zulHNsUqonsZkjhILnvWwD3ZnmOM4ObM1wXTc6xuS3GzPmjArp8QC/sGsn6K + f691zulHNsUqonsZkjhILnvWwD3ZnmOM4ObM1wXTc6xuS3GzPmjArp8QC/sGsn6K\ + """ @@ -889,7 +941,8 @@ sample_get_bucket_location_result = """\ -EU +EU\ + """ sample_request_payment = """\ @@ -903,12 +956,18 @@ SignatureDoesNotMatch - The request signature we calculated does not match the signature you provided. Check your key and signing method. - 47 45 54 0a 31 42 32 4d 32 59 38 41 73 67 54 70 67 41 6d 59 37 50 68 43 66 67 3d 3d 0a 0a 54 68 75 2c 20 30 35 20 4e 6f 76 20 32 30 30 39 20 32 31 3a 33 33 3a 32 39 20 47 4d 54 0a 2f + The request signature we calculated does not match the signature\ + you provided. Check your key and signing method. + 47 45 54 0a 31 42 32 4d 32 59 38 41 73 67 54 70 67 41 6d\ + 59 37 50 68 43 66 67 3d 3d 0a 0a 54 68 75 2c 20 30 35 20 4e 6f 76 20 32 30\ + 30 39 20 32 31 3a 33 33 3a 32 39 20 47 4d 54 0a 2f AB9216C8640751B2 - sAPBpmFdsOsgUUwtSLsiT6KIwP1mPbmrYY0xUoahzJE263qmABkTaqzGhHddgOq5 - ltowhdrbjaQ8dQc9VS5MxzJfsPJZi0BZHEzJC3r9pzU= - GET\n1B2M2Y8AsgTpgAmY7PhCfg==\n\nThu, 05 Nov 2009 21:33:29 GMT\n/ + sAPBpmFdsOsgUUwtSLsiT6KIwP1mPbmrYY0xUoahzJE263qmABkTaqzGhHddgOq5\ + + ltowhdrbjaQ8dQc9VS5MxzJfsPJZi0BZHEzJC3r9pzU= + + GET\n1B2M2Y8AsgTpgAmY7PhCfg==\n\nThu, 05 Nov 2009 21:33:29\ + GMT\n/ SOMEKEYID """ @@ -918,9 +977,11 @@ InvalidAccessKeyId - The AWS Access Key Id you provided does not exist in our records. + The AWS Access Key Id you provided does not exist in our records.\ + 0223AD81A94821CE - HAw5g9P1VkN8ztgLKFTK20CY5LmCfTwXcSths1O7UQV6NuJx2P4tmFnpuOsziwOE + HAw5g9P1VkN8ztgLKFTK20CY5LmCfTwXcSths1O7UQV6NuJx2P4tmFnpuOsziwOE\ + SOMEKEYID """ @@ -933,14 +994,16 @@ - + 8a6925ce4adf588a4f21c32aa379004fef foo@example.net FULL_CONTROL - + 8a6925ce4adf588a4f21c32aa37900feed bar@example.net @@ -949,3 +1012,80 @@ """ +sample_s3_get_bucket_lifecycle_result = """\ + + + + 30-day-log-deletion-rule + logs + Enabled + + 30 + + +""" + +sample_s3_get_bucket_lifecycle_multiple_rules_result = """\ + + + + 30-day-log-deletion-rule + logs + Enabled + + 30 + + + + another-id + another-logs + Disabled + + 37 + + +""" + +sample_s3_get_bucket_website_result = """\ + + + + index.html + + + 404.html + +""" + +sample_s3_get_bucket_website_no_error_result = """\ + + + + index.html + +""" + +sample_s3_get_bucket_notification_result = """\ +""" + +sample_s3_get_bucket_notification_with_topic_result = """\ + + + arn:aws:sns:us-east-1:123456789012:myTopic + s3:ReducedRedundancyLostObject + +""" + +sample_s3_get_bucket_versioning_result = """\ +""" + +sample_s3_get_bucket_versioning_enabled_result = """\ + + Enabled +""" + +sample_s3_get_bucket_versioning_mfa_disabled_result = """\ + + Enabled + Disabled +""" diff -Nru txaws-0.2/txaws/tests/__init__.py txaws-0.2.3/txaws/tests/__init__.py --- txaws-0.2/txaws/tests/__init__.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/tests/__init__.py 2012-04-11 14:19:09.000000000 +0000 @@ -1 +0,0 @@ - diff -Nru txaws-0.2/txaws/tests/test_credentials.py txaws-0.2.3/txaws/tests/test_credentials.py --- txaws-0.2/txaws/tests/test_credentials.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/tests/test_credentials.py 2012-04-11 14:19:09.000000000 +0000 @@ -7,7 +7,7 @@ from txaws.testing.base import TXAWSTestCase -class TestCredentials(TXAWSTestCase): +class CredentialsTestCase(TXAWSTestCase): def test_no_access_errors(self): # Without anything in os.environ, AWSService() blows up diff -Nru txaws-0.2/txaws/tests/test_service.py txaws-0.2.3/txaws/tests/test_service.py --- txaws-0.2/txaws/tests/test_service.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/tests/test_service.py 2012-04-11 14:19:09.000000000 +0000 @@ -6,7 +6,7 @@ try: from txaws.s3.client import S3Client except ImportError: - s3clientSkip = ("S3Client couldn't be imported (perhaps because epsilon, " + s3clientSkip = ("S3Client couldn't be imported (perhaps because dateutil, " "on which it depends, isn't present)") else: s3clientSkip = None diff -Nru txaws-0.2/txaws/tests/test_util.py txaws-0.2.3/txaws/tests/test_util.py --- txaws-0.2/txaws/tests/test_util.py 2011-06-14 02:33:24.000000000 +0000 +++ txaws-0.2.3/txaws/tests/test_util.py 2012-04-11 14:19:09.000000000 +0000 @@ -4,7 +4,8 @@ from txaws.util import hmac_sha1, iso8601time, parse -class MiscellaneousTests(TestCase): + +class MiscellaneousTestCase(TestCase): def test_hmac_sha1(self): cases = [ @@ -20,8 +21,8 @@ self.assertEqual(hmac_sha1(key, data), expected) def test_iso8601time(self): - self.assertEqual("2006-07-07T15:04:56Z", iso8601time((2006,7,7,15,4,56, - 0, 0, 0))) + self.assertEqual("2006-07-07T15:04:56Z", + iso8601time((2006, 7, 7, 15, 4, 56, 0, 0, 0))) class ParseUrlTestCase(TestCase): diff -Nru txaws-0.2/txaws/tests/test_wsdl.py txaws-0.2.3/txaws/tests/test_wsdl.py --- txaws-0.2/txaws/tests/test_wsdl.py 1970-01-01 00:00:00.000000000 +0000 +++ txaws-0.2.3/txaws/tests/test_wsdl.py 2012-04-11 14:19:09.000000000 +0000 @@ -0,0 +1,798 @@ +# Copyright (C) 2010-2012 Canonical Ltd. +# Licenced under the txaws licence available at /LICENSE in the txaws source. + +import os +from twisted.trial.unittest import TestCase + +from txaws.wsdl import ( + WSDLParseError, LeafSchema, NodeSchema, NodeItem, SequenceSchema, + SequenceItem, WSDLParser, etree) + + +class WsdlBaseTestCase(TestCase): + + if not etree: + skip = "lxml is either not installed or broken on your system." + + +class NodeSchemaTestCase(WsdlBaseTestCase): + + def test_create_with_bad_tag(self): + """ + L{NodeSchema.create} raises an error if the tag of the given element + doesn't match the expected one. + """ + schema = NodeSchema("foo", [LeafSchema("bar")]) + root = etree.fromstring("spam") + error = self.assertRaises(WSDLParseError, schema.create, root) + self.assertEqual("Expected response with tag 'foo', but got " + "'egg' instead", error.args[0]) + + def test_add_with_invalid_min(self): + """ + L{NodeSchema.add} allows the C{min_occurs} parameter to only be + C{None}, zero or one. + """ + schema = NodeSchema("foo") + self.assertRaises(RuntimeError, schema.add, LeafSchema("bar"), + min_occurs=-1) + self.assertRaises(RuntimeError, schema.add, LeafSchema("bar"), + min_occurs=2) + + def test_dump(self): + """ + L{NodeSchema.dump} creates an L{etree.Element} out of a L{NodeItem}. + """ + schema = NodeSchema("foo", [LeafSchema("bar")]) + foo = NodeItem(schema) + foo.bar = "spam" + self.assertEqual("spam", + etree.tostring(schema.dump(foo))) + + def test_dump_with_multiple_children(self): + """ + L{NodeSchema.dump} supports multiple children. + """ + schema = NodeSchema("foo", [LeafSchema("bar"), LeafSchema("egg")]) + foo = NodeItem(schema) + foo.bar = "spam1" + foo.egg = "spam2" + self.assertEqual("spam1spam2", + etree.tostring(schema.dump(foo))) + + def test_dump_with_missing_attribute(self): + """ + L{NodeSchema.dump} ignores missing attributes if C{min_occurs} is zero. + """ + schema = NodeSchema("foo") + schema.add(LeafSchema("bar"), min_occurs=0) + foo = NodeItem(schema) + self.assertEqual("", etree.tostring(schema.dump(foo))) + + +class NodeItemTestCase(WsdlBaseTestCase): + + def test_get(self): + """ + The child leaf elements of a L{NodeItem} can be accessed as attributes. + """ + schema = NodeSchema("foo", [LeafSchema("bar")]) + root = etree.fromstring("egg") + foo = schema.create(root) + self.assertEqual("egg", foo.bar) + + def test_get_with_many_children(self): + """ + Multiple children are supported. + """ + schema = NodeSchema("foo", [LeafSchema("bar"), LeafSchema("egg")]) + root = etree.fromstring("spam1spam2") + foo = schema.create(root) + self.assertEqual("spam1", foo.bar) + self.assertEqual("spam2", foo.egg) + + def test_get_with_namespace(self): + """ + The child leaf elements of a L{NodeItem} can be accessed as attributes. + """ + schema = NodeSchema("foo", [LeafSchema("bar")]) + root = etree.fromstring("egg") + foo = schema.create(root) + self.assertEqual("egg", foo.bar) + + def test_get_with_unknown_tag(self): + """ + An error is raised when trying to access an attribute not in the + schema. + """ + schema = NodeSchema("foo", [LeafSchema("bar")]) + root = etree.fromstring("eggboom") + foo = schema.create(root) + error = self.assertRaises(WSDLParseError, getattr, foo, "spam") + self.assertEqual("Unknown tag 'spam'", error.args[0]) + + def test_get_with_duplicate_tag(self): + """ + An error is raised when trying to access an attribute associated + with a tag that appears more than once. + """ + schema = NodeSchema("foo", [LeafSchema("bar")]) + root = etree.fromstring("spam1spam2") + item = schema.create(root) + error = self.assertRaises(WSDLParseError, getattr, item, "bar") + self.assertEqual("Duplicate tag 'bar'", error.args[0]) + + def test_get_with_missing_required_tag(self): + """ + An error is raised when trying to access a required attribute and + the associated tag is missing. + """ + schema = NodeSchema("foo", [LeafSchema("bar")]) + root = etree.fromstring("") + item = schema.create(root) + error = self.assertRaises(WSDLParseError, getattr, item, "bar") + self.assertEqual("Missing tag 'bar'", error.args[0]) + + def test_get_with_empty_required_tag(self): + """ + An error is raised if an expected required tag is found but has and + empty value. + """ + schema = NodeSchema("foo", [LeafSchema("bar")]) + root = etree.fromstring("") + item = schema.create(root) + error = self.assertRaises(WSDLParseError, getattr, item, "bar") + self.assertEqual("Missing tag 'bar'", error.args[0]) + + def test_get_with_non_required_tag(self): + """ + No error is raised if a tag is missing and its min count is zero. + """ + schema = NodeSchema("foo") + schema.add(LeafSchema("bar"), min_occurs=0) + root = etree.fromstring("") + foo = schema.create(root) + self.assertIdentical(None, foo.bar) + + def test_get_with_reserved_keyword(self): + """ + Attributes associated to tags named against required attributes can + be accessed appending a '_' to the name. + """ + schema = NodeSchema("foo", [LeafSchema("return")]) + root = etree.fromstring("true") + foo = schema.create(root) + self.assertEqual("true", foo.return_) + + def test_get_with_nested(self): + """ + It is possible to access nested nodes. + """ + schema = NodeSchema("foo", [NodeSchema("bar", [LeafSchema("egg")])]) + root = etree.fromstring("spam") + foo = schema.create(root) + self.assertEqual("spam", foo.bar.egg) + + def test_get_with_non_required_nested(self): + """ + It is possible to access a non-required nested node that has no + associated element in the XML yet, in that case a new element is + created for it. + """ + schema = NodeSchema("foo") + schema.add(NodeSchema("bar", [LeafSchema("egg")]), min_occurs=0) + root = etree.fromstring("") + foo = schema.create(root) + foo.bar.egg = "spam" + self.assertEqual("spam", + etree.tostring(schema.dump(foo))) + + def test_set_with_unknown_tag(self): + """ + An error is raised when trying to set an attribute not in the schema. + """ + schema = NodeSchema("foo") + foo = schema.create() + error = self.assertRaises(WSDLParseError, setattr, foo, "bar", "egg") + self.assertEqual("Unknown tag 'bar'", error.args[0]) + + def test_set_with_duplicate_tag(self): + """ + An error is raised when trying to set an attribute associated + with a tag that appears more than once. + """ + schema = NodeSchema("foo", [LeafSchema("bar")]) + root = etree.fromstring("spam1spam2") + foo = schema.create(root) + error = self.assertRaises(WSDLParseError, setattr, foo, "bar", "egg") + self.assertEqual("Duplicate tag 'bar'", error.args[0]) + + def test_set_with_required_tag(self): + """ + An error is raised when trying to set a required attribute to C{None}. + """ + schema = NodeSchema("foo", [LeafSchema("bar")]) + root = etree.fromstring("spam") + foo = schema.create(root) + error = self.assertRaises(WSDLParseError, setattr, foo, "bar", None) + self.assertEqual("Missing tag 'bar'", error.args[0]) + self.assertEqual("spam", foo.bar) + + def test_set_with_non_required_tag(self): + """ + It is possible to set a non-required tag value to C{None}, in that + case the element will be removed if present. + """ + schema = NodeSchema("foo") + schema.add(LeafSchema("bar"), min_occurs=0) + root = etree.fromstring("spam") + foo = schema.create(root) + foo.bar = None + self.assertEqual("", etree.tostring(schema.dump(foo))) + + def test_set_with_non_leaf_tag(self): + """ + An error is raised when trying to set a non-leaf attribute to + a value other than C{None}. + """ + schema = NodeSchema("foo", [NodeSchema("bar", [LeafSchema("egg")])]) + root = etree.fromstring("spam") + foo = schema.create(root) + error = self.assertRaises(WSDLParseError, setattr, foo, "bar", "yo") + self.assertEqual("Can't set non-leaf tag 'bar'", error.args[0]) + + def test_set_with_optional_node_tag(self): + """ + It is possible to set an optional node tag to C{None}, in that + case it will be removed from the tree. + """ + schema = NodeSchema("foo") + schema.add(NodeSchema("bar", [LeafSchema("egg")]), min_occurs=0) + root = etree.fromstring("spam") + foo = schema.create(root) + foo.bar = None + self.assertEqual("", etree.tostring(schema.dump(foo))) + + def test_set_with_sequence_tag(self): + """ + It is possible to set a sequence tag to C{None}, in that case + all its children will be removed + """ + schema = NodeSchema("foo") + schema.add(SequenceSchema("bar", + NodeSchema("item", [LeafSchema("egg")]))) + root = etree.fromstring("" + "spam<" + "/foo>") + foo = schema.create(root) + foo.bar = None + self.assertEqual("", etree.tostring(schema.dump(foo))) + + def test_set_with_required_non_leaf_tag(self): + """ + An error is raised when trying to set a required non-leaf tag + to C{None}. + """ + schema = NodeSchema("foo", [NodeSchema("bar", [LeafSchema("egg")])]) + root = etree.fromstring("spam") + foo = schema.create(root) + error = self.assertRaises(WSDLParseError, setattr, foo, "bar", None) + self.assertEqual("Missing tag 'bar'", error.args[0]) + self.assertTrue(hasattr(foo, "bar")) + + +class SequenceSchemaTestCase(WsdlBaseTestCase): + + def test_create_with_bad_tag(self): + """ + L{SequenceSchema.create} raises an error if the tag of the given + element doesn't match the expected one. + """ + schema = SequenceSchema("foo", NodeSchema("item", [LeafSchema("bar")])) + root = etree.fromstring("egg") + error = self.assertRaises(WSDLParseError, schema.create, root) + self.assertEqual("Expected response with tag 'foo', but got " + "'spam' instead", error.args[0]) + + def test_set_with_leaf(self): + """ + L{SequenceSchema.set} raises an error if the given child is a leaf node + """ + schema = SequenceSchema("foo") + error = self.assertRaises(RuntimeError, schema.set, LeafSchema("bar")) + self.assertEqual("Sequence can't have leaf children", str(error)) + + def test_set_with_previous_child(self): + """ + L{SequenceSchema.set} raises an error if the sequence has already + a child. + """ + schema = SequenceSchema("foo", NodeSchema("item", [LeafSchema("bar")])) + error = self.assertRaises(RuntimeError, schema.set, NodeSchema("egg")) + self.assertEqual("Sequence has already a child", str(error)) + + def test_set_with_no_min_or_max(self): + """ + L{SequenceSchema.set} raises an error if no values are provided for the + min and max parameters. + """ + schema = SequenceSchema("foo") + child = NodeSchema("item", [LeafSchema("bar")]) + error = self.assertRaises(RuntimeError, schema.set, child, + min_occurs=0, max_occurs=None) + self.assertEqual("Sequence node without min or max", str(error)) + error = self.assertRaises(RuntimeError, schema.set, child, + min_occurs=None, max_occurs=1) + self.assertEqual("Sequence node without min or max", str(error)) + + def test_dump(self): + """ + L{SequenceSchema.dump} creates a L{etree.Element} out of + a L{SequenceItem}. + """ + schema = SequenceSchema("foo", NodeSchema("item", [LeafSchema("bar")])) + foo = SequenceItem(schema) + foo.append().bar = "egg" + self.assertEqual("egg", + etree.tostring(schema.dump(foo))) + + def test_dump_with_many_items(self): + """ + L{SequenceSchema.dump} supports many child items in the sequence. + """ + schema = SequenceSchema("foo", NodeSchema("item", [LeafSchema("bar")])) + foo = SequenceItem(schema) + foo.append().bar = "spam0" + foo.append().bar = "spam1" + self.assertEqual("" + "spam0" + "spam1" + "", + etree.tostring(schema.dump(foo))) + + +class SequenceItemTestCase(WsdlBaseTestCase): + + def test_get(self): + """ + The child elements of a L{SequenceItem} can be accessed as attributes. + """ + schema = SequenceSchema("foo", NodeSchema("item", [LeafSchema("bar")])) + root = etree.fromstring("egg") + foo = schema.create(root) + self.assertEqual("egg", foo[0].bar) + + def test_get_items(self): + """L{SequenceItem} supports elements with many child items.""" + schema = SequenceSchema("foo", NodeSchema("item", [LeafSchema("bar")])) + root = etree.fromstring("" + "egg0" + "egg1" + "") + foo = schema.create(root) + self.assertEqual("egg0", foo[0].bar) + self.assertEqual("egg1", foo[1].bar) + + def test_get_with_namespace(self): + """ + The child elements of a L{SequenceItem} can be accessed as attributes. + """ + schema = SequenceSchema("foo", NodeSchema("item", [LeafSchema("bar")])) + root = etree.fromstring("" + "egg" + "") + foo = schema.create(root) + self.assertEqual("egg", foo[0].bar) + + def test_get_with_non_existing_index(self): + """An error is raised when trying to access a non existing item.""" + schema = SequenceSchema("foo", NodeSchema("item", [LeafSchema("bar")])) + root = etree.fromstring("egg") + foo = schema.create(root) + error = self.assertRaises(WSDLParseError, foo.__getitem__, 1) + self.assertEqual("Non existing item in tag 'foo'", error.args[0]) + + def test_get_with_index_higher_than_max(self): + """ + An error is raised when trying to access an item above the allowed + max value. + """ + schema = SequenceSchema("foo") + schema.set(NodeSchema("item", [LeafSchema("bar")]), min_occurs=0, + max_occurs=1) + root = etree.fromstring("" + "egg0" + "egg1" + "") + foo = schema.create(root) + error = self.assertRaises(WSDLParseError, foo.__getitem__, 1) + self.assertEqual("Out of range item in tag 'foo'", error.args[0]) + + def test_append(self): + """ + L{SequenceItem.append} adds a new item to the sequence, appending it + at the end. + """ + schema = SequenceSchema("foo", NodeSchema("item", [LeafSchema("bar")])) + root = etree.fromstring("egg0") + foo = schema.create(root) + foo.append().bar = "egg1" + self.assertEqual("egg1", foo[1].bar) + self.assertEqual("" + "egg0" + "egg1" + "", + etree.tostring(schema.dump(foo))) + + def test_append_with_too_many_items(self): + """ + An error is raised when trying to append items above the max. + """ + schema = SequenceSchema("foo") + schema.set(NodeSchema("item", [LeafSchema("bar")]), min_occurs=0, + max_occurs=1) + root = etree.fromstring("egg") + foo = schema.create(root) + error = self.assertRaises(WSDLParseError, foo.append) + self.assertEqual("Too many items in tag 'foo'", error.args[0]) + self.assertEqual(1, len(list(foo))) + + def test_delitem(self): + """ + L{SequenceItem.__delitem__} removes from the sequence the item with the + given index. + """ + schema = SequenceSchema("foo", NodeSchema("item", [LeafSchema("bar")])) + root = etree.fromstring("" + "egg0" + "egg1" + "") + foo = schema.create(root) + del foo[0] + self.assertEqual("egg1", foo[0].bar) + self.assertEqual("egg1", + etree.tostring(schema.dump(foo))) + + def test_delitem_with_not_enough_items(self): + """ + L{SequenceItem.__delitem__} raises an error if trying to remove an item + would make the sequence shorter than the required minimum. + """ + schema = SequenceSchema("foo") + schema.set(NodeSchema("item", [LeafSchema("bar")]), min_occurs=1, + max_occurs=10) + root = etree.fromstring("egg") + foo = schema.create(root) + error = self.assertRaises(WSDLParseError, foo.__delitem__, 0) + self.assertEqual("Not enough items in tag 'foo'", error.args[0]) + self.assertEqual(1, len(list(foo))) + + def test_remove(self): + """ + L{SequenceItem.remove} removes the given item from the sequence. + """ + schema = SequenceSchema("foo", NodeSchema("item", [LeafSchema("bar")])) + root = etree.fromstring("" + "egg0" + "egg1" + "") + foo = schema.create(root) + foo.remove(foo[0]) + self.assertEqual("egg1", foo[0].bar) + self.assertEqual("egg1", + etree.tostring(schema.dump(foo))) + + def test_remove_with_non_existing_item(self): + """ + L{SequenceItem.remove} raises an exception when trying to remove a + non existing item + """ + schema = SequenceSchema("foo", NodeSchema("item", [LeafSchema("bar")])) + root = etree.fromstring("egg") + foo = schema.create(root) + item = foo.remove(foo[0]) + error = self.assertRaises(WSDLParseError, foo.remove, item) + self.assertEqual("Non existing item in tag 'foo'", error.args[0]) + + def test_iter(self): + """L{SequenceItem} objects are iterable.""" + schema = SequenceSchema("foo", NodeSchema("item", [LeafSchema("bar")])) + root = etree.fromstring("" + "egg0" + "egg1" + "") + foo = schema.create(root) + [item0, item1] = list(foo) + self.assertEqual("egg0", item0.bar) + self.assertEqual("egg1", item1.bar) + + +class WDSLParserTestCase(WsdlBaseTestCase): + + def setUp(self): + super(WDSLParserTestCase, self).setUp() + parser = WSDLParser() + wsdl_dir = os.path.join(os.path.dirname(__file__), "../../wsdl") + wsdl_path = os.path.join(wsdl_dir, "2009-11-30.ec2.wsdl") + self.schemas = parser.parse(open(wsdl_path).read()) + + def test_parse_create_key_pair_response(self): + """Parse a CreateKeyPairResponse payload.""" + schema = self.schemas["CreateKeyPairResponse"] + xmlns = "http://ec2.amazonaws.com/doc/2008-12-01/" + xml = ("" + "65d85081-abbc" + "foo" + "9a:81:96:46" + "MIIEowIBAAKCAQEAi" + "" % xmlns) + + response = schema.create(etree.fromstring(xml)) + self.assertEqual("65d85081-abbc", response.requestId) + self.assertEqual("foo", response.keyName) + self.assertEqual("9a:81:96:46", response.keyFingerprint) + self.assertEqual("MIIEowIBAAKCAQEAi", response.keyMaterial) + self.assertEqual(xml, etree.tostring(schema.dump(response))) + + def test_parse_delete_key_pair_response(self): + """Parse a DeleteKeyPairResponse payload.""" + schema = self.schemas["DeleteKeyPairResponse"] + xmlns = "http://ec2.amazonaws.com/doc/2008-12-01/" + xml = ("" + "acc41b73-4c47-4f80" + "true" + "" % xmlns) + root = etree.fromstring(xml) + response = schema.create(root) + self.assertEqual("acc41b73-4c47-4f80", response.requestId) + self.assertEqual("true", response.return_) + self.assertEqual(xml, etree.tostring(schema.dump(response))) + + def test_parse_describe_key_pairs_response(self): + """Parse a DescribeKeyPairsResponse payload.""" + schema = self.schemas["DescribeKeyPairsResponse"] + xmlns = "http://ec2.amazonaws.com/doc/2008-12-01/" + xml = ("" + "3ef0aa1d-57dd-4272" + "" + "" + "europe-key" + "94:88:29:60:cf" + "" + "" + "" % xmlns) + root = etree.fromstring(xml) + response = schema.create(root) + self.assertEqual("3ef0aa1d-57dd-4272", response.requestId) + self.assertEqual("europe-key", response.keySet[0].keyName) + self.assertEqual("94:88:29:60:cf", response.keySet[0].keyFingerprint) + self.assertEqual(xml, etree.tostring(schema.dump(response))) + + def test_modify_describe_key_pairs_response(self): + """Modify a DescribeKeyPairsResponse payload.""" + schema = self.schemas["DescribeKeyPairsResponse"] + xmlns = "http://ec2.amazonaws.com/doc/2008-12-01/" + xml = ("" + "3ef0aa1d-57dd-4272" + "" + "" + "europe-key" + "94:88:29:60:cf" + "" + "" + "" % xmlns) + root = etree.fromstring(xml) + response = schema.create(root) + response.keySet[0].keyName = "new-key" + xml = ("" + "3ef0aa1d-57dd-4272" + "" + "" + "new-key" + "94:88:29:60:cf" + "" + "" + "" % xmlns) + self.assertEqual(xml, etree.tostring(schema.dump(response))) + + def test_create_describe_key_pairs_response(self): + """Create a DescribeKeyPairsResponse payload.""" + schema = self.schemas["DescribeKeyPairsResponse"] + xmlns = "http://ec2.amazonaws.com/doc/2008-12-01/" + response = schema.create(namespace=xmlns) + response.requestId = "abc" + key = response.keySet.append() + key.keyName = "some-key" + key.keyFingerprint = "11:22:33:44" + xml = ("" + "abc" + "" + "" + "some-key" + "11:22:33:44" + "" + "" + "" % xmlns) + self.assertEqual(xml, etree.tostring(schema.dump(response))) + + def test_create_describe_addresses_response(self): + """Create a DescribeAddressesResponse payload. + """ + schema = self.schemas["DescribeAddressesResponse"] + xmlns = "http://ec2.amazonaws.com/doc/2008-12-01/" + response = schema.create(namespace=xmlns) + response.requestId = "abc" + address = response.addressesSet.append() + address.publicIp = "192.168.0.1" + xml = ("" + "abc" + "" + "" + "192.168.0.1" + "" + "" + "" % xmlns) + self.assertEqual(xml, etree.tostring(schema.dump(response))) + + def test_create_describe_instances_response_with_username(self): + """Create a DescribeInstancesResponse payload. + """ + schema = self.schemas["DescribeInstancesResponse"] + xmlns = "http://ec2.amazonaws.com/doc/2008-12-01/" + response = schema.create(namespace=xmlns) + response.requestId = "abc" + reservation = response.reservationSet.append() + instance = reservation.instancesSet.append() + instance.instanceId = "i-01234567" + xml = ("" + "abc" + "" + "" + "" + "" + "i-01234567" + "" + "" + "" + "" + "" % xmlns) + self.assertEqual(xml, etree.tostring(schema.dump(response))) + + def test_create_describe_instances_response(self): + """Create a DescribeInstancesResponse payload. + """ + schema = self.schemas["DescribeInstancesResponse"] + xmlns = "http://ec2.amazonaws.com/doc/2008-12-01/" + response = schema.create(namespace=xmlns) + response.requestId = "abc" + reservation = response.reservationSet.append() + instance = reservation.instancesSet.append() + instance.instanceId = "i-01234567" + xml = ("" + "abc" + "" + "" + "" + "" + "i-01234567" + "" + "" + "" + "" + "" % xmlns) + self.assertEqual(xml, etree.tostring(schema.dump(response))) + + def test_parse_describe_security_groups_response(self): + """Parse a DescribeSecurityGroupsResponse payload.""" + schema = self.schemas["DescribeSecurityGroupsResponse"] + xmlns = "http://ec2.amazonaws.com/doc/2008-12-01/" + xml = ("" + "3ef0aa1d-57dd-4272" + "" + "" + "UYY3TLBUXIEON5NQVUUX6OMPWBZIQNFM" + "WebServers" + "Web" + "" + "" + "tcp" + "80" + "80" + "" + "" + "" + "0.0.0.0/0" + "" + "" + "" + "" + "" + "" + "" % xmlns) + root = etree.fromstring(xml) + response = schema.create(root) + self.assertEqual("3ef0aa1d-57dd-4272", response.requestId) + self.assertEqual("UYY3TLBUXIEON5NQVUUX6OMPWBZIQNFM", + response.securityGroupInfo[0].ownerId) + self.assertEqual("WebServers", response.securityGroupInfo[0].groupName) + self.assertEqual("Web", response.securityGroupInfo[0].groupDescription) + self.assertEqual(xml, etree.tostring(schema.dump(response))) + + def test_modify_describe_security_groups_response(self): + """Modify a DescribeSecurityGroupsResponse payload.""" + schema = self.schemas["DescribeSecurityGroupsResponse"] + xmlns = "http://ec2.amazonaws.com/doc/2008-12-01/" + xml = ("" + "3ef0aa1d-57dd-4272" + "" + "" + "UYY3TLBUXIEON5NQVUUX6OMPWBZIQNFM" + "WebServers" + "Web" + "" + "" + "tcp" + "80" + "80" + "" + "" + "" + "0.0.0.0/0" + "" + "" + "" + "" + "" + "" + "" % xmlns) + root = etree.fromstring(xml) + response = schema.create(root) + response.securityGroupInfo[0].ownerId = "abc123" + response.securityGroupInfo[0].groupName = "Everybody" + response.securityGroupInfo[0].groupDescription = "All People" + xml = ("" + "3ef0aa1d-57dd-4272" + "" + "" + "abc123" + "Everybody" + "All People" + "" + "" + "tcp" + "80" + "80" + "" + "" + "" + "0.0.0.0/0" + "" + "" + "" + "" + "" + "" + "" % xmlns) + self.assertEqual(xml, etree.tostring(schema.dump(response))) + + def test_create_describe_security_groups_response(self): + """Create a DescribeSecurityGroupsResponse payload.""" + schema = self.schemas["DescribeSecurityGroupsResponse"] + xmlns = "http://ec2.amazonaws.com/doc/2008-12-01/" + response = schema.create(namespace=xmlns) + response.requestId = "requestId123" + group = response.securityGroupInfo.append() + group.ownerId = "deadbeef31337" + group.groupName = "hexadecimalonly" + group.groupDescription = "All people that love hex" + xml = ("" + "requestId123" + "" + "" + "deadbeef31337" + "hexadecimalonly" + "All people that love hex" + "" + "" + "" % xmlns) + self.assertEqual(xml, etree.tostring(schema.dump(response))) diff -Nru txaws-0.2/txaws/version.py txaws-0.2.3/txaws/version.py --- txaws-0.2/txaws/version.py 2011-06-14 02:36:55.000000000 +0000 +++ txaws-0.2.3/txaws/version.py 2012-04-11 14:19:09.000000000 +0000 @@ -1,3 +1,3 @@ -txaws = "0.2" -ec2_api = "2008-12-01" +txaws = "0.2.3" +ec2_api = "2009-11-30" s3_api = "2006-03-01" diff -Nru txaws-0.2/txaws/wsdl.py txaws-0.2.3/txaws/wsdl.py --- txaws-0.2/txaws/wsdl.py 1970-01-01 00:00:00.000000000 +0000 +++ txaws-0.2.3/txaws/wsdl.py 2012-04-11 14:19:09.000000000 +0000 @@ -0,0 +1,584 @@ +# Copyright (C) 2010-2012 Canonical Ltd. +# Licenced under the txaws licence available at /LICENSE in the txaws source. + +"""Parse WSDL definitions and generate schemas. + +To understand how the machinery in this module works, let's consider the +following bit of the WSDL definition, that specifies the format for +the response of a DescribeKeyPairs query: + + + + + + + + + + + + + + + + + + + + + + +The L{WSDLParser} will take the above XML input and automatically generate +a top-level L{NodeSchema} that can be used to access and modify the XML content +of an actual DescribeKeyPairsResponse payload in an easy way. + +The automatically generated L{NodeSchema} object will be the same as the +following manually created one: + +>>> child1 = LeafSchema('requestId') +>>> sub_sub_child1 = LeafSchema('key_name') +>>> sub_sub_child2 = LeafSchema('key_fingerprint') +>>> sub_child = NodeSchema('item') +>>> sub_child.add(sub_sub_child1) +>>> sub_child.add(sub_sub_child2) +>>> child2 = SequenceSchema('keySet') +>>> child2.set(sub_child) +>>> schema = NodeSchema('DescribeKeyPairsResponse') +>>> schema.add(child1) +>>> schema.add(child2) + +Now this L{NodeSchema} object can be used to access and modify a response +XML payload, for example: + + + 3ef0aa1d-57dd-4272 + + + some-key + 94:88:29:60:cf + + + + +Let's assume to have an 'xml' variable that holds the XML payload above, now +we can: + +>>> response = schema.create(etree.fromstring(xml)) +>>> response.requestId +3ef0aa1d-57dd-4272 +>>> response.keySet[0].keyName +some-key +>>> response.keySet[0].keyFingerprint +94:88:29:60:cf + +Note that there is no upfront parsing, the schema just makes sure that +the response elements one actually accesses are consistent with the +WDSL definition and that all modifications of those items are consistent +as well. +""" +try: + from lxml import etree +except ImportError: + etree = None + + +class WSDLParseError(Exception): + """Raised when a response doesn't comply with its schema.""" + + +class LeafSchema(object): + """Schema for a single XML leaf element in a response. + + @param tag: The name of the XML element tag this schema is for. + """ + + def __init__(self, tag): + self.tag = tag + + +class NodeSchema(object): + """Schema for a single XML inner node in a response. + + A L{Node} can have other L{Node} or L{LeafSchema} objects as children. + + @param tag: The name of the XML element tag this schema is for. + @param _children: Optionally, the schemas for the child nodes, used only + by tests. + """ + + reserved = ["return"] + + def __init__(self, tag, _children=None): + self.tag = tag + self.children = {} + self.children_min_occurs = {} + if _children: + for child in _children: + self.add(child) + + def create(self, root=None, namespace=None): + """Create an inner node element. + + @param root: The inner C{etree.Element} the item will be rooted at. + @result: A L{NodeItem} with the given root, or a new one if none. + @raises L{ECResponseError}: If the given C{root} has a bad tag. + """ + if root is not None: + tag = root.tag + if root.nsmap: + namespace = root.nsmap[None] + tag = tag[len(namespace) + 2:] + if tag != self.tag: + raise WSDLParseError("Expected response with tag '%s', but " + "got '%s' instead" % (self.tag, tag)) + return NodeItem(self, root, namespace) + + def dump(self, item): + """Return the C{etree.Element} of the given L{NodeItem}. + + @param item: The L{NodeItem} to dump. + """ + return item._root + + def add(self, child, min_occurs=1): + """Add a child node. + + @param child: The schema for the child node. + @param min_occurs: The minimum number of times the child node must + occur, if C{None} is given the default is 1. + """ + if not min_occurs in (0, 1): + raise RuntimeError("Unexpected min bound for node schema") + self.children[child.tag] = child + self.children_min_occurs[child.tag] = min_occurs + return child + + +class NodeItem(object): + """An inner node item in a tree of response elements. + + @param schema: The L{NodeSchema} this item must comply to. + @param root: The C{etree.Element} this item is rooted at, if C{None} + a new one will be created. + """ + + def __init__(self, schema, root=None, namespace=None): + object.__setattr__(self, "_schema", schema) + object.__setattr__(self, "_namespace", namespace) + if root is None: + tag = self._get_namespace_tag(schema.tag) + nsmap = None + if namespace is not None: + nsmap = {None: namespace} + root = etree.Element(tag, nsmap=nsmap) + object.__setattr__(self, "_root", root) + + def __getattr__(self, name): + """Get the child item with the given C{name}. + + @raises L{WSDLParseError}: In the following cases: + - The given C{name} is not in the schema. + - There is more than one element tagged C{name} in the response. + - No matching element is found in the response and C{name} is + requred. + - A required element is present but empty. + """ + tag = self._get_tag(name) + schema = self._get_schema(tag) + + child = self._find_child(tag) + if child is None: + if isinstance(schema, LeafSchema): + return self._check_value(tag, None) + child = self._create_child(tag) + + if isinstance(schema, LeafSchema): + return self._check_value(tag, child.text) + return schema.create(child) + + def __setattr__(self, name, value): + """Set the child item with the given C{name} to the given C{value}. + + Setting a non-leaf child item to C{None} will make it disappear from + the tree completely. + + @raises L{WSDLParseError}: In the following cases: + - The given C{name} is not in the schema. + - There is more than one element tagged C{name} in the response. + - The given value is C{None} and the element is required. + - The given C{name} is associated with a non-leaf node, and + the given C{value} is not C{None}. + - The given C{name} is associated with a required non-leaf + and the given C{value} is C{None}. + """ + tag = self._get_tag(name) + schema = self._get_schema(tag) + child = self._find_child(tag) + if not isinstance(schema, LeafSchema): + if value is not None: + raise WSDLParseError("Can't set non-leaf tag '%s'" % tag) + + if isinstance(schema, NodeSchema): + # Setting a node child item to None means removing it. + self._check_value(tag, None) + if child is not None: + self._root.remove(child) + if isinstance(schema, SequenceSchema): + # Setting a sequence child item to None means removing all + # its children. + if child is None: + child = self._create_child(tag) + for item in child.getchildren(): + child.remove(item) + return + + if child is None: + child = self._create_child(tag) + child.text = self._check_value(tag, value) + if child.text is None: + self._root.remove(child) + + def _create_child(self, tag): + """Create a new child element with the given tag.""" + return etree.SubElement(self._root, self._get_namespace_tag(tag)) + + def _find_child(self, tag): + """Find the child C{etree.Element} with the matching C{tag}. + + @raises L{WSDLParseError}: If more than one such elements are found. + """ + tag = self._get_namespace_tag(tag) + children = self._root.findall(tag) + if len(children) > 1: + raise WSDLParseError("Duplicate tag '%s'" % tag) + if len(children) == 0: + return None + return children[0] + + def _check_value(self, tag, value): + """Ensure that the element matching C{tag} can have the given C{value}. + + @param tag: The tag to consider. + @param value: The value to check + @return: The unchanged L{value}, if valid. + @raises L{WSDLParseError}: If the value is invalid. + """ + if value is None: + if self._schema.children_min_occurs[tag] > 0: + raise WSDLParseError("Missing tag '%s'" % tag) + return value + return value + + def _get_tag(self, name): + """Get the L{NodeItem} attribute name for the given C{tag}.""" + if name.endswith("_"): + if name[:-1] in self._schema.reserved: + return name[:-1] + return name + + def _get_namespace_tag(self, tag): + """Return the given C{tag} with the namespace prefix added, if any.""" + if self._namespace is not None: + tag = "{%s}%s" % (self._namespace, tag) + return tag + + def _get_schema(self, tag): + """Return the child schema for the given C{tag}. + + @raises L{WSDLParseError}: If the tag doesn't belong to the schema. + """ + schema = self._schema.children.get(tag) + if not schema: + raise WSDLParseError("Unknown tag '%s'" % tag) + return schema + + def to_xml(self): + """Convert the response to bare bones XML.""" + return etree.tostring(self._root, encoding="utf-8") + + +class SequenceSchema(object): + """Schema for a single XML inner node holding a sequence of other nodes. + + @param tag: The name of the XML element tag this schema is for. + @param _child: Optionally the schema of the items in the sequence, used + by tests only. + """ + + def __init__(self, tag, _child=None): + self.tag = tag + self.child = None + if _child: + self.set(_child, 0, "unbounded") + + def create(self, root=None, namespace=None): + """Create a sequence element with the given root. + + @param root: The C{etree.Element} to root the sequence at, if C{None} a + new one will be created.. + @result: A L{SequenceItem} with the given root. + @raises L{ECResponseError}: If the given C{root} has a bad tag. + """ + if root is not None: + tag = root.tag + if root.nsmap: + namespace = root.nsmap[None] + tag = tag[len(namespace) + 2:] + if tag != self.tag: + raise WSDLParseError("Expected response with tag '%s', but " + "got '%s' instead" % (self.tag, tag)) + return SequenceItem(self, root, namespace) + + def dump(self, item): + """Return the C{etree.Element} of the given L{SequenceItem}. + + @param item: The L{SequenceItem} to dump. + """ + return item._root + + def set(self, child, min_occurs=1, max_occurs=1): + """Set the schema for the sequence children. + + @param child: The schema that children must match. + @param min_occurs: The minimum number of children the sequence + must have. + @param max_occurs: The maximum number of children the sequence + can have. + """ + if isinstance(child, LeafSchema): + raise RuntimeError("Sequence can't have leaf children") + if self.child is not None: + raise RuntimeError("Sequence has already a child") + if min_occurs is None or max_occurs is None: + raise RuntimeError("Sequence node without min or max") + if isinstance(child, LeafSchema): + raise RuntimeError("Sequence node with leaf child type") + if not child.tag == "item": + raise RuntimeError("Sequence node with bad child tag") + + self.child = child + self.min_occurs = min_occurs + self.max_occurs = max_occurs + return child + + +class SequenceItem(object): + """A sequence node item in a tree of response elements. + + @param schema: The L{SequenceSchema} this item must comply to. + @param root: The C{etree.Element} this item is rooted at, if C{None} + a new one will be created. + """ + + def __init__(self, schema, root=None, namespace=None): + if root is None: + root = etree.Element(schema.tag) + object.__setattr__(self, "_schema", schema) + object.__setattr__(self, "_root", root) + object.__setattr__(self, "_namespace", namespace) + + def __getitem__(self, index): + """Get the item with the given C{index} in the sequence. + + @raises L{WSDLParseError}: In the following cases: + - If there is no child element with the given C{index}. + - The given C{index} is higher than the allowed max. + """ + schema = self._schema.child + tag = self._schema.tag + if (self._schema.max_occurs != "unbounded" and + index > self._schema.max_occurs - 1): + raise WSDLParseError("Out of range item in tag '%s'" % tag) + child = self._get_child(self._root.getchildren(), index) + return schema.create(child) + + def append(self): + """Append a new item to the sequence, appending it to the end. + + @return: The newly created item. + @raises L{WSDLParseError}: If the operation would result in having + more child elements than the allowed max. + """ + tag = self._schema.tag + children = self._root.getchildren() + if len(children) >= self._schema.max_occurs: + raise WSDLParseError("Too many items in tag '%s'" % tag) + schema = self._schema.child + tag = "item" + if self._namespace is not None: + tag = "{%s}%s" % (self._namespace, tag) + child = etree.SubElement(self._root, tag) + return schema.create(child) + + def __delitem__(self, index): + """Remove the item with the given C{index} from the sequence. + + @raises L{WSDLParseError}: If the operation would result in having + less child elements than the required min_occurs, or if no such + index is found. + """ + tag = self._schema.tag + children = self._root.getchildren() + if len(children) <= self._schema.min_occurs: + raise WSDLParseError("Not enough items in tag '%s'" % tag) + self._root.remove(self._get_child(children, index)) + + def remove(self, item): + """Remove the given C{item} from the sequence. + + @raises L{WSDLParseError}: If the operation would result in having + less child elements than the required min_occurs, or if no such + index is found. + """ + for index, child in enumerate(self._root.getchildren()): + if child is item._root: + del self[index] + return item + raise WSDLParseError("Non existing item in tag '%s'" % + self._schema.tag) + + def __iter__(self): + """Iter all the sequence items in order.""" + schema = self._schema.child + for child in self._root.iterchildren(): + yield schema.create(child) + + def __len__(self): + """Return the length of the sequence.""" + return len(self._root.getchildren()) + + def _get_child(self, children, index): + """Return the child with the given index.""" + try: + return children[index] + except IndexError: + raise WSDLParseError("Non existing item in tag '%s'" % + self._schema.tag) + + +class WSDLParser(object): + """Build response schemas out of WSDL definitions""" + + leaf_types = ["string", "boolean", "dateTime", "int", "long", "double", + "integer"] + + def parse(self, wsdl): + """Parse the given C{wsdl} data and build the associated schemas. + + @param wdsl: A string containing the raw xml of the WDSL definition + to parse. + @return: A C{dict} mapping response type names to their schemas. + """ + parser = etree.XMLParser(remove_blank_text=True, remove_comments=True) + root = etree.fromstring(wsdl, parser=parser) + types = {} + responses = {} + schemas = {} + namespace = root.attrib["targetNamespace"] + + for element in root[0][0]: + self._remove_namespace_from_tag(element) + if element.tag in ["annotation", "group"]: + continue + name = element.attrib["name"] + if element.tag == "element": + if name.endswith("Response"): + if name in responses: + raise RuntimeError("Schema already defined") + responses[name] = element + elif element.tag == "complexType": + types[name] = [element, False] + else: + raise RuntimeError("Top-level element with unexpected tag") + + for name, element in responses.iteritems(): + schemas[name] = self._parse_type(element, types) + schemas[name].namespace = namespace + + return schemas + + def _remove_namespace_from_tag(self, element): + tag = element.tag + if "}" in tag: + tag = tag.split("}", 1)[1] + element.tag = tag + + def _parse_type(self, element, types): + """Parse a 'complexType' element. + + @param element: The top-level complexType element + @param types: A map of the elements of all available complexType's. + @return: The schema for the complexType. + """ + name = element.attrib["name"] + type = element.attrib["type"] + if not type.startswith("tns:"): + raise RuntimeError("Unexpected element type %s" % type) + type = type[4:] + + [children] = types[type][0] + types[type][1] = True + + self._remove_namespace_from_tag(children) + if children.tag not in ("sequence", "choice"): + raise RuntimeError("Unexpected children type %s" % children.tag) + + if children[0].attrib["name"] == "item": + schema = SequenceSchema(name) + else: + schema = NodeSchema(name) + + for child in children: + self._remove_namespace_from_tag(child) + if child.tag == "element": + name, type, min_occurs, max_occurs = self._parse_child(child) + if type in self.leaf_types: + if max_occurs != 1: + raise RuntimeError("Unexpected max value for leaf") + if not isinstance(schema, NodeSchema): + raise RuntimeError("Attempt to add leaf to a non-node") + schema.add(LeafSchema(name), min_occurs=min_occurs) + else: + if name == "item": # sequence + if not isinstance(schema, SequenceSchema): + raise RuntimeError("Attempt to set child for " + "non-sequence") + schema.set(self._parse_type(child, types), + min_occurs=min_occurs, + max_occurs=max_occurs) + else: + if max_occurs != 1: + raise RuntimeError("Unexpected max for node") + if not isinstance(schema, NodeSchema): + raise RuntimeError("Unexpected schema type") + schema.add(self._parse_type(child, types), + min_occurs=min_occurs) + elif child.tag == "choice": + pass + else: + raise RuntimeError("Unexpected child type") + return schema + + def _parse_child(self, child): + """Parse a single child element. + + @param child: The child C{etree.Element} to parse. + @return: A tuple C{(name, type, min_occurs, max_occurs)} with the + details about the given child. + """ + if set(child.attrib) - set(["name", "type", "minOccurs", "maxOccurs"]): + raise RuntimeError("Unexpected attribute in child") + name = child.attrib["name"] + type = child.attrib["type"].split(":")[1] + min_occurs = child.attrib.get("minOccurs") + max_occurs = child.attrib.get("maxOccurs") + if min_occurs is None: + min_occurs = "1" + min_occurs = int(min_occurs) + if max_occurs is None: + max_occurs = "1" + if max_occurs != "unbounded": + max_occurs = int(max_occurs) + return name, type, min_occurs, max_occurs diff -Nru txaws-0.2/txAWS.egg-info/dependency_links.txt txaws-0.2.3/txAWS.egg-info/dependency_links.txt --- txaws-0.2/txAWS.egg-info/dependency_links.txt 2011-06-14 02:37:48.000000000 +0000 +++ txaws-0.2.3/txAWS.egg-info/dependency_links.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ - diff -Nru txaws-0.2/txAWS.egg-info/PKG-INFO txaws-0.2.3/txAWS.egg-info/PKG-INFO --- txaws-0.2/txAWS.egg-info/PKG-INFO 2011-06-14 02:37:48.000000000 +0000 +++ txaws-0.2.3/txAWS.egg-info/PKG-INFO 1970-01-01 00:00:00.000000000 +0000 @@ -1,23 +0,0 @@ -Metadata-Version: 1.0 -Name: txAWS -Version: 0.2 -Summary: Async library for EC2 and Eucalyptus -Home-page: https://launchpad.net/txaws -Author: txAWS Developers -Author-email: txaws-discuss@lists.launchpad.net -License: MIT -Description: - Twisted-based Asynchronous Libraries for Amazon Web Services and Eucalyptus - private clouds This project's goal is to have a complete Twisted API - representing the spectrum of Amazon's web services as well as support for - Eucalyptus clouds. - -Platform: UNKNOWN -Classifier: Development Status :: 4 - Beta -Classifier: Intended Audience :: Developers -Classifier: Intended Audience :: System Administrators -Classifier: Intended Audience :: Information Technology -Classifier: Programming Language :: Python -Classifier: Topic :: Database -Classifier: Topic :: Internet :: WWW/HTTP -Classifier: License :: OSI Approved :: MIT License diff -Nru txaws-0.2/txAWS.egg-info/requires.txt txaws-0.2.3/txAWS.egg-info/requires.txt --- txaws-0.2/txAWS.egg-info/requires.txt 2011-06-14 02:37:48.000000000 +0000 +++ txaws-0.2.3/txAWS.egg-info/requires.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,2 +0,0 @@ -Epsilon -zope.datetime \ No newline at end of file diff -Nru txaws-0.2/txAWS.egg-info/SOURCES.txt txaws-0.2.3/txAWS.egg-info/SOURCES.txt --- txaws-0.2/txAWS.egg-info/SOURCES.txt 2011-06-14 02:37:48.000000000 +0000 +++ txaws-0.2.3/txAWS.egg-info/SOURCES.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,79 +0,0 @@ -LICENSE -MANIFEST.in -README -setup.py -./bin/aws-status -./bin/txaws-create-bucket -./bin/txaws-delete-bucket -./bin/txaws-delete-object -./bin/txaws-discover -./bin/txaws-get-bucket -./bin/txaws-get-object -./bin/txaws-head-object -./bin/txaws-list-buckets -./bin/txaws-put-object -txAWS.egg-info/PKG-INFO -txAWS.egg-info/SOURCES.txt -txAWS.egg-info/dependency_links.txt -txAWS.egg-info/requires.txt -txAWS.egg-info/top_level.txt -txaws/__init__.py -txaws/credentials.py -txaws/exception.py -txaws/meta.py -txaws/reactor.py -txaws/script.py -txaws/service.py -txaws/util.py -txaws/version.py -txaws/client/__init__.py -txaws/client/base.py -txaws/client/discover/__init__.py -txaws/client/discover/command.py -txaws/client/discover/entry_point.py -txaws/client/discover/tests/__init__.py -txaws/client/discover/tests/test_command.py -txaws/client/discover/tests/test_entry_point.py -txaws/client/gui/__init__.py -txaws/client/gui/gtk.py -txaws/client/gui/tests/__init__.py -txaws/client/gui/tests/test_gtk.py -txaws/client/tests/__init__.py -txaws/client/tests/test_client.py -txaws/ec2/__init__.py -txaws/ec2/client.py -txaws/ec2/exception.py -txaws/ec2/model.py -txaws/ec2/tests/__init__.py -txaws/ec2/tests/test_client.py -txaws/ec2/tests/test_exception.py -txaws/ec2/tests/test_model.py -txaws/s3/__init__.py -txaws/s3/acls.py -txaws/s3/client.py -txaws/s3/exception.py -txaws/s3/model.py -txaws/s3/tests/__init__.py -txaws/s3/tests/test_acls.py -txaws/s3/tests/test_client.py -txaws/s3/tests/test_exception.py -txaws/server/__init__.py -txaws/server/call.py -txaws/server/exception.py -txaws/server/resource.py -txaws/server/schema.py -txaws/server/tests/__init__.py -txaws/server/tests/test_call.py -txaws/server/tests/test_exception.py -txaws/server/tests/test_resource.py -txaws/server/tests/test_schema.py -txaws/testing/__init__.py -txaws/testing/base.py -txaws/testing/ec2.py -txaws/testing/payload.py -txaws/testing/service.py -txaws/tests/__init__.py -txaws/tests/test_credentials.py -txaws/tests/test_exception.py -txaws/tests/test_service.py -txaws/tests/test_util.py \ No newline at end of file diff -Nru txaws-0.2/txAWS.egg-info/top_level.txt txaws-0.2.3/txAWS.egg-info/top_level.txt --- txaws-0.2/txAWS.egg-info/top_level.txt 2011-06-14 02:37:48.000000000 +0000 +++ txaws-0.2.3/txAWS.egg-info/top_level.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -txaws diff -Nru txaws-0.2/wsdl/2009-11-30.ec2.wsdl txaws-0.2.3/wsdl/2009-11-30.ec2.wsdl --- txaws-0.2/wsdl/2009-11-30.ec2.wsdl 1970-01-01 00:00:00.000000000 +0000 +++ txaws-0.2.3/wsdl/2009-11-30.ec2.wsdl 2012-04-11 14:19:09.000000000 +0000 @@ -0,0 +1,4668 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file