git-svn-id: https://s3tools.svn.sourceforge.net/svnroot/s3tools/s3cmd/branches/s3cmd-airlock@339 830e0280-6d2a-0410-9c65-932aecc39d9d
Michal Ludvig authored on 2009/01/15 22:02:214 | 9 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,136 @@ |
0 |
+## Amazon CloudFront support |
|
1 |
+## Author: Michal Ludvig <michal@logix.cz> |
|
2 |
+## http://www.logix.cz/michal |
|
3 |
+## License: GPL Version 2 |
|
4 |
+ |
|
5 |
+import base64 |
|
6 |
+import time |
|
7 |
+import httplib |
|
8 |
+from logging import debug, info, warning, error |
|
9 |
+ |
|
10 |
+try: |
|
11 |
+ from hashlib import md5, sha1 |
|
12 |
+except ImportError: |
|
13 |
+ from md5 import md5 |
|
14 |
+ import sha as sha1 |
|
15 |
+import hmac |
|
16 |
+ |
|
17 |
+from Config import Config |
|
18 |
+from Exceptions import * |
|
19 |
+ |
|
20 |
+try: |
|
21 |
+ import xml.etree.ElementTree as ET |
|
22 |
+except ImportError: |
|
23 |
+ import elementtree.ElementTree as ET |
|
24 |
+ |
|
25 |
+class Distribution(object): |
|
26 |
+ pass |
|
27 |
+ |
|
28 |
+class CloudFront(object): |
|
29 |
+ operations = { |
|
30 |
+ "Create" : { 'method' : "PUT", 'resource' : "" }, |
|
31 |
+ "Delete" : { 'method' : "DELETE", 'resource' : "/%(dist_id)s" }, |
|
32 |
+ "GetList" : { 'method' : "GET", 'resource' : "" }, |
|
33 |
+ "GetDistInfo" : { 'method' : "GET", 'resource' : "/%(dist_id)s" }, |
|
34 |
+ "GetDistConfig" : { 'method' : "GET", 'resource' : "/%(dist_id)s/config" }, |
|
35 |
+ "SetDistConfig" : { 'method' : "PUT", 'resource' : "/%(dist_id)s/config" }, |
|
36 |
+ } |
|
37 |
+ |
|
38 |
+ ## Maximum attempts of re-issuing failed requests |
|
39 |
+ _max_retries = 5 |
|
40 |
+ |
|
41 |
+ def __init__(self, config): |
|
42 |
+ self.config = config |
|
43 |
+ |
|
44 |
+ ## -------------------------------------------------- |
|
45 |
+ ## Methods implementing CloudFront API |
|
46 |
+ ## -------------------------------------------------- |
|
47 |
+ |
|
48 |
+ def GetList(self): |
|
49 |
+ response = self.send_request("GetList") |
|
50 |
+ return response |
|
51 |
+ |
|
52 |
+ ## -------------------------------------------------- |
|
53 |
+ ## Low-level methods for handling CloudFront requests |
|
54 |
+ ## -------------------------------------------------- |
|
55 |
+ |
|
56 |
+ def send_request(self, op_name, dist_id = None, body = None, retries = _max_retries): |
|
57 |
+ operation = self.operations[op_name] |
|
58 |
+ request = self.create_request(operation, dist_id) |
|
59 |
+ conn = self.get_connection() |
|
60 |
+ conn.request(request['method'], request['resource'], body, request['headers']) |
|
61 |
+ http_response = conn.getresponse() |
|
62 |
+ response = {} |
|
63 |
+ response["status"] = http_response.status |
|
64 |
+ response["reason"] = http_response.reason |
|
65 |
+ response["headers"] = dict(http_response.getheaders()) |
|
66 |
+ response["data"] = http_response.read() |
|
67 |
+ conn.close() |
|
68 |
+ |
|
69 |
+ debug("CloudFront: response: %r" % response) |
|
70 |
+ |
|
71 |
+ if response["status"] >= 400: |
|
72 |
+ e = CloudFrontError(response) |
|
73 |
+ if retries: |
|
74 |
+ warning(u"Retrying failed request: %s" % op_name) |
|
75 |
+ warning(unicode(e)) |
|
76 |
+ warning("Waiting %d sec..." % self._fail_wait(retries)) |
|
77 |
+ time.sleep(self._fail_wait(retries)) |
|
78 |
+ return self.send_request(op_name, dist_id, body, retries - 1) |
|
79 |
+ else: |
|
80 |
+ raise e |
|
81 |
+ |
|
82 |
+ if response["status"] < 200 or response["status"] > 299: |
|
83 |
+ raise CloudFrontError(response) |
|
84 |
+ |
|
85 |
+ return response |
|
86 |
+ |
|
87 |
+ def create_request(self, operation, dist_id = None, headers = None): |
|
88 |
+ resource = self.config.cloudfront_resource + ( |
|
89 |
+ operation['resource'] % { 'dist_id' : dist_id }) |
|
90 |
+ |
|
91 |
+ if not headers: |
|
92 |
+ headers = {} |
|
93 |
+ |
|
94 |
+ if headers.has_key("date"): |
|
95 |
+ if not headers.has_key("x-amz-date"): |
|
96 |
+ headers["x-amz-date"] = headers["date"] |
|
97 |
+ del(headers["date"]) |
|
98 |
+ |
|
99 |
+ if not headers.has_key("x-amz-date"): |
|
100 |
+ headers["x-amz-date"] = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime()) |
|
101 |
+ |
|
102 |
+ signature = self.sign_request(headers) |
|
103 |
+ headers["Authorization"] = "AWS "+self.config.access_key+":"+signature |
|
104 |
+ |
|
105 |
+ request = {} |
|
106 |
+ request['resource'] = resource |
|
107 |
+ request['headers'] = headers |
|
108 |
+ request['method'] = operation['method'] |
|
109 |
+ |
|
110 |
+ return request |
|
111 |
+ |
|
112 |
+ def sign_request(self, headers): |
|
113 |
+ string_to_sign = headers['x-amz-date'] |
|
114 |
+ signature = base64.encodestring(hmac.new(self.config.secret_key, string_to_sign, sha1).digest()).strip() |
|
115 |
+ debug(u"CloudFront.sign_request('%s') = %s" % (string_to_sign, signature)) |
|
116 |
+ return signature |
|
117 |
+ |
|
118 |
+ def get_connection(self): |
|
119 |
+ if self.config.proxy_host != "": |
|
120 |
+ raise ParameterError("CloudFront commands don't work from behind a HTTP proxy") |
|
121 |
+ return httplib.HTTPSConnection(self.config.cloudfront_host) |
|
122 |
+ |
|
123 |
+ def _fail_wait(self, retries): |
|
124 |
+ # Wait a few seconds. The more it fails the more we wait. |
|
125 |
+ return (self._max_retries - retries + 1) * 3 |
|
126 |
+ |
|
127 |
+class Cmd(object): |
|
128 |
+ """ |
|
129 |
+ Class that implements CloudFront commands |
|
130 |
+ """ |
|
131 |
+ |
|
132 |
+ @staticmethod |
|
133 |
+ def list(args): |
|
134 |
+ cf = CloudFront(Config()) |
|
135 |
+ response = cf.GetList() |
... | ... |
@@ -17,6 +17,8 @@ class Config(object): |
17 | 17 |
host_base = "s3.amazonaws.com" |
18 | 18 |
host_bucket = "%(bucket)s.s3.amazonaws.com" |
19 | 19 |
simpledb_host = "sdb.amazonaws.com" |
20 |
+ cloudfront_host = "cloudfront.amazonaws.com" |
|
21 |
+ cloudfront_resource = "/2008-06-30/distribution" |
|
20 | 22 |
verbosity = logging.WARNING |
21 | 23 |
progress_meter = True |
22 | 24 |
progress_class = Progress.ProgressCR |
... | ... |
@@ -3,7 +3,7 @@ |
3 | 3 |
## http://www.logix.cz/michal |
4 | 4 |
## License: GPL Version 2 |
5 | 5 |
|
6 |
-from Utils import getRootTagName, unicodise, deunicodise |
|
6 |
+from Utils import getTreeFromXml, unicodise, deunicodise |
|
7 | 7 |
from logging import debug, info, warning, error |
8 | 8 |
|
9 | 9 |
try: |
... | ... |
@@ -30,21 +30,26 @@ class S3Error (S3Exception): |
30 | 30 |
if response.has_key("headers"): |
31 | 31 |
for header in response["headers"]: |
32 | 32 |
debug("HttpHeader: %s: %s" % (header, response["headers"][header])) |
33 |
- if response.has_key("data") and getRootTagName(response["data"]) == "Error": |
|
34 |
- tree = ET.fromstring(response["data"]) |
|
35 |
- for child in tree.getchildren(): |
|
33 |
+ if response.has_key("data"): |
|
34 |
+ tree = getTreeFromXml(response["data"]) |
|
35 |
+ error_node = tree |
|
36 |
+ if not error_node.tag == "Error": |
|
37 |
+ error_node = tree.find(".//Error") |
|
38 |
+ for child in error_node.getchildren(): |
|
36 | 39 |
if child.text != "": |
37 | 40 |
debug("ErrorXML: " + child.tag + ": " + repr(child.text)) |
38 | 41 |
self.info[child.tag] = child.text |
39 | 42 |
|
40 | 43 |
def __unicode__(self): |
41 |
- retval = "%d (%s)" % (self.status, self.reason) |
|
42 |
- try: |
|
43 |
- retval += (": %s" % self.info["Code"]) |
|
44 |
- except (AttributeError, KeyError): |
|
45 |
- pass |
|
44 |
+ retval = u"%d " % (self.status) |
|
45 |
+ retval += (u"(%s)" % (self.info.has_key("Code") and self.info["Code"] or self.reason)) |
|
46 |
+ if self.info.has_key("Message"): |
|
47 |
+ retval += (u": %s" % self.info["Message"]) |
|
46 | 48 |
return retval |
47 | 49 |
|
50 |
+class CloudFrontError(S3Error): |
|
51 |
+ pass |
|
52 |
+ |
|
48 | 53 |
class S3UploadError(S3Exception): |
49 | 54 |
pass |
50 | 55 |
|
... | ... |
@@ -21,6 +21,9 @@ from optparse import OptionParser, Option, OptionValueError, IndentedHelpFormatt |
21 | 21 |
from logging import debug, info, warning, error |
22 | 22 |
from distutils.spawn import find_executable |
23 | 23 |
|
24 |
+commands = {} |
|
25 |
+commands_list = [] |
|
26 |
+ |
|
24 | 27 |
def output(message): |
25 | 28 |
sys.stdout.write(message + "\n") |
26 | 29 |
|
... | ... |
@@ -1098,8 +1101,8 @@ def process_exclude_from_file(exf, exclude_array): |
1098 | 1098 |
debug(u"adding rule: %s" % ex) |
1099 | 1099 |
exclude_array.append(ex) |
1100 | 1100 |
|
1101 |
-commands = {} |
|
1102 |
-commands_list = [ |
|
1101 |
+def get_commands_list(): |
|
1102 |
+ return [ |
|
1103 | 1103 |
{"cmd":"mb", "label":"Make bucket", "param":"s3://BUCKET", "func":cmd_bucket_create, "argc":1}, |
1104 | 1104 |
{"cmd":"rb", "label":"Remove bucket", "param":"s3://BUCKET", "func":cmd_bucket_delete, "argc":1}, |
1105 | 1105 |
{"cmd":"ls", "label":"List objects or buckets", "param":"[s3://BUCKET[/PREFIX]]", "func":cmd_ls, "argc":0}, |
... | ... |
@@ -1114,6 +1117,12 @@ commands_list = [ |
1114 | 1114 |
{"cmd":"cp", "label":"Copy object", "param":"s3://BUCKET1/OBJECT1 s3://BUCKET2[/OBJECT2]", "func":cmd_cp, "argc":2}, |
1115 | 1115 |
{"cmd":"mv", "label":"Move object", "param":"s3://BUCKET1/OBJECT1 s3://BUCKET2[/OBJECT2]", "func":cmd_mv, "argc":2}, |
1116 | 1116 |
{"cmd":"setacl", "label":"Modify Access control list for Bucket or Object", "param":"s3://BUCKET[/OBJECT]", "func":cmd_setacl, "argc":1}, |
1117 |
+ ## CloudFront commands |
|
1118 |
+ {"cmd":"cflist", "label":"List CloudFront distribution points", "param":"", "func":CfCmd.list, "argc":0}, |
|
1119 |
+ #{"cmd":"cfcreate", "label":"Create CloudFront distribution point", "param":"s3://BUCKET", "func":cmd_cf_create, "argc":1}, |
|
1120 |
+ #{"cmd":"cfdelete", "label":"Delete CloudFront distribution point", "param":"cf://DIST_ID", "func":cmd_cf_delete, "argc":1}, |
|
1121 |
+ #{"cmd":"cfinfo", "label":"Display CloudFront distribution point parameters", "param":"cf://DIST_ID", "func":cmd_cf_info, "argc":1}, |
|
1122 |
+ #{"cmd":"cfmodify", "label":"Change CloudFront distribution point parameters", "param":"cf://DIST_ID", "func":cmd_cf_modify, "argc":1}, |
|
1117 | 1123 |
] |
1118 | 1124 |
|
1119 | 1125 |
def format_commands(progname): |
... | ... |
@@ -1145,6 +1154,9 @@ def main(): |
1145 | 1145 |
sys.stderr.write("ERROR: Python 2.4 or higher required, sorry.\n") |
1146 | 1146 |
sys.exit(1) |
1147 | 1147 |
|
1148 |
+ global commands_list, commands |
|
1149 |
+ commands_list = get_commands_list() |
|
1150 |
+ commands = {} |
|
1148 | 1151 |
## Populate "commands" from "commands_list" |
1149 | 1152 |
for cmd in commands_list: |
1150 | 1153 |
if cmd.has_key("cmd"): |
... | ... |
@@ -1347,8 +1359,6 @@ def main(): |
1347 | 1347 |
cmd_func(args) |
1348 | 1348 |
except S3Error, e: |
1349 | 1349 |
error(u"S3 error: %s" % e) |
1350 |
- if e.info.has_key("Message"): |
|
1351 |
- error(e.info['Message']) |
|
1352 | 1350 |
sys.exit(1) |
1353 | 1351 |
except ParameterError, e: |
1354 | 1352 |
error(u"Parameter problem: %s" % e) |
... | ... |
@@ -1367,6 +1377,7 @@ if __name__ == '__main__': |
1367 | 1367 |
from S3.Exceptions import * |
1368 | 1368 |
from S3.Utils import unicodise |
1369 | 1369 |
from S3.Progress import Progress |
1370 |
+ from S3.CloudFront import Cmd as CfCmd |
|
1370 | 1371 |
|
1371 | 1372 |
main() |
1372 | 1373 |
sys.exit(0) |