Browse code

* S3/CloudFront.py: Initial support for creating new Distros, implemented parser for ListAllDists response. * s3cmd: enabled 'cfcreate' command.

git-svn-id: https://s3tools.svn.sourceforge.net/svnroot/s3tools/s3cmd/branches/s3cmd-airlock@342 830e0280-6d2a-0410-9c65-932aecc39d9d

Michal Ludvig authored on 2009/01/16 22:31:06
Showing 3 changed files
... ...
@@ -1,5 +1,11 @@
1 1
 2009-01-17  Michal Ludvig  <michal@logix.cz>
2 2
 
3
+	* S3/CloudFront.py: Initial support for creating new Distros,
4
+	  implemented parser for ListAllDists response.
5
+	* s3cmd: enabled 'cfcreate' command.
6
+
7
+2009-01-17  Michal Ludvig  <michal@logix.cz>
8
+
3 9
 	* S3/Utils.py: Added getDictFromTree() and appendXmlTextNode()
4 10
 	* S3/S3Uri.py: Added some convenience methods to S3UriS3() 
5 11
 
... ...
@@ -3,6 +3,7 @@
3 3
 ##         http://www.logix.cz/michal
4 4
 ## License: GPL Version 2
5 5
 
6
+import sys
6 7
 import base64
7 8
 import time
8 9
 import httplib
... ...
@@ -15,21 +16,150 @@ except ImportError:
15 15
 	import sha as sha1
16 16
 import hmac
17 17
 
18
-from Config import Config
19
-from Exceptions import *
20
-
21 18
 try:
22 19
 	import xml.etree.ElementTree as ET
23 20
 except ImportError:
24 21
 	import elementtree.ElementTree as ET
25 22
 
23
+from Config import Config
24
+from Exceptions import *
25
+from Utils import getTreeFromXml, appendXmlTextNode, getDictFromTree, dateS3toPython
26
+from S3Uri import S3Uri
27
+
28
+def output(message):
29
+	sys.stdout.write(message + "\n")
30
+
31
+def pretty_output(label, message):
32
+	#label = ("%s " % label).ljust(20, ".")
33
+	label = ("%s:" % label).ljust(15)
34
+	output("%s %s" % (label, message))
35
+
36
+class DistributionSummary(object):
37
+	## Example:
38
+	##
39
+	## <DistributionSummary>
40
+	##	<Id>1234567890ABC</Id>
41
+	##	<Status>Deployed</Status>
42
+	##	<LastModifiedTime>2009-01-16T11:49:02.189Z</LastModifiedTime>
43
+	##	<DomainName>blahblahblah.cloudfront.net</DomainName>
44
+	##	<Origin>example.bucket.s3.amazonaws.com</Origin>
45
+	##	<Enabled>true</Enabled>
46
+	## </DistributionSummary>
47
+	
48
+	def __init__(self, tree):
49
+		if tree.tag != "DistributionSummary":
50
+			raise ValueError("Expected <DistributionSummary /> xml, got: <%s />" % tree.tag)
51
+		self.parse(tree)
52
+
53
+	def parse(self, tree):
54
+		self.info = getDictFromTree(tree)
55
+		self.info['Enabled'] = (self.info['Enabled'].lower() == "true")
56
+
57
+class DistributionList(object):
58
+	## Example:
59
+	## 
60
+	## <DistributionList xmlns="http://cloudfront.amazonaws.com/doc/2008-06-30/">
61
+	##	<Marker />
62
+	##	<MaxItems>100</MaxItems>
63
+	##	<IsTruncated>false</IsTruncated>
64
+	##	<DistributionSummary>
65
+	##	... handled by DistributionSummary() class ...
66
+	##	</DistributionSummary>
67
+	## </DistributionList>
68
+
69
+	def __init__(self, xml):
70
+		tree = getTreeFromXml(xml)
71
+		if tree.tag != "DistributionList":
72
+			raise ValueError("Expected <DistributionList /> xml, got: <%s />" % tree.tag)
73
+		self.parse(tree)
74
+
75
+	def parse(self, tree):
76
+		self.info = getDictFromTree(tree)
77
+		## Normalise some items
78
+		self.info['IsTruncated'] = (self.info['IsTruncated'].lower() == "true")
79
+
80
+		self.dist_summs = []
81
+		for dist_summ in tree.findall(".//DistributionSummary"):
82
+			self.dist_summs.append(DistributionSummary(dist_summ))
83
+
26 84
 class Distribution(object):
27
-	pass
85
+	## Example:
86
+	##
87
+	## <Distribution xmlns="http://cloudfront.amazonaws.com/doc/2008-06-30/">
88
+	##	<Id>1234567890ABC</Id>
89
+	##	<Status>InProgress</Status>
90
+	##	<LastModifiedTime>2009-01-16T13:07:11.319Z</LastModifiedTime>
91
+	##	<DomainName>blahblahblah.cloudfront.net</DomainName>
92
+	##	<DistributionConfig>
93
+	##	... handled by DistributionConfig() class ...
94
+	##	</DistributionConfig>
95
+	## </Distribution>
96
+
97
+	def __init__(self, xml):
98
+		tree = getTreeFromXml(xml)
99
+		if tree.tag != "Distribution":
100
+			raise ValueError("Expected <Distribution /> xml, got: <%s />" % tree.tag)
101
+		self.parse(tree)
102
+
103
+	def parse(self, tree):
104
+		self.info = getDictFromTree(tree)
105
+		## Normalise some items
106
+		self.info['LastModifiedTime'] = dateS3toPython(self.info['LastModifiedTime'])
107
+
108
+		self.info['DistributionConfig'] = DistributionConfig(tree = tree.find(".//DistributionConfig"))
109
+
110
+class DistributionConfig(object):
111
+	## Example:
112
+	##
113
+	## <DistributionConfig>
114
+	##	<Origin>somebucket.s3.amazonaws.com</Origin>
115
+	##	<CallerReference>s3://somebucket/</CallerReference>
116
+	##	<Comment>http://somebucket.s3.amazonaws.com/</Comment>
117
+	##	<Enabled>true</Enabled>
118
+	## </DistributionConfig>
119
+
120
+	EMPTY_CONFIG = "<DistributionConfig></DistributionConfig>"
121
+	xmlns = "http://cloudfront.amazonaws.com/doc/2008-06-30/"
122
+	def __init__(self, xml = None, tree = None):
123
+		if not xml:
124
+			xml = DistributionConfig.EMPTY_CONFIG
125
+
126
+		if not tree:
127
+			tree = getTreeFromXml(xml)
128
+
129
+		if tree.tag != "DistributionConfig":
130
+			raise ValueError("Expected <DistributionConfig /> xml, got: <%s />" % tree.tag)
131
+		self.parse(tree)
132
+
133
+	def parse(self, tree):
134
+		self.Origin = tree.findtext(".//Origin") or ""
135
+		self.CallerReference = tree.findtext(".//CallerReference") or ""
136
+		self.Comment = tree.findtext(".//Comment") or ""
137
+		self.Cnames = []
138
+		for cname in tree.findall(".//CNAME"):
139
+			self.Cnames.append(cname.text.lower())
140
+		enabled = tree.findtext(".//Enabled") or ""
141
+		self.Enabled = (enabled.lower() == "true")
142
+
143
+	def __str__(self):
144
+		tree = getTreeFromXml(DistributionConfig.EMPTY_CONFIG)
145
+		tree.attrib['xmlns'] = DistributionConfig.xmlns
146
+
147
+		## Retain the order of the following calls!
148
+		appendXmlTextNode("Origin", self.Origin, tree)
149
+		appendXmlTextNode("CallerReference", self.CallerReference, tree)
150
+		if self.Comment:
151
+			appendXmlTextNode("Comment", self.Comment, tree)
152
+		for cname in self.Cnames:
153
+			appendXmlTextNode("CNAME", cname.lower(), tree)
154
+		appendXmlTextNode("Enabled", str(self.Enabled).lower(), tree)
155
+
156
+		return ET.tostring(tree)
28 157
 
29 158
 class CloudFront(object):
30 159
 	operations = {
31
-		"Create" : { 'method' : "PUT", 'resource' : "" },
32
-		"Delete" : { 'method' : "DELETE", 'resource' : "/%(dist_id)s" },
160
+		"CreateDist" : { 'method' : "POST", 'resource' : "" },
161
+		"DeleteDist" : { 'method' : "DELETE", 'resource' : "/%(dist_id)s" },
33 162
 		"GetList" : { 'method' : "GET", 'resource' : "" },
34 163
 		"GetDistInfo" : { 'method' : "GET", 'resource' : "/%(dist_id)s" },
35 164
 		"GetDistConfig" : { 'method' : "GET", 'resource' : "/%(dist_id)s/config" },
... ...
@@ -48,6 +178,24 @@ class CloudFront(object):
48 48
 
49 49
 	def GetList(self):
50 50
 		response = self.send_request("GetList")
51
+		response['dist_list'] = DistributionList(response['data'])
52
+		if response['dist_list'].info['IsTruncated']:
53
+			raise NotImplementedError("List is truncated. Ask s3cmd author to add support.")
54
+		## TODO: handle Truncated 
55
+		return response
56
+	
57
+	def CreateDistribution(self, uri, cnames = []):
58
+		dist_conf = DistributionConfig()
59
+		dist_conf.Enabled = True
60
+		dist_conf.Origin = uri.host_name()
61
+		dist_conf.CallerReference = str(uri)
62
+		dist_conf.Comment = uri.public_url()
63
+		if cnames:
64
+			dist_conf.Cnames = cnames
65
+		request_body = str(dist_conf)
66
+		debug("CreateDistribution(): request_body: %s" % request_body)
67
+		response = self.send_request("CreateDist", body = request_body)
68
+		response['distribution'] = Distribution(response['data'])
51 69
 		return response
52 70
 
53 71
 	## --------------------------------------------------
... ...
@@ -56,8 +204,12 @@ class CloudFront(object):
56 56
 
57 57
 	def send_request(self, op_name, dist_id = None, body = None, retries = _max_retries):
58 58
 		operation = self.operations[op_name]
59
-		request = self.create_request(operation, dist_id)
59
+		headers = {}
60
+		if body:
61
+			headers['content-type'] = 'text/plain'
62
+		request = self.create_request(operation, dist_id, headers)
60 63
 		conn = self.get_connection()
64
+		debug("send_request(): %s %s" % (request['method'], request['resource']))
61 65
 		conn.request(request['method'], request['resource'], body, request['headers'])
62 66
 		http_response = conn.getresponse()
63 67
 		response = {}
... ...
@@ -69,7 +221,7 @@ class CloudFront(object):
69 69
 
70 70
 		debug("CloudFront: response: %r" % response)
71 71
 
72
-		if response["status"] >= 400:
72
+		if response["status"] >= 500:
73 73
 			e = CloudFrontError(response)
74 74
 			if retries:
75 75
 				warning(u"Retrying failed request: %s" % op_name)
... ...
@@ -129,8 +281,43 @@ class Cmd(object):
129 129
 	"""
130 130
 	Class that implements CloudFront commands
131 131
 	"""
132
-
132
+	
133 133
 	@staticmethod
134 134
 	def list(args):
135 135
 		cf = CloudFront(Config())
136 136
 		response = cf.GetList()
137
+		for d in response['dist_list'].dist_summs:
138
+			pretty_output("Origin", d.info['Origin'])
139
+			pretty_output("DomainName", d.info['DomainName'])
140
+			pretty_output("Id", d.info['Id'])
141
+			pretty_output("Status", d.info['Status'])
142
+			pretty_output("Enabled", d.info['Enabled'])
143
+			output("")
144
+	
145
+	@staticmethod
146
+	def create(args):
147
+		cf = CloudFront(Config())
148
+		buckets = []
149
+		for arg in args:
150
+			uri = S3Uri(arg)
151
+			if uri.type != "s3":
152
+				raise ParameterError("Bucket can only be created from a s3:// URI instead of: %s" % arg)
153
+			if uri.object():
154
+				raise ParameterError("Use s3:// URI with a bucket name only instead of: %s" % arg)
155
+			if not uri.is_dns_compatible():
156
+				raise ParameterError("CloudFront can only handle lowercase-named buckets.")
157
+			buckets.append(uri)
158
+		if not buckets:
159
+			raise ParameterError("No valid bucket names found")
160
+		for uri in buckets:
161
+			info("Creating distribution from: %s" % uri)
162
+			response = cf.CreateDistribution(uri)
163
+			d = response['distribution']
164
+			dc = d.info['DistributionConfig']
165
+			output("Distribution created:")
166
+			#pretty_output("Origin", dc.info['Origin'])
167
+			pretty_output("Origin", dc.Origin)
168
+			pretty_output("DomainName", d.info['DomainName'])
169
+			pretty_output("Id", d.info['Id'])
170
+			pretty_output("Status", d.info['Status'])
171
+			pretty_output("Enabled", dc.Enabled)
... ...
@@ -1119,7 +1119,7 @@ def get_commands_list():
1119 1119
 	{"cmd":"setacl", "label":"Modify Access control list for Bucket or Object", "param":"s3://BUCKET[/OBJECT]", "func":cmd_setacl, "argc":1},
1120 1120
 	## CloudFront commands
1121 1121
 	{"cmd":"cflist", "label":"List CloudFront distribution points", "param":"", "func":CfCmd.list, "argc":0},
1122
-	#{"cmd":"cfcreate", "label":"Create CloudFront distribution point", "param":"s3://BUCKET", "func":cmd_cf_create, "argc":1},
1122
+	{"cmd":"cfcreate", "label":"Create CloudFront distribution point", "param":"s3://BUCKET", "func":CfCmd.create, "argc":1},
1123 1123
 	#{"cmd":"cfdelete", "label":"Delete CloudFront distribution point", "param":"cf://DIST_ID", "func":cmd_cf_delete, "argc":1},
1124 1124
 	#{"cmd":"cfinfo", "label":"Display CloudFront distribution point parameters", "param":"cf://DIST_ID", "func":cmd_cf_info, "argc":1},
1125 1125
 	#{"cmd":"cfmodify", "label":"Change CloudFront distribution point parameters", "param":"cf://DIST_ID", "func":cmd_cf_modify, "argc":1},