Browse code

* S3/CloudFront.py: New module for CloudFront support. * s3cmd, S3/Config.py, S3/Exceptions.py: Wire in CF support.

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:21
Showing 5 changed files
... ...
@@ -1,3 +1,8 @@
1
+2009-01-16  Michal Ludvig  <michal@logix.cz>
2
+
3
+	* S3/CloudFront.py: New module for CloudFront support.
4
+	* s3cmd, S3/Config.py, S3/Exceptions.py: Wire in CF support.
5
+
1 6
 2009-01-13  Michal Ludvig  <michal@logix.cz>
2 7
 
3 8
 	* TODO: Updated.
4 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)