git-svn-id: https://s3tools.svn.sourceforge.net/svnroot/s3tools/s3cmd/trunk@358 830e0280-6d2a-0410-9c65-932aecc39d9d
Michal Ludvig authored on 2009/01/27 10:53:184 | 9 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,487 @@ |
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 sys |
|
6 |
+import base64 |
|
7 |
+import time |
|
8 |
+import httplib |
|
9 |
+from logging import debug, info, warning, error |
|
10 |
+ |
|
11 |
+try: |
|
12 |
+ from hashlib import md5, sha1 |
|
13 |
+except ImportError: |
|
14 |
+ from md5 import md5 |
|
15 |
+ import sha as sha1 |
|
16 |
+import hmac |
|
17 |
+ |
|
18 |
+try: |
|
19 |
+ import xml.etree.ElementTree as ET |
|
20 |
+except ImportError: |
|
21 |
+ import elementtree.ElementTree as ET |
|
22 |
+ |
|
23 |
+from Config import Config |
|
24 |
+from Exceptions import * |
|
25 |
+from Utils import getTreeFromXml, appendXmlTextNode, getDictFromTree, dateS3toPython |
|
26 |
+from S3Uri import S3Uri, S3UriS3 |
|
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 |
+ def uri(self): |
|
58 |
+ return S3Uri("cf://%s" % self.info['Id']) |
|
59 |
+ |
|
60 |
+class DistributionList(object): |
|
61 |
+ ## Example: |
|
62 |
+ ## |
|
63 |
+ ## <DistributionList xmlns="http://cloudfront.amazonaws.com/doc/2008-06-30/"> |
|
64 |
+ ## <Marker /> |
|
65 |
+ ## <MaxItems>100</MaxItems> |
|
66 |
+ ## <IsTruncated>false</IsTruncated> |
|
67 |
+ ## <DistributionSummary> |
|
68 |
+ ## ... handled by DistributionSummary() class ... |
|
69 |
+ ## </DistributionSummary> |
|
70 |
+ ## </DistributionList> |
|
71 |
+ |
|
72 |
+ def __init__(self, xml): |
|
73 |
+ tree = getTreeFromXml(xml) |
|
74 |
+ if tree.tag != "DistributionList": |
|
75 |
+ raise ValueError("Expected <DistributionList /> xml, got: <%s />" % tree.tag) |
|
76 |
+ self.parse(tree) |
|
77 |
+ |
|
78 |
+ def parse(self, tree): |
|
79 |
+ self.info = getDictFromTree(tree) |
|
80 |
+ ## Normalise some items |
|
81 |
+ self.info['IsTruncated'] = (self.info['IsTruncated'].lower() == "true") |
|
82 |
+ |
|
83 |
+ self.dist_summs = [] |
|
84 |
+ for dist_summ in tree.findall(".//DistributionSummary"): |
|
85 |
+ self.dist_summs.append(DistributionSummary(dist_summ)) |
|
86 |
+ |
|
87 |
+class Distribution(object): |
|
88 |
+ ## Example: |
|
89 |
+ ## |
|
90 |
+ ## <Distribution xmlns="http://cloudfront.amazonaws.com/doc/2008-06-30/"> |
|
91 |
+ ## <Id>1234567890ABC</Id> |
|
92 |
+ ## <Status>InProgress</Status> |
|
93 |
+ ## <LastModifiedTime>2009-01-16T13:07:11.319Z</LastModifiedTime> |
|
94 |
+ ## <DomainName>blahblahblah.cloudfront.net</DomainName> |
|
95 |
+ ## <DistributionConfig> |
|
96 |
+ ## ... handled by DistributionConfig() class ... |
|
97 |
+ ## </DistributionConfig> |
|
98 |
+ ## </Distribution> |
|
99 |
+ |
|
100 |
+ def __init__(self, xml): |
|
101 |
+ tree = getTreeFromXml(xml) |
|
102 |
+ if tree.tag != "Distribution": |
|
103 |
+ raise ValueError("Expected <Distribution /> xml, got: <%s />" % tree.tag) |
|
104 |
+ self.parse(tree) |
|
105 |
+ |
|
106 |
+ def parse(self, tree): |
|
107 |
+ self.info = getDictFromTree(tree) |
|
108 |
+ ## Normalise some items |
|
109 |
+ self.info['LastModifiedTime'] = dateS3toPython(self.info['LastModifiedTime']) |
|
110 |
+ |
|
111 |
+ self.info['DistributionConfig'] = DistributionConfig(tree = tree.find(".//DistributionConfig")) |
|
112 |
+ |
|
113 |
+ def uri(self): |
|
114 |
+ return S3Uri("cf://%s" % self.info['Id']) |
|
115 |
+ |
|
116 |
+class DistributionConfig(object): |
|
117 |
+ ## Example: |
|
118 |
+ ## |
|
119 |
+ ## <DistributionConfig> |
|
120 |
+ ## <Origin>somebucket.s3.amazonaws.com</Origin> |
|
121 |
+ ## <CallerReference>s3://somebucket/</CallerReference> |
|
122 |
+ ## <Comment>http://somebucket.s3.amazonaws.com/</Comment> |
|
123 |
+ ## <Enabled>true</Enabled> |
|
124 |
+ ## </DistributionConfig> |
|
125 |
+ |
|
126 |
+ EMPTY_CONFIG = "<DistributionConfig><Origin/><CallerReference/><Enabled>true</Enabled></DistributionConfig>" |
|
127 |
+ xmlns = "http://cloudfront.amazonaws.com/doc/2008-06-30/" |
|
128 |
+ def __init__(self, xml = None, tree = None): |
|
129 |
+ if not xml: |
|
130 |
+ xml = DistributionConfig.EMPTY_CONFIG |
|
131 |
+ |
|
132 |
+ if not tree: |
|
133 |
+ tree = getTreeFromXml(xml) |
|
134 |
+ |
|
135 |
+ if tree.tag != "DistributionConfig": |
|
136 |
+ raise ValueError("Expected <DistributionConfig /> xml, got: <%s />" % tree.tag) |
|
137 |
+ self.parse(tree) |
|
138 |
+ |
|
139 |
+ def parse(self, tree): |
|
140 |
+ self.info = getDictFromTree(tree) |
|
141 |
+ self.info['Enabled'] = (self.info['Enabled'].lower() == "true") |
|
142 |
+ if not self.info.has_key("CNAME"): |
|
143 |
+ self.info['CNAME'] = [] |
|
144 |
+ if type(self.info['CNAME']) != list: |
|
145 |
+ self.info['CNAME'] = [self.info['CNAME']] |
|
146 |
+ self.info['CNAME'] = [cname.lower() for cname in self.info['CNAME']] |
|
147 |
+ if not self.info.has_key("Comment"): |
|
148 |
+ self.info['Comment'] = "" |
|
149 |
+ |
|
150 |
+ def __str__(self): |
|
151 |
+ tree = ET.Element("DistributionConfig") |
|
152 |
+ tree.attrib['xmlns'] = DistributionConfig.xmlns |
|
153 |
+ |
|
154 |
+ ## Retain the order of the following calls! |
|
155 |
+ appendXmlTextNode("Origin", self.info['Origin'], tree) |
|
156 |
+ appendXmlTextNode("CallerReference", self.info['CallerReference'], tree) |
|
157 |
+ for cname in self.info['CNAME']: |
|
158 |
+ appendXmlTextNode("CNAME", cname.lower(), tree) |
|
159 |
+ if self.info['Comment']: |
|
160 |
+ appendXmlTextNode("Comment", self.info['Comment'], tree) |
|
161 |
+ appendXmlTextNode("Enabled", str(self.info['Enabled']).lower(), tree) |
|
162 |
+ |
|
163 |
+ return ET.tostring(tree) |
|
164 |
+ |
|
165 |
+class CloudFront(object): |
|
166 |
+ operations = { |
|
167 |
+ "CreateDist" : { 'method' : "POST", 'resource' : "" }, |
|
168 |
+ "DeleteDist" : { 'method' : "DELETE", 'resource' : "/%(dist_id)s" }, |
|
169 |
+ "GetList" : { 'method' : "GET", 'resource' : "" }, |
|
170 |
+ "GetDistInfo" : { 'method' : "GET", 'resource' : "/%(dist_id)s" }, |
|
171 |
+ "GetDistConfig" : { 'method' : "GET", 'resource' : "/%(dist_id)s/config" }, |
|
172 |
+ "SetDistConfig" : { 'method' : "PUT", 'resource' : "/%(dist_id)s/config" }, |
|
173 |
+ } |
|
174 |
+ |
|
175 |
+ ## Maximum attempts of re-issuing failed requests |
|
176 |
+ _max_retries = 5 |
|
177 |
+ |
|
178 |
+ def __init__(self, config): |
|
179 |
+ self.config = config |
|
180 |
+ |
|
181 |
+ ## -------------------------------------------------- |
|
182 |
+ ## Methods implementing CloudFront API |
|
183 |
+ ## -------------------------------------------------- |
|
184 |
+ |
|
185 |
+ def GetList(self): |
|
186 |
+ response = self.send_request("GetList") |
|
187 |
+ response['dist_list'] = DistributionList(response['data']) |
|
188 |
+ if response['dist_list'].info['IsTruncated']: |
|
189 |
+ raise NotImplementedError("List is truncated. Ask s3cmd author to add support.") |
|
190 |
+ ## TODO: handle Truncated |
|
191 |
+ return response |
|
192 |
+ |
|
193 |
+ def CreateDistribution(self, uri, cnames_add = [], comment = None): |
|
194 |
+ dist_config = DistributionConfig() |
|
195 |
+ dist_config.info['Enabled'] = True |
|
196 |
+ dist_config.info['Origin'] = uri.host_name() |
|
197 |
+ dist_config.info['CallerReference'] = str(uri) |
|
198 |
+ if comment == None: |
|
199 |
+ dist_config.info['Comment'] = uri.public_url() |
|
200 |
+ else: |
|
201 |
+ dist_config.info['Comment'] = comment |
|
202 |
+ for cname in cnames_add: |
|
203 |
+ if dist_config.info['CNAME'].count(cname) == 0: |
|
204 |
+ dist_config.info['CNAME'].append(cname) |
|
205 |
+ request_body = str(dist_config) |
|
206 |
+ debug("CreateDistribution(): request_body: %s" % request_body) |
|
207 |
+ response = self.send_request("CreateDist", body = request_body) |
|
208 |
+ response['distribution'] = Distribution(response['data']) |
|
209 |
+ return response |
|
210 |
+ |
|
211 |
+ def ModifyDistribution(self, cfuri, cnames_add = [], cnames_remove = [], |
|
212 |
+ comment = None, enabled = None): |
|
213 |
+ if cfuri.type != "cf": |
|
214 |
+ raise ValueError("Expected CFUri instead of: %s" % cfuri) |
|
215 |
+ # Get current dist status (enabled/disabled) and Etag |
|
216 |
+ info("Checking current status of %s" % cfuri) |
|
217 |
+ response = self.GetDistConfig(cfuri) |
|
218 |
+ dc = response['dist_config'] |
|
219 |
+ if enabled != None: |
|
220 |
+ dc.info['Enabled'] = enabled |
|
221 |
+ if comment != None: |
|
222 |
+ dc.info['Comment'] = comment |
|
223 |
+ for cname in cnames_add: |
|
224 |
+ if dc.info['CNAME'].count(cname) == 0: |
|
225 |
+ dc.info['CNAME'].append(cname) |
|
226 |
+ for cname in cnames_remove: |
|
227 |
+ while dc.info['CNAME'].count(cname) > 0: |
|
228 |
+ dc.info['CNAME'].remove(cname) |
|
229 |
+ response = self.SetDistConfig(cfuri, dc, response['headers']['etag']) |
|
230 |
+ return response |
|
231 |
+ |
|
232 |
+ def DeleteDistribution(self, cfuri): |
|
233 |
+ if cfuri.type != "cf": |
|
234 |
+ raise ValueError("Expected CFUri instead of: %s" % cfuri) |
|
235 |
+ # Get current dist status (enabled/disabled) and Etag |
|
236 |
+ info("Checking current status of %s" % cfuri) |
|
237 |
+ response = self.GetDistConfig(cfuri) |
|
238 |
+ if response['dist_config'].info['Enabled']: |
|
239 |
+ info("Distribution is ENABLED. Disabling first.") |
|
240 |
+ response['dist_config'].info['Enabled'] = False |
|
241 |
+ response = self.SetDistConfig(cfuri, response['dist_config'], |
|
242 |
+ response['headers']['etag']) |
|
243 |
+ warning("Waiting for Distribution to become disabled.") |
|
244 |
+ warning("This may take several minutes, please wait.") |
|
245 |
+ while True: |
|
246 |
+ response = self.GetDistInfo(cfuri) |
|
247 |
+ d = response['distribution'] |
|
248 |
+ if d.info['Status'] == "Deployed" and d.info['Enabled'] == False: |
|
249 |
+ info("Distribution is now disabled") |
|
250 |
+ break |
|
251 |
+ warning("Still waiting...") |
|
252 |
+ time.sleep(10) |
|
253 |
+ headers = {} |
|
254 |
+ headers['if-match'] = response['headers']['etag'] |
|
255 |
+ response = self.send_request("DeleteDist", dist_id = cfuri.dist_id(), |
|
256 |
+ headers = headers) |
|
257 |
+ return response |
|
258 |
+ |
|
259 |
+ def GetDistInfo(self, cfuri): |
|
260 |
+ if cfuri.type != "cf": |
|
261 |
+ raise ValueError("Expected CFUri instead of: %s" % cfuri) |
|
262 |
+ response = self.send_request("GetDistInfo", dist_id = cfuri.dist_id()) |
|
263 |
+ response['distribution'] = Distribution(response['data']) |
|
264 |
+ return response |
|
265 |
+ |
|
266 |
+ def GetDistConfig(self, cfuri): |
|
267 |
+ if cfuri.type != "cf": |
|
268 |
+ raise ValueError("Expected CFUri instead of: %s" % cfuri) |
|
269 |
+ response = self.send_request("GetDistConfig", dist_id = cfuri.dist_id()) |
|
270 |
+ response['dist_config'] = DistributionConfig(response['data']) |
|
271 |
+ return response |
|
272 |
+ |
|
273 |
+ def SetDistConfig(self, cfuri, dist_config, etag = None): |
|
274 |
+ if etag == None: |
|
275 |
+ debug("SetDistConfig(): Etag not set. Fetching it first.") |
|
276 |
+ etag = self.GetDistConfig(cfuri)['headers']['etag'] |
|
277 |
+ debug("SetDistConfig(): Etag = %s" % etag) |
|
278 |
+ request_body = str(dist_config) |
|
279 |
+ debug("SetDistConfig(): request_body: %s" % request_body) |
|
280 |
+ headers = {} |
|
281 |
+ headers['if-match'] = etag |
|
282 |
+ response = self.send_request("SetDistConfig", dist_id = cfuri.dist_id(), |
|
283 |
+ body = request_body, headers = headers) |
|
284 |
+ return response |
|
285 |
+ |
|
286 |
+ ## -------------------------------------------------- |
|
287 |
+ ## Low-level methods for handling CloudFront requests |
|
288 |
+ ## -------------------------------------------------- |
|
289 |
+ |
|
290 |
+ def send_request(self, op_name, dist_id = None, body = None, headers = {}, retries = _max_retries): |
|
291 |
+ operation = self.operations[op_name] |
|
292 |
+ if body: |
|
293 |
+ headers['content-type'] = 'text/plain' |
|
294 |
+ request = self.create_request(operation, dist_id, headers) |
|
295 |
+ conn = self.get_connection() |
|
296 |
+ debug("send_request(): %s %s" % (request['method'], request['resource'])) |
|
297 |
+ conn.request(request['method'], request['resource'], body, request['headers']) |
|
298 |
+ http_response = conn.getresponse() |
|
299 |
+ response = {} |
|
300 |
+ response["status"] = http_response.status |
|
301 |
+ response["reason"] = http_response.reason |
|
302 |
+ response["headers"] = dict(http_response.getheaders()) |
|
303 |
+ response["data"] = http_response.read() |
|
304 |
+ conn.close() |
|
305 |
+ |
|
306 |
+ debug("CloudFront: response: %r" % response) |
|
307 |
+ |
|
308 |
+ if response["status"] >= 500: |
|
309 |
+ e = CloudFrontError(response) |
|
310 |
+ if retries: |
|
311 |
+ warning(u"Retrying failed request: %s" % op_name) |
|
312 |
+ warning(unicode(e)) |
|
313 |
+ warning("Waiting %d sec..." % self._fail_wait(retries)) |
|
314 |
+ time.sleep(self._fail_wait(retries)) |
|
315 |
+ return self.send_request(op_name, dist_id, body, retries - 1) |
|
316 |
+ else: |
|
317 |
+ raise e |
|
318 |
+ |
|
319 |
+ if response["status"] < 200 or response["status"] > 299: |
|
320 |
+ raise CloudFrontError(response) |
|
321 |
+ |
|
322 |
+ return response |
|
323 |
+ |
|
324 |
+ def create_request(self, operation, dist_id = None, headers = None): |
|
325 |
+ resource = self.config.cloudfront_resource + ( |
|
326 |
+ operation['resource'] % { 'dist_id' : dist_id }) |
|
327 |
+ |
|
328 |
+ if not headers: |
|
329 |
+ headers = {} |
|
330 |
+ |
|
331 |
+ if headers.has_key("date"): |
|
332 |
+ if not headers.has_key("x-amz-date"): |
|
333 |
+ headers["x-amz-date"] = headers["date"] |
|
334 |
+ del(headers["date"]) |
|
335 |
+ |
|
336 |
+ if not headers.has_key("x-amz-date"): |
|
337 |
+ headers["x-amz-date"] = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime()) |
|
338 |
+ |
|
339 |
+ signature = self.sign_request(headers) |
|
340 |
+ headers["Authorization"] = "AWS "+self.config.access_key+":"+signature |
|
341 |
+ |
|
342 |
+ request = {} |
|
343 |
+ request['resource'] = resource |
|
344 |
+ request['headers'] = headers |
|
345 |
+ request['method'] = operation['method'] |
|
346 |
+ |
|
347 |
+ return request |
|
348 |
+ |
|
349 |
+ def sign_request(self, headers): |
|
350 |
+ string_to_sign = headers['x-amz-date'] |
|
351 |
+ signature = base64.encodestring(hmac.new(self.config.secret_key, string_to_sign, sha1).digest()).strip() |
|
352 |
+ debug(u"CloudFront.sign_request('%s') = %s" % (string_to_sign, signature)) |
|
353 |
+ return signature |
|
354 |
+ |
|
355 |
+ def get_connection(self): |
|
356 |
+ if self.config.proxy_host != "": |
|
357 |
+ raise ParameterError("CloudFront commands don't work from behind a HTTP proxy") |
|
358 |
+ return httplib.HTTPSConnection(self.config.cloudfront_host) |
|
359 |
+ |
|
360 |
+ def _fail_wait(self, retries): |
|
361 |
+ # Wait a few seconds. The more it fails the more we wait. |
|
362 |
+ return (self._max_retries - retries + 1) * 3 |
|
363 |
+ |
|
364 |
+class Cmd(object): |
|
365 |
+ """ |
|
366 |
+ Class that implements CloudFront commands |
|
367 |
+ """ |
|
368 |
+ |
|
369 |
+ class Options(object): |
|
370 |
+ cf_cnames_add = [] |
|
371 |
+ cf_cnames_remove = [] |
|
372 |
+ cf_comment = None |
|
373 |
+ cf_enable = None |
|
374 |
+ |
|
375 |
+ def option_list(self): |
|
376 |
+ return [opt for opt in dir(self) if opt.startswith("cf_")] |
|
377 |
+ |
|
378 |
+ def update_option(self, option, value): |
|
379 |
+ setattr(Cmd.options, option, value) |
|
380 |
+ |
|
381 |
+ options = Options() |
|
382 |
+ |
|
383 |
+ @staticmethod |
|
384 |
+ def info(args): |
|
385 |
+ cf = CloudFront(Config()) |
|
386 |
+ if not args: |
|
387 |
+ response = cf.GetList() |
|
388 |
+ for d in response['dist_list'].dist_summs: |
|
389 |
+ pretty_output("Origin", S3UriS3.httpurl_to_s3uri(d.info['Origin'])) |
|
390 |
+ pretty_output("DistId", d.uri()) |
|
391 |
+ pretty_output("DomainName", d.info['DomainName']) |
|
392 |
+ pretty_output("Status", d.info['Status']) |
|
393 |
+ pretty_output("Enabled", d.info['Enabled']) |
|
394 |
+ output("") |
|
395 |
+ else: |
|
396 |
+ cfuris = [] |
|
397 |
+ for arg in args: |
|
398 |
+ cfuris.append(S3Uri(arg)) |
|
399 |
+ if cfuris[-1].type != 'cf': |
|
400 |
+ raise ParameterError("CloudFront URI required instead of: %s" % arg) |
|
401 |
+ for cfuri in cfuris: |
|
402 |
+ response = cf.GetDistInfo(cfuri) |
|
403 |
+ d = response['distribution'] |
|
404 |
+ dc = d.info['DistributionConfig'] |
|
405 |
+ pretty_output("Origin", S3UriS3.httpurl_to_s3uri(dc.info['Origin'])) |
|
406 |
+ pretty_output("DistId", d.uri()) |
|
407 |
+ pretty_output("DomainName", d.info['DomainName']) |
|
408 |
+ pretty_output("Status", d.info['Status']) |
|
409 |
+ pretty_output("CNAMEs", ", ".join(dc.info['CNAME'])) |
|
410 |
+ pretty_output("Comment", dc.info['Comment']) |
|
411 |
+ pretty_output("Enabled", dc.info['Enabled']) |
|
412 |
+ pretty_output("Etag", response['headers']['etag']) |
|
413 |
+ |
|
414 |
+ @staticmethod |
|
415 |
+ def create(args): |
|
416 |
+ cf = CloudFront(Config()) |
|
417 |
+ buckets = [] |
|
418 |
+ for arg in args: |
|
419 |
+ uri = S3Uri(arg) |
|
420 |
+ if uri.type != "s3": |
|
421 |
+ raise ParameterError("Bucket can only be created from a s3:// URI instead of: %s" % arg) |
|
422 |
+ if uri.object(): |
|
423 |
+ raise ParameterError("Use s3:// URI with a bucket name only instead of: %s" % arg) |
|
424 |
+ if not uri.is_dns_compatible(): |
|
425 |
+ raise ParameterError("CloudFront can only handle lowercase-named buckets.") |
|
426 |
+ buckets.append(uri) |
|
427 |
+ if not buckets: |
|
428 |
+ raise ParameterError("No valid bucket names found") |
|
429 |
+ for uri in buckets: |
|
430 |
+ info("Creating distribution from: %s" % uri) |
|
431 |
+ response = cf.CreateDistribution(uri, cnames_add = Cmd.options.cf_cnames_add, |
|
432 |
+ comment = Cmd.options.cf_comment) |
|
433 |
+ d = response['distribution'] |
|
434 |
+ dc = d.info['DistributionConfig'] |
|
435 |
+ output("Distribution created:") |
|
436 |
+ pretty_output("Origin", S3UriS3.httpurl_to_s3uri(dc.info['Origin'])) |
|
437 |
+ pretty_output("DistId", d.uri()) |
|
438 |
+ pretty_output("DomainName", d.info['DomainName']) |
|
439 |
+ pretty_output("CNAMEs", ", ".join(dc.info['CNAME'])) |
|
440 |
+ pretty_output("Comment", dc.info['Comment']) |
|
441 |
+ pretty_output("Status", d.info['Status']) |
|
442 |
+ pretty_output("Enabled", dc.info['Enabled']) |
|
443 |
+ pretty_output("Etag", response['headers']['etag']) |
|
444 |
+ |
|
445 |
+ @staticmethod |
|
446 |
+ def delete(args): |
|
447 |
+ cf = CloudFront(Config()) |
|
448 |
+ cfuris = [] |
|
449 |
+ for arg in args: |
|
450 |
+ cfuris.append(S3Uri(arg)) |
|
451 |
+ if cfuris[-1].type != 'cf': |
|
452 |
+ raise ParameterError("CloudFront URI required instead of: %s" % arg) |
|
453 |
+ for cfuri in cfuris: |
|
454 |
+ response = cf.DeleteDistribution(cfuri) |
|
455 |
+ if response['status'] >= 400: |
|
456 |
+ error("Distribution %s could not be deleted: %s" % (cfuri, response['reason'])) |
|
457 |
+ output("Distribution %s deleted" % cfuri) |
|
458 |
+ |
|
459 |
+ @staticmethod |
|
460 |
+ def modify(args): |
|
461 |
+ cf = CloudFront(Config()) |
|
462 |
+ cfuri = S3Uri(args.pop(0)) |
|
463 |
+ if cfuri.type != 'cf': |
|
464 |
+ raise ParameterError("CloudFront URI required instead of: %s" % arg) |
|
465 |
+ if len(args): |
|
466 |
+ raise ParameterError("Too many parameters. Modify one Distribution at a time.") |
|
467 |
+ |
|
468 |
+ response = cf.ModifyDistribution(cfuri, |
|
469 |
+ cnames_add = Cmd.options.cf_cnames_add, |
|
470 |
+ cnames_remove = Cmd.options.cf_cnames_remove, |
|
471 |
+ comment = Cmd.options.cf_comment, |
|
472 |
+ enabled = Cmd.options.cf_enable) |
|
473 |
+ if response['status'] >= 400: |
|
474 |
+ error("Distribution %s could not be modified: %s" % (cfuri, response['reason'])) |
|
475 |
+ output("Distribution modified: %s" % cfuri) |
|
476 |
+ response = cf.GetDistInfo(cfuri) |
|
477 |
+ d = response['distribution'] |
|
478 |
+ dc = d.info['DistributionConfig'] |
|
479 |
+ pretty_output("Origin", S3UriS3.httpurl_to_s3uri(dc.info['Origin'])) |
|
480 |
+ pretty_output("DistId", d.uri()) |
|
481 |
+ pretty_output("DomainName", d.info['DomainName']) |
|
482 |
+ pretty_output("Status", d.info['Status']) |
|
483 |
+ pretty_output("CNAMEs", ", ".join(dc.info['CNAME'])) |
|
484 |
+ pretty_output("Comment", dc.info['Comment']) |
|
485 |
+ pretty_output("Enabled", dc.info['Enabled']) |
|
486 |
+ pretty_output("Etag", response['headers']['etag']) |
... | ... |
@@ -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: |
... | ... |
@@ -38,21 +38,26 @@ class S3Error (S3Exception): |
38 | 38 |
if response.has_key("headers"): |
39 | 39 |
for header in response["headers"]: |
40 | 40 |
debug("HttpHeader: %s: %s" % (header, response["headers"][header])) |
41 |
- if response.has_key("data") and getRootTagName(response["data"]) == "Error": |
|
42 |
- tree = ET.fromstring(response["data"]) |
|
43 |
- for child in tree.getchildren(): |
|
41 |
+ if response.has_key("data"): |
|
42 |
+ tree = getTreeFromXml(response["data"]) |
|
43 |
+ error_node = tree |
|
44 |
+ if not error_node.tag == "Error": |
|
45 |
+ error_node = tree.find(".//Error") |
|
46 |
+ for child in error_node.getchildren(): |
|
44 | 47 |
if child.text != "": |
45 | 48 |
debug("ErrorXML: " + child.tag + ": " + repr(child.text)) |
46 | 49 |
self.info[child.tag] = child.text |
47 | 50 |
|
48 | 51 |
def __unicode__(self): |
49 |
- retval = "%d (%s)" % (self.status, self.reason) |
|
50 |
- try: |
|
51 |
- retval += (": %s" % self.info["Code"]) |
|
52 |
- except (AttributeError, KeyError): |
|
53 |
- pass |
|
52 |
+ retval = u"%d " % (self.status) |
|
53 |
+ retval += (u"(%s)" % (self.info.has_key("Code") and self.info["Code"] or self.reason)) |
|
54 |
+ if self.info.has_key("Message"): |
|
55 |
+ retval += (u": %s" % self.info["Message"]) |
|
54 | 56 |
return retval |
55 | 57 |
|
58 |
+class CloudFrontError(S3Error): |
|
59 |
+ pass |
|
60 |
+ |
|
56 | 61 |
class S3UploadError(S3Exception): |
57 | 62 |
pass |
58 | 63 |
|
... | ... |
@@ -72,16 +72,48 @@ class S3UriS3(S3Uri): |
72 | 72 |
def uri(self): |
73 | 73 |
return "/".join(["s3:/", self._bucket, self._object]) |
74 | 74 |
|
75 |
+ def is_dns_compatible(self): |
|
76 |
+ return S3.check_bucket_name_dns_conformity(self._bucket) |
|
77 |
+ |
|
75 | 78 |
def public_url(self): |
76 |
- if S3.check_bucket_name_dns_conformity(self._bucket): |
|
79 |
+ if self.is_dns_compatible(): |
|
77 | 80 |
return "http://%s.s3.amazonaws.com/%s" % (self._bucket, self._object) |
78 | 81 |
else: |
79 | 82 |
return "http://s3.amazonaws.com/%s/%s" % (self._bucket, self._object) |
80 | 83 |
|
84 |
+ def host_name(self): |
|
85 |
+ if self.is_dns_compatible(): |
|
86 |
+ return "%s.s3.amazonaws.com" % (self._bucket) |
|
87 |
+ else: |
|
88 |
+ return "s3.amazonaws.com" |
|
89 |
+ |
|
81 | 90 |
@staticmethod |
82 | 91 |
def compose_uri(bucket, object = ""): |
83 | 92 |
return "s3://%s/%s" % (bucket, object) |
84 | 93 |
|
94 |
+ @staticmethod |
|
95 |
+ def httpurl_to_s3uri(http_url): |
|
96 |
+ m=re.match("(https?://)?([^/]+)/?(.*)", http_url, re.IGNORECASE) |
|
97 |
+ hostname, object = m.groups()[1:] |
|
98 |
+ hostname = hostname.lower() |
|
99 |
+ if hostname == "s3.amazonaws.com": |
|
100 |
+ ## old-style url: http://s3.amazonaws.com/bucket/object |
|
101 |
+ if object.count("/") == 0: |
|
102 |
+ ## no object given |
|
103 |
+ bucket = object |
|
104 |
+ object = "" |
|
105 |
+ else: |
|
106 |
+ ## bucket/object |
|
107 |
+ bucket, object = object.split("/", 1) |
|
108 |
+ elif hostname.endswith(".s3.amazonaws.com"): |
|
109 |
+ ## new-style url: http://bucket.s3.amazonaws.com/object |
|
110 |
+ bucket = hostname[:-(len(".s3.amazonaws.com"))] |
|
111 |
+ else: |
|
112 |
+ raise ValueError("Unable to parse URL: %s" % http_url) |
|
113 |
+ return S3Uri("s3://%(bucket)s/%(object)s" % { |
|
114 |
+ 'bucket' : bucket, |
|
115 |
+ 'object' : object }) |
|
116 |
+ |
|
85 | 117 |
class S3UriS3FS(S3Uri): |
86 | 118 |
type = "s3fs" |
87 | 119 |
_re = re.compile("^s3fs://([^/]*)/?(.*)", re.IGNORECASE) |
... | ... |
@@ -124,6 +156,22 @@ class S3UriFile(S3Uri): |
124 | 124 |
def dirname(self): |
125 | 125 |
return os.path.dirname(self.path()) |
126 | 126 |
|
127 |
+class S3UriCloudFront(S3Uri): |
|
128 |
+ type = "cf" |
|
129 |
+ _re = re.compile("^cf://([^/]*)/?", re.IGNORECASE) |
|
130 |
+ def __init__(self, string): |
|
131 |
+ match = self._re.match(string) |
|
132 |
+ if not match: |
|
133 |
+ raise ValueError("%s: not a CloudFront URI" % string) |
|
134 |
+ groups = match.groups() |
|
135 |
+ self._dist_id = groups[0] |
|
136 |
+ |
|
137 |
+ def dist_id(self): |
|
138 |
+ return self._dist_id |
|
139 |
+ |
|
140 |
+ def uri(self): |
|
141 |
+ return "/".join(["cf:/", self.dist_id()]) |
|
142 |
+ |
|
127 | 143 |
if __name__ == "__main__": |
128 | 144 |
uri = S3Uri("s3://bucket/object") |
129 | 145 |
print "type() =", type(uri) |
... | ... |
@@ -153,3 +201,11 @@ if __name__ == "__main__": |
153 | 153 |
print "uri.type=", uri.type |
154 | 154 |
print "path =", uri.path() |
155 | 155 |
|
156 |
+ |
|
157 |
+ uri = S3Uri("cf://1234567890ABCD/") |
|
158 |
+ print "type() =", type(uri) |
|
159 |
+ print "uri =", uri |
|
160 |
+ print "uri.type=", uri.type |
|
161 |
+ print "dist_id =", uri.dist_id() |
|
162 |
|
|
163 |
+ |
... | ... |
@@ -63,7 +63,21 @@ def getListFromXml(xml, node): |
63 | 63 |
tree = getTreeFromXml(xml) |
64 | 64 |
nodes = tree.findall('.//%s' % (node)) |
65 | 65 |
return parseNodes(nodes) |
66 |
- |
|
66 |
+ |
|
67 |
+def getDictFromTree(tree): |
|
68 |
+ ret_dict = {} |
|
69 |
+ for child in tree.getchildren(): |
|
70 |
+ if child.getchildren(): |
|
71 |
+ ## Complex-type child. We're not interested |
|
72 |
+ continue |
|
73 |
+ if ret_dict.has_key(child.tag): |
|
74 |
+ if not type(ret_dict[child.tag]) == list: |
|
75 |
+ ret_dict[child.tag] = [ret_dict[child.tag]] |
|
76 |
+ ret_dict[child.tag].append(child.text or "") |
|
77 |
+ else: |
|
78 |
+ ret_dict[child.tag] = child.text or "" |
|
79 |
+ return ret_dict |
|
80 |
+ |
|
67 | 81 |
def getTextFromXml(xml, xpath): |
68 | 82 |
tree = getTreeFromXml(xml) |
69 | 83 |
if tree.tag.endswith(xpath): |
... | ... |
@@ -75,6 +89,20 @@ def getRootTagName(xml): |
75 | 75 |
tree = getTreeFromXml(xml) |
76 | 76 |
return tree.tag |
77 | 77 |
|
78 |
+def xmlTextNode(tag_name, text): |
|
79 |
+ el = ET.Element(tag_name) |
|
80 |
+ el.text = unicode(text) |
|
81 |
+ return el |
|
82 |
+ |
|
83 |
+def appendXmlTextNode(tag_name, text, parent): |
|
84 |
+ """ |
|
85 |
+ Creates a new <tag_name> Node and sets |
|
86 |
+ its content to 'text'. Then appends the |
|
87 |
+ created Node to 'parent' element if given. |
|
88 |
+ Returns the newly created Node. |
|
89 |
+ """ |
|
90 |
+ parent.append(xmlTextNode(tag_name, text)) |
|
91 |
+ |
|
78 | 92 |
def dateS3toPython(date): |
79 | 93 |
date = re.compile("\.\d\d\dZ").sub(".000Z", date) |
80 | 94 |
return time.strptime(date, "%Y-%m-%dT%H:%M:%S.000Z") |
... | ... |
@@ -1230,8 +1230,8 @@ def process_patterns(patterns_list, patterns_from, is_glob, option_txt = ""): |
1230 | 1230 |
|
1231 | 1231 |
return patterns_compiled, patterns_textual |
1232 | 1232 |
|
1233 |
-commands = {} |
|
1234 |
-commands_list = [ |
|
1233 |
+def get_commands_list(): |
|
1234 |
+ return [ |
|
1235 | 1235 |
{"cmd":"mb", "label":"Make bucket", "param":"s3://BUCKET", "func":cmd_bucket_create, "argc":1}, |
1236 | 1236 |
{"cmd":"rb", "label":"Remove bucket", "param":"s3://BUCKET", "func":cmd_bucket_delete, "argc":1}, |
1237 | 1237 |
{"cmd":"ls", "label":"List objects or buckets", "param":"[s3://BUCKET[/PREFIX]]", "func":cmd_ls, "argc":0}, |
... | ... |
@@ -1246,9 +1246,15 @@ commands_list = [ |
1246 | 1246 |
{"cmd":"cp", "label":"Copy object", "param":"s3://BUCKET1/OBJECT1 s3://BUCKET2[/OBJECT2]", "func":cmd_cp, "argc":2}, |
1247 | 1247 |
{"cmd":"mv", "label":"Move object", "param":"s3://BUCKET1/OBJECT1 s3://BUCKET2[/OBJECT2]", "func":cmd_mv, "argc":2}, |
1248 | 1248 |
{"cmd":"setacl", "label":"Modify Access control list for Bucket or Object", "param":"s3://BUCKET[/OBJECT]", "func":cmd_setacl, "argc":1}, |
1249 |
+ ## CloudFront commands |
|
1250 |
+ {"cmd":"cflist", "label":"List CloudFront distribution points", "param":"", "func":CfCmd.info, "argc":0}, |
|
1251 |
+ {"cmd":"cfinfo", "label":"Display CloudFront distribution point parameters", "param":"[cf://DIST_ID]", "func":CfCmd.info, "argc":0}, |
|
1252 |
+ {"cmd":"cfcreate", "label":"Create CloudFront distribution point", "param":"s3://BUCKET", "func":CfCmd.create, "argc":1}, |
|
1253 |
+ {"cmd":"cfdelete", "label":"Delete CloudFront distribution point", "param":"cf://DIST_ID", "func":CfCmd.delete, "argc":1}, |
|
1254 |
+ {"cmd":"cfmodify", "label":"Change CloudFront distribution point parameters", "param":"cf://DIST_ID", "func":CfCmd.modify, "argc":1}, |
|
1249 | 1255 |
] |
1250 | 1256 |
|
1251 |
-def format_commands(progname): |
|
1257 |
+def format_commands(progname, commands_list): |
|
1252 | 1258 |
help = "Commands:\n" |
1253 | 1259 |
for cmd in commands_list: |
1254 | 1260 |
help += " %s\n %s %s %s\n" % (cmd["label"], progname, cmd["cmd"], cmd["param"]) |
... | ... |
@@ -1277,6 +1283,9 @@ def main(): |
1277 | 1277 |
sys.stderr.write("ERROR: Python 2.4 or higher required, sorry.\n") |
1278 | 1278 |
sys.exit(1) |
1279 | 1279 |
|
1280 |
+ commands_list = get_commands_list() |
|
1281 |
+ commands = {} |
|
1282 |
+ |
|
1280 | 1283 |
## Populate "commands" from "commands_list" |
1281 | 1284 |
for cmd in commands_list: |
1282 | 1285 |
if cmd.has_key("cmd"): |
... | ... |
@@ -1337,6 +1346,11 @@ def main(): |
1337 | 1337 |
|
1338 | 1338 |
optparser.add_option( "--progress", dest="progress_meter", action="store_true", help="Display progress meter (default on TTY).") |
1339 | 1339 |
optparser.add_option( "--no-progress", dest="progress_meter", action="store_false", help="Don't display progress meter (default on non-TTY).") |
1340 |
+ optparser.add_option( "--enable", dest="cf_enable", action="store_true", help="Enable given CloudFront distribution (only for [cfmodify] command)") |
|
1341 |
+ optparser.add_option( "--disable", dest="cf_enable", action="store_false", help="Enable given CloudFront distribution (only for [cfmodify] command)") |
|
1342 |
+ optparser.add_option( "--cf-add-cname", dest="cf_cnames_add", action="append", metavar="CNAME", help="Add given CNAME to a CloudFront distribution (only for [cfcreate] and [cfmodify] commands)") |
|
1343 |
+ optparser.add_option( "--cf-remove-cname", dest="cf_cnames_remove", action="append", metavar="CNAME", help="Remove given CNAME from a CloudFront distribution (only for [cfmodify] command)") |
|
1344 |
+ optparser.add_option( "--cf-comment", dest="cf_comment", action="store", metavar="COMMENT", help="Set COMMENT for a given CloudFront distribution (only for [cfcreate] and [cfmodify] commands)") |
|
1340 | 1345 |
optparser.add_option("-v", "--verbose", dest="verbosity", action="store_const", const=logging.INFO, help="Enable verbose output.") |
1341 | 1346 |
optparser.add_option("-d", "--debug", dest="verbosity", action="store_const", const=logging.DEBUG, help="Enable debug output.") |
1342 | 1347 |
optparser.add_option( "--version", dest="show_version", action="store_true", help="Show s3cmd version (%s) and exit." % (PkgInfo.version)) |
... | ... |
@@ -1346,7 +1360,7 @@ def main(): |
1346 | 1346 |
'Amazon S3 storage. It allows for making and removing '+ |
1347 | 1347 |
'"buckets" and uploading, downloading and removing '+ |
1348 | 1348 |
'"objects" from these buckets.') |
1349 |
- optparser.epilog = format_commands(optparser.get_prog_name()) |
|
1349 |
+ optparser.epilog = format_commands(optparser.get_prog_name(), commands_list) |
|
1350 | 1350 |
optparser.epilog += ("\nSee program homepage for more information at\n%s\n" % PkgInfo.url) |
1351 | 1351 |
|
1352 | 1352 |
(options, args) = optparser.parse_args() |
... | ... |
@@ -1400,11 +1414,21 @@ def main(): |
1400 | 1400 |
for option in cfg.option_list(): |
1401 | 1401 |
try: |
1402 | 1402 |
if getattr(options, option) != None: |
1403 |
- debug(u"Updating %s -> %s" % (option, getattr(options, option))) |
|
1403 |
+ debug(u"Updating Config.Config %s -> %s" % (option, getattr(options, option))) |
|
1404 | 1404 |
cfg.update_option(option, getattr(options, option)) |
1405 | 1405 |
except AttributeError: |
1406 | 1406 |
## Some Config() options are not settable from command line |
1407 | 1407 |
pass |
1408 |
+ |
|
1409 |
+ ## Update CloudFront options if some were set |
|
1410 |
+ for option in CfCmd.options.option_list(): |
|
1411 |
+ try: |
|
1412 |
+ if getattr(options, option) != None: |
|
1413 |
+ debug(u"Updating CloudFront.Cmd %s -> %s" % (option, getattr(options, option))) |
|
1414 |
+ CfCmd.options.update_option(option, getattr(options, option)) |
|
1415 |
+ except AttributeError: |
|
1416 |
+ ## Some CloudFront.Cmd.Options() options are not settable from command line |
|
1417 |
+ pass |
|
1408 | 1418 |
|
1409 | 1419 |
## Set output and filesystem encoding for printing out filenames. |
1410 | 1420 |
sys.stdout = codecs.getwriter(cfg.encoding)(sys.stdout, "replace") |
... | ... |
@@ -1469,8 +1493,6 @@ def main(): |
1469 | 1469 |
cmd_func(args) |
1470 | 1470 |
except S3Error, e: |
1471 | 1471 |
error(u"S3 error: %s" % e) |
1472 |
- if e.info.has_key("Message"): |
|
1473 |
- error(e.info['Message']) |
|
1474 | 1472 |
sys.exit(1) |
1475 | 1473 |
except ParameterError, e: |
1476 | 1474 |
error(u"Parameter problem: %s" % e) |
... | ... |
@@ -1489,6 +1511,7 @@ if __name__ == '__main__': |
1489 | 1489 |
from S3.Exceptions import * |
1490 | 1490 |
from S3.Utils import unicodise |
1491 | 1491 |
from S3.Progress import Progress |
1492 |
+ from S3.CloudFront import Cmd as CfCmd |
|
1492 | 1493 |
|
1493 | 1494 |
main() |
1494 | 1495 |
sys.exit(0) |