Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 753 lines (668 sloc) 31.433 kb
b3488ba @mludvig * S3/CloudFront.py: New module for CloudFront support.
mludvig authored
1 ## Amazon CloudFront support
2 ## Author: Michal Ludvig <michal@logix.cz>
3 ## http://www.logix.cz/michal
4 ## License: GPL Version 2
5
bf68d32 @mludvig * S3/CloudFront.py: Initial support for creating new Distros,
mludvig authored
6 import sys
b3488ba @mludvig * S3/CloudFront.py: New module for CloudFront support.
mludvig authored
7 import time
8 import httplib
0c7bf27 @mludvig Support for CloudFront invalidation using [sync --cf-invalidate]
mludvig authored
9 import random
10 from datetime import datetime
b3488ba @mludvig * S3/CloudFront.py: New module for CloudFront support.
mludvig authored
11 from logging import debug, info, warning, error
12
13 try:
d439efb @mludvig ATTENTION -- Mega WhiteSpace conversion !!!
mludvig authored
14 import xml.etree.ElementTree as ET
b3488ba @mludvig * S3/CloudFront.py: New module for CloudFront support.
mludvig authored
15 except ImportError:
d439efb @mludvig ATTENTION -- Mega WhiteSpace conversion !!!
mludvig authored
16 import elementtree.ElementTree as ET
b3488ba @mludvig * S3/CloudFront.py: New module for CloudFront support.
mludvig authored
17
bf68d32 @mludvig * S3/CloudFront.py: Initial support for creating new Distros,
mludvig authored
18 from Config import Config
19 from Exceptions import *
b020ea0 @mludvig * s3cmd, S3/CloudFront.py, S3/Config.py: Support access
mludvig authored
20 from Utils import getTreeFromXml, appendXmlTextNode, getDictFromTree, dateS3toPython, sign_string, getBucketFromHostname, getHostnameFromBucket
84d1dc3 @mludvig * S3/CloudFront.py: Added Cmd.info() that handles both
mludvig authored
21 from S3Uri import S3Uri, S3UriS3
0c7bf27 @mludvig Support for CloudFront invalidation using [sync --cf-invalidate]
mludvig authored
22 from FileLists import fetch_remote_list
23
24 cloudfront_api_version = "2010-11-01"
25 cloudfront_resource = "/%(api_ver)s/distribution" % { 'api_ver' : cloudfront_api_version }
bf68d32 @mludvig * S3/CloudFront.py: Initial support for creating new Distros,
mludvig authored
26
27 def output(message):
d439efb @mludvig ATTENTION -- Mega WhiteSpace conversion !!!
mludvig authored
28 sys.stdout.write(message + "\n")
bf68d32 @mludvig * S3/CloudFront.py: Initial support for creating new Distros,
mludvig authored
29
30 def pretty_output(label, message):
d439efb @mludvig ATTENTION -- Mega WhiteSpace conversion !!!
mludvig authored
31 #label = ("%s " % label).ljust(20, ".")
32 label = ("%s:" % label).ljust(15)
33 output("%s %s" % (label, message))
bf68d32 @mludvig * S3/CloudFront.py: Initial support for creating new Distros,
mludvig authored
34
35 class DistributionSummary(object):
d439efb @mludvig ATTENTION -- Mega WhiteSpace conversion !!!
mludvig authored
36 ## Example:
37 ##
38 ## <DistributionSummary>
39 ## <Id>1234567890ABC</Id>
40 ## <Status>Deployed</Status>
41 ## <LastModifiedTime>2009-01-16T11:49:02.189Z</LastModifiedTime>
42 ## <DomainName>blahblahblah.cloudfront.net</DomainName>
43 ## <S3Origin>
44 ## <DNSName>example.bucket.s3.amazonaws.com</DNSName>
45 ## </S3Origin>
46 ## <CNAME>cdn.example.com</CNAME>
47 ## <CNAME>img.example.com</CNAME>
48 ## <Comment>What Ever</Comment>
49 ## <Enabled>true</Enabled>
50 ## </DistributionSummary>
51
52 def __init__(self, tree):
53 if tree.tag != "DistributionSummary":
54 raise ValueError("Expected <DistributionSummary /> xml, got: <%s />" % tree.tag)
55 self.parse(tree)
56
57 def parse(self, tree):
58 self.info = getDictFromTree(tree)
59 self.info['Enabled'] = (self.info['Enabled'].lower() == "true")
60 if self.info.has_key("CNAME") and type(self.info['CNAME']) != list:
61 self.info['CNAME'] = [self.info['CNAME']]
62
63 def uri(self):
64 return S3Uri("cf://%s" % self.info['Id'])
84d1dc3 @mludvig * S3/CloudFront.py: Added Cmd.info() that handles both
mludvig authored
65
bf68d32 @mludvig * S3/CloudFront.py: Initial support for creating new Distros,
mludvig authored
66 class DistributionList(object):
d439efb @mludvig ATTENTION -- Mega WhiteSpace conversion !!!
mludvig authored
67 ## Example:
68 ##
69 ## <DistributionList xmlns="http://cloudfront.amazonaws.com/doc/2010-07-15/">
70 ## <Marker />
71 ## <MaxItems>100</MaxItems>
72 ## <IsTruncated>false</IsTruncated>
73 ## <DistributionSummary>
74 ## ... handled by DistributionSummary() class ...
75 ## </DistributionSummary>
76 ## </DistributionList>
77
78 def __init__(self, xml):
79 tree = getTreeFromXml(xml)
80 if tree.tag != "DistributionList":
81 raise ValueError("Expected <DistributionList /> xml, got: <%s />" % tree.tag)
82 self.parse(tree)
83
84 def parse(self, tree):
85 self.info = getDictFromTree(tree)
86 ## Normalise some items
87 self.info['IsTruncated'] = (self.info['IsTruncated'].lower() == "true")
88
89 self.dist_summs = []
90 for dist_summ in tree.findall(".//DistributionSummary"):
91 self.dist_summs.append(DistributionSummary(dist_summ))
bf68d32 @mludvig * S3/CloudFront.py: Initial support for creating new Distros,
mludvig authored
92
b3488ba @mludvig * S3/CloudFront.py: New module for CloudFront support.
mludvig authored
93 class Distribution(object):
d439efb @mludvig ATTENTION -- Mega WhiteSpace conversion !!!
mludvig authored
94 ## Example:
95 ##
96 ## <Distribution xmlns="http://cloudfront.amazonaws.com/doc/2010-07-15/">
97 ## <Id>1234567890ABC</Id>
98 ## <Status>InProgress</Status>
99 ## <LastModifiedTime>2009-01-16T13:07:11.319Z</LastModifiedTime>
100 ## <DomainName>blahblahblah.cloudfront.net</DomainName>
101 ## <DistributionConfig>
102 ## ... handled by DistributionConfig() class ...
103 ## </DistributionConfig>
104 ## </Distribution>
105
106 def __init__(self, xml):
107 tree = getTreeFromXml(xml)
108 if tree.tag != "Distribution":
109 raise ValueError("Expected <Distribution /> xml, got: <%s />" % tree.tag)
110 self.parse(tree)
111
112 def parse(self, tree):
113 self.info = getDictFromTree(tree)
114 ## Normalise some items
115 self.info['LastModifiedTime'] = dateS3toPython(self.info['LastModifiedTime'])
116
117 self.info['DistributionConfig'] = DistributionConfig(tree = tree.find(".//DistributionConfig"))
118
119 def uri(self):
120 return S3Uri("cf://%s" % self.info['Id'])
bf68d32 @mludvig * S3/CloudFront.py: Initial support for creating new Distros,
mludvig authored
121
122 class DistributionConfig(object):
d439efb @mludvig ATTENTION -- Mega WhiteSpace conversion !!!
mludvig authored
123 ## Example:
124 ##
125 ## <DistributionConfig>
126 ## <Origin>somebucket.s3.amazonaws.com</Origin>
127 ## <CallerReference>s3://somebucket/</CallerReference>
128 ## <Comment>http://somebucket.s3.amazonaws.com/</Comment>
129 ## <Enabled>true</Enabled>
130 ## <Logging>
131 ## <Bucket>bu.ck.et</Bucket>
132 ## <Prefix>/cf-somebucket/</Prefix>
133 ## </Logging>
134 ## </DistributionConfig>
135
136 EMPTY_CONFIG = "<DistributionConfig><Origin/><CallerReference/><Enabled>true</Enabled></DistributionConfig>"
137 xmlns = "http://cloudfront.amazonaws.com/doc/%(api_ver)s/" % { 'api_ver' : cloudfront_api_version }
138 def __init__(self, xml = None, tree = None):
139 if xml is None:
140 xml = DistributionConfig.EMPTY_CONFIG
141
142 if tree is None:
143 tree = getTreeFromXml(xml)
144
145 if tree.tag != "DistributionConfig":
146 raise ValueError("Expected <DistributionConfig /> xml, got: <%s />" % tree.tag)
147 self.parse(tree)
148
149 def parse(self, tree):
150 self.info = getDictFromTree(tree)
151 self.info['Enabled'] = (self.info['Enabled'].lower() == "true")
152 if not self.info.has_key("CNAME"):
153 self.info['CNAME'] = []
154 if type(self.info['CNAME']) != list:
155 self.info['CNAME'] = [self.info['CNAME']]
156 self.info['CNAME'] = [cname.lower() for cname in self.info['CNAME']]
157 if not self.info.has_key("Comment"):
158 self.info['Comment'] = ""
159 if not self.info.has_key("DefaultRootObject"):
160 self.info['DefaultRootObject'] = ""
161 ## Figure out logging - complex node not parsed by getDictFromTree()
162 logging_nodes = tree.findall(".//Logging")
163 if logging_nodes:
164 logging_dict = getDictFromTree(logging_nodes[0])
165 logging_dict['Bucket'], success = getBucketFromHostname(logging_dict['Bucket'])
166 if not success:
167 warning("Logging to unparsable bucket name: %s" % logging_dict['Bucket'])
168 self.info['Logging'] = S3UriS3("s3://%(Bucket)s/%(Prefix)s" % logging_dict)
169 else:
170 self.info['Logging'] = None
171
172 def __str__(self):
173 tree = ET.Element("DistributionConfig")
174 tree.attrib['xmlns'] = DistributionConfig.xmlns
175
176 ## Retain the order of the following calls!
177 appendXmlTextNode("Origin", self.info['Origin'], tree)
178 appendXmlTextNode("CallerReference", self.info['CallerReference'], tree)
179 for cname in self.info['CNAME']:
180 appendXmlTextNode("CNAME", cname.lower(), tree)
181 if self.info['Comment']:
182 appendXmlTextNode("Comment", self.info['Comment'], tree)
183 appendXmlTextNode("Enabled", str(self.info['Enabled']).lower(), tree)
184 # don't create a empty DefaultRootObject element as it would result in a MalformedXML error
185 if str(self.info['DefaultRootObject']):
186 appendXmlTextNode("DefaultRootObject", str(self.info['DefaultRootObject']), tree)
187 if self.info['Logging']:
188 logging_el = ET.Element("Logging")
189 appendXmlTextNode("Bucket", getHostnameFromBucket(self.info['Logging'].bucket()), logging_el)
190 appendXmlTextNode("Prefix", self.info['Logging'].object(), logging_el)
191 tree.append(logging_el)
192 return ET.tostring(tree)
b3488ba @mludvig * S3/CloudFront.py: New module for CloudFront support.
mludvig authored
193
26c9954 @mludvig Support for checking status of CF Invalidation Requests [cfinvalinfo].
mludvig authored
194 class Invalidation(object):
d439efb @mludvig ATTENTION -- Mega WhiteSpace conversion !!!
mludvig authored
195 ## Example:
196 ##
197 ## <Invalidation xmlns="http://cloudfront.amazonaws.com/doc/2010-11-01/">
198 ## <Id>id</Id>
199 ## <Status>status</Status>
200 ## <CreateTime>date</CreateTime>
201 ## <InvalidationBatch>
202 ## <Path>/image1.jpg</Path>
203 ## <Path>/image2.jpg</Path>
204 ## <Path>/videos/movie.flv</Path>
205 ## <CallerReference>my-batch</CallerReference>
206 ## </InvalidationBatch>
207 ## </Invalidation>
208
209 def __init__(self, xml):
210 tree = getTreeFromXml(xml)
211 if tree.tag != "Invalidation":
212 raise ValueError("Expected <Invalidation /> xml, got: <%s />" % tree.tag)
213 self.parse(tree)
214
215 def parse(self, tree):
216 self.info = getDictFromTree(tree)
217
218 def __str__(self):
219 return str(self.info)
26c9954 @mludvig Support for checking status of CF Invalidation Requests [cfinvalinfo].
mludvig authored
220
221 class InvalidationList(object):
d439efb @mludvig ATTENTION -- Mega WhiteSpace conversion !!!
mludvig authored
222 ## Example:
223 ##
224 ## <InvalidationList>
225 ## <Marker/>
226 ## <NextMarker>Invalidation ID</NextMarker>
227 ## <MaxItems>2</MaxItems>
228 ## <IsTruncated>true</IsTruncated>
229 ## <InvalidationSummary>
230 ## <Id>[Second Invalidation ID]</Id>
231 ## <Status>Completed</Status>
232 ## </InvalidationSummary>
233 ## <InvalidationSummary>
234 ## <Id>[First Invalidation ID]</Id>
235 ## <Status>Completed</Status>
236 ## </InvalidationSummary>
237 ## </InvalidationList>
238
239 def __init__(self, xml):
240 tree = getTreeFromXml(xml)
241 if tree.tag != "InvalidationList":
242 raise ValueError("Expected <InvalidationList /> xml, got: <%s />" % tree.tag)
243 self.parse(tree)
244
245 def parse(self, tree):
246 self.info = getDictFromTree(tree)
247
248 def __str__(self):
249 return str(self.info)
26c9954 @mludvig Support for checking status of CF Invalidation Requests [cfinvalinfo].
mludvig authored
250
0c7bf27 @mludvig Support for CloudFront invalidation using [sync --cf-invalidate]
mludvig authored
251 class InvalidationBatch(object):
d439efb @mludvig ATTENTION -- Mega WhiteSpace conversion !!!
mludvig authored
252 ## Example:
253 ##
254 ## <InvalidationBatch>
255 ## <Path>/image1.jpg</Path>
256 ## <Path>/image2.jpg</Path>
257 ## <Path>/videos/movie.flv</Path>
258 ## <Path>/sound%20track.mp3</Path>
259 ## <CallerReference>my-batch</CallerReference>
260 ## </InvalidationBatch>
261
262 def __init__(self, reference = None, distribution = None, paths = []):
263 if reference:
264 self.reference = reference
265 else:
266 if not distribution:
267 distribution="0"
268 self.reference = "%s.%s.%s" % (distribution,
269 datetime.strftime(datetime.now(),"%Y%m%d%H%M%S"),
270 random.randint(1000,9999))
271 self.paths = []
272 self.add_objects(paths)
273
274 def add_objects(self, paths):
275 self.paths.extend(paths)
276
277 def get_reference(self):
278 return self.reference
279
280 def __str__(self):
281 tree = ET.Element("InvalidationBatch")
282
283 for path in self.paths:
284 if path[0] != "/":
285 path = "/" + path
286 appendXmlTextNode("Path", path, tree)
287 appendXmlTextNode("CallerReference", self.reference, tree)
288 return ET.tostring(tree)
0c7bf27 @mludvig Support for CloudFront invalidation using [sync --cf-invalidate]
mludvig authored
289
b3488ba @mludvig * S3/CloudFront.py: New module for CloudFront support.
mludvig authored
290 class CloudFront(object):
d439efb @mludvig ATTENTION -- Mega WhiteSpace conversion !!!
mludvig authored
291 operations = {
292 "CreateDist" : { 'method' : "POST", 'resource' : "" },
293 "DeleteDist" : { 'method' : "DELETE", 'resource' : "/%(dist_id)s" },
294 "GetList" : { 'method' : "GET", 'resource' : "" },
295 "GetDistInfo" : { 'method' : "GET", 'resource' : "/%(dist_id)s" },
296 "GetDistConfig" : { 'method' : "GET", 'resource' : "/%(dist_id)s/config" },
297 "SetDistConfig" : { 'method' : "PUT", 'resource' : "/%(dist_id)s/config" },
298 "Invalidate" : { 'method' : "POST", 'resource' : "/%(dist_id)s/invalidation" },
299 "GetInvalList" : { 'method' : "GET", 'resource' : "/%(dist_id)s/invalidation" },
300 "GetInvalInfo" : { 'method' : "GET", 'resource' : "/%(dist_id)s/invalidation/%(request_id)s" },
301 }
302
303 ## Maximum attempts of re-issuing failed requests
304 _max_retries = 5
305 dist_list = None
306
307 def __init__(self, config):
308 self.config = config
309
310 ## --------------------------------------------------
311 ## Methods implementing CloudFront API
312 ## --------------------------------------------------
313
314 def GetList(self):
315 response = self.send_request("GetList")
316 response['dist_list'] = DistributionList(response['data'])
317 if response['dist_list'].info['IsTruncated']:
318 raise NotImplementedError("List is truncated. Ask s3cmd author to add support.")
319 ## TODO: handle Truncated
320 return response
321
322 def CreateDistribution(self, uri, cnames_add = [], comment = None, logging = None, default_root_object = None):
323 dist_config = DistributionConfig()
324 dist_config.info['Enabled'] = True
325 dist_config.info['Origin'] = uri.host_name()
326 dist_config.info['CallerReference'] = str(uri)
327 dist_config.info['DefaultRootObject'] = default_root_object
328 if comment == None:
329 dist_config.info['Comment'] = uri.public_url()
330 else:
331 dist_config.info['Comment'] = comment
332 for cname in cnames_add:
333 if dist_config.info['CNAME'].count(cname) == 0:
334 dist_config.info['CNAME'].append(cname)
335 if logging:
336 dist_config.info['Logging'] = S3UriS3(logging)
337 request_body = str(dist_config)
338 debug("CreateDistribution(): request_body: %s" % request_body)
339 response = self.send_request("CreateDist", body = request_body)
340 response['distribution'] = Distribution(response['data'])
341 return response
342
343 def ModifyDistribution(self, cfuri, cnames_add = [], cnames_remove = [],
344 comment = None, enabled = None, logging = None,
678159c @mludvig * s3cmd, S3/CloudFront.py, S3/Config.py: Added support
mludvig authored
345 default_root_object = None):
d439efb @mludvig ATTENTION -- Mega WhiteSpace conversion !!!
mludvig authored
346 if cfuri.type != "cf":
347 raise ValueError("Expected CFUri instead of: %s" % cfuri)
348 # Get current dist status (enabled/disabled) and Etag
349 info("Checking current status of %s" % cfuri)
350 response = self.GetDistConfig(cfuri)
351 dc = response['dist_config']
352 if enabled != None:
353 dc.info['Enabled'] = enabled
354 if comment != None:
355 dc.info['Comment'] = comment
356 if default_root_object != None:
357 dc.info['DefaultRootObject'] = default_root_object
358 for cname in cnames_add:
359 if dc.info['CNAME'].count(cname) == 0:
360 dc.info['CNAME'].append(cname)
361 for cname in cnames_remove:
362 while dc.info['CNAME'].count(cname) > 0:
363 dc.info['CNAME'].remove(cname)
364 if logging != None:
365 if logging == False:
366 dc.info['Logging'] = False
367 else:
368 dc.info['Logging'] = S3UriS3(logging)
369 response = self.SetDistConfig(cfuri, dc, response['headers']['etag'])
370 return response
371
372 def DeleteDistribution(self, cfuri):
373 if cfuri.type != "cf":
374 raise ValueError("Expected CFUri instead of: %s" % cfuri)
375 # Get current dist status (enabled/disabled) and Etag
376 info("Checking current status of %s" % cfuri)
377 response = self.GetDistConfig(cfuri)
378 if response['dist_config'].info['Enabled']:
379 info("Distribution is ENABLED. Disabling first.")
380 response['dist_config'].info['Enabled'] = False
381 response = self.SetDistConfig(cfuri, response['dist_config'],
382 response['headers']['etag'])
383 warning("Waiting for Distribution to become disabled.")
384 warning("This may take several minutes, please wait.")
385 while True:
386 response = self.GetDistInfo(cfuri)
387 d = response['distribution']
388 if d.info['Status'] == "Deployed" and d.info['Enabled'] == False:
389 info("Distribution is now disabled")
390 break
391 warning("Still waiting...")
392 time.sleep(10)
393 headers = {}
394 headers['if-match'] = response['headers']['etag']
395 response = self.send_request("DeleteDist", dist_id = cfuri.dist_id(),
396 headers = headers)
397 return response
398
399 def GetDistInfo(self, cfuri):
400 if cfuri.type != "cf":
401 raise ValueError("Expected CFUri instead of: %s" % cfuri)
402 response = self.send_request("GetDistInfo", dist_id = cfuri.dist_id())
403 response['distribution'] = Distribution(response['data'])
404 return response
405
406 def GetDistConfig(self, cfuri):
407 if cfuri.type != "cf":
408 raise ValueError("Expected CFUri instead of: %s" % cfuri)
409 response = self.send_request("GetDistConfig", dist_id = cfuri.dist_id())
410 response['dist_config'] = DistributionConfig(response['data'])
411 return response
412
413 def SetDistConfig(self, cfuri, dist_config, etag = None):
414 if etag == None:
415 debug("SetDistConfig(): Etag not set. Fetching it first.")
416 etag = self.GetDistConfig(cfuri)['headers']['etag']
417 debug("SetDistConfig(): Etag = %s" % etag)
418 request_body = str(dist_config)
419 debug("SetDistConfig(): request_body: %s" % request_body)
420 headers = {}
421 headers['if-match'] = etag
422 response = self.send_request("SetDistConfig", dist_id = cfuri.dist_id(),
423 body = request_body, headers = headers)
424 return response
425
426 def InvalidateObjects(self, uri, paths):
427 # uri could be either cf:// or s3:// uri
428 cfuri = self.get_dist_name_for_bucket(uri)
429 if len(paths) > 999:
430 try:
431 tmp_filename = Utils.mktmpfile()
432 f = open(tmp_filename, "w")
433 f.write("\n".join(paths)+"\n")
434 f.close()
435 warning("Request to invalidate %d paths (max 999 supported)" % len(paths))
436 warning("All the paths are now saved in: %s" % tmp_filename)
437 except:
438 pass
439 raise ParameterError("Too many paths to invalidate")
440 invalbatch = InvalidationBatch(distribution = cfuri.dist_id(), paths = paths)
441 debug("InvalidateObjects(): request_body: %s" % invalbatch)
442 response = self.send_request("Invalidate", dist_id = cfuri.dist_id(),
443 body = str(invalbatch))
444 response['dist_id'] = cfuri.dist_id()
445 if response['status'] == 201:
446 inval_info = Invalidation(response['data']).info
447 response['request_id'] = inval_info['Id']
448 debug("InvalidateObjects(): response: %s" % response)
449 return response
450
451 def GetInvalList(self, cfuri):
452 if cfuri.type != "cf":
453 raise ValueError("Expected CFUri instead of: %s" % cfuri)
454 response = self.send_request("GetInvalList", dist_id = cfuri.dist_id())
455 response['inval_list'] = InvalidationList(response['data'])
456 return response
457
458 def GetInvalInfo(self, cfuri):
459 if cfuri.type != "cf":
460 raise ValueError("Expected CFUri instead of: %s" % cfuri)
461 if cfuri.request_id() is None:
462 raise ValueError("Expected CFUri with Request ID")
463 response = self.send_request("GetInvalInfo", dist_id = cfuri.dist_id(), request_id = cfuri.request_id())
464 response['inval_status'] = Invalidation(response['data'])
465 return response
466
467 ## --------------------------------------------------
468 ## Low-level methods for handling CloudFront requests
469 ## --------------------------------------------------
470
471 def send_request(self, op_name, dist_id = None, request_id = None, body = None, headers = {}, retries = _max_retries):
472 operation = self.operations[op_name]
473 if body:
474 headers['content-type'] = 'text/plain'
475 request = self.create_request(operation, dist_id, request_id, headers)
476 conn = self.get_connection()
477 debug("send_request(): %s %s" % (request['method'], request['resource']))
478 conn.request(request['method'], request['resource'], body, request['headers'])
479 http_response = conn.getresponse()
480 response = {}
481 response["status"] = http_response.status
482 response["reason"] = http_response.reason
483 response["headers"] = dict(http_response.getheaders())
484 response["data"] = http_response.read()
485 conn.close()
486
487 debug("CloudFront: response: %r" % response)
488
489 if response["status"] >= 500:
490 e = CloudFrontError(response)
491 if retries:
492 warning(u"Retrying failed request: %s" % op_name)
493 warning(unicode(e))
494 warning("Waiting %d sec..." % self._fail_wait(retries))
495 time.sleep(self._fail_wait(retries))
496 return self.send_request(op_name, dist_id, body, retries - 1)
497 else:
498 raise e
499
500 if response["status"] < 200 or response["status"] > 299:
501 raise CloudFrontError(response)
502
503 return response
504
505 def create_request(self, operation, dist_id = None, request_id = None, headers = None):
506 resource = cloudfront_resource + (
507 operation['resource'] % { 'dist_id' : dist_id, 'request_id' : request_id })
508
509 if not headers:
510 headers = {}
511
512 if headers.has_key("date"):
513 if not headers.has_key("x-amz-date"):
514 headers["x-amz-date"] = headers["date"]
515 del(headers["date"])
516
517 if not headers.has_key("x-amz-date"):
518 headers["x-amz-date"] = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime())
519
520 signature = self.sign_request(headers)
521 headers["Authorization"] = "AWS "+self.config.access_key+":"+signature
522
523 request = {}
524 request['resource'] = resource
525 request['headers'] = headers
526 request['method'] = operation['method']
527
528 return request
529
530 def sign_request(self, headers):
531 string_to_sign = headers['x-amz-date']
532 signature = sign_string(string_to_sign)
533 debug(u"CloudFront.sign_request('%s') = %s" % (string_to_sign, signature))
534 return signature
535
536 def get_connection(self):
537 if self.config.proxy_host != "":
538 raise ParameterError("CloudFront commands don't work from behind a HTTP proxy")
539 return httplib.HTTPSConnection(self.config.cloudfront_host)
540
541 def _fail_wait(self, retries):
542 # Wait a few seconds. The more it fails the more we wait.
543 return (self._max_retries - retries + 1) * 3
544
545 def get_dist_name_for_bucket(self, uri):
546 if (uri.type == "cf"):
547 return uri
548 if (uri.type != "s3"):
549 raise ParameterError("CloudFront or S3 URI required instead of: %s" % arg)
550
551 debug("_get_dist_name_for_bucket(%r)" % uri)
552 if CloudFront.dist_list is None:
553 response = self.GetList()
554 CloudFront.dist_list = {}
555 for d in response['dist_list'].dist_summs:
0479b57 @mludvig Ignore CF distros with CustomOrigin in [sync --cf-inval]
mludvig authored
556 if d.info.has_key("S3Origin"):
557 CloudFront.dist_list[getBucketFromHostname(d.info['S3Origin']['DNSName'])[0]] = d.uri()
6eacb08 @aral Added S3 static site support for Amazon CloudFront invalidation on sync.
aral authored
558 elif d.info.has_key("CustomOrigin"):
559 # Aral: This used to skip over distributions with CustomOrigin, however, we mustn't
560 # do this since S3 buckets that are set up as websites use custom origins.
561 # Thankfully, the custom origin URLs they use start with the URL of the
562 # S3 bucket. Here, we make use this naming convention to support this use case.
563 distListIndex = getBucketFromHostname(d.info['CustomOrigin']['DNSName'])[0];
564 distListIndex = distListIndex[:len(uri.bucket())]
565 CloudFront.dist_list[distListIndex] = d.uri()
0479b57 @mludvig Ignore CF distros with CustomOrigin in [sync --cf-inval]
mludvig authored
566 else:
6eacb08 @aral Added S3 static site support for Amazon CloudFront invalidation on sync.
aral authored
567 # Aral: I'm not sure when this condition will be reached, but keeping it in there.
0479b57 @mludvig Ignore CF distros with CustomOrigin in [sync --cf-inval]
mludvig authored
568 continue
d439efb @mludvig ATTENTION -- Mega WhiteSpace conversion !!!
mludvig authored
569 debug("dist_list: %s" % CloudFront.dist_list)
570 try:
571 return CloudFront.dist_list[uri.bucket()]
572 except Exception, e:
573 debug(e)
574 raise ParameterError("Unable to translate S3 URI to CloudFront distribution name: %s" % arg)
c6a015b @mludvig * S3/CloudFront.py: Cmd._get_dist_name_for_bucket() moved to
mludvig authored
575
b3488ba @mludvig * S3/CloudFront.py: New module for CloudFront support.
mludvig authored
576 class Cmd(object):
d439efb @mludvig ATTENTION -- Mega WhiteSpace conversion !!!
mludvig authored
577 """
578 Class that implements CloudFront commands
579 """
580
581 class Options(object):
582 cf_cnames_add = []
583 cf_cnames_remove = []
584 cf_comment = None
585 cf_enable = None
586 cf_logging = None
587 cf_default_root_object = None
588
589 def option_list(self):
590 return [opt for opt in dir(self) if opt.startswith("cf_")]
591
592 def update_option(self, option, value):
593 setattr(Cmd.options, option, value)
594
595 options = Options()
596
597 @staticmethod
598 def _parse_args(args):
599 cf = CloudFront(Config())
600 cfuris = []
601 for arg in args:
602 uri = cf.get_dist_name_for_bucket(S3Uri(arg))
603 cfuris.append(uri)
604 return cfuris
605
606 @staticmethod
607 def info(args):
608 cf = CloudFront(Config())
609 if not args:
610 response = cf.GetList()
611 for d in response['dist_list'].dist_summs:
612 if d.info.has_key("S3Origin"):
613 origin = S3UriS3.httpurl_to_s3uri(d.info['S3Origin']['DNSName'])
614 elif d.info.has_key("CustomOrigin"):
615 origin = "http://%s/" % d.info['CustomOrigin']['DNSName']
616 else:
617 origin = "<unknown>"
618 pretty_output("Origin", origin)
619 pretty_output("DistId", d.uri())
620 pretty_output("DomainName", d.info['DomainName'])
621 if d.info.has_key("CNAME"):
622 pretty_output("CNAMEs", ", ".join(d.info['CNAME']))
623 pretty_output("Status", d.info['Status'])
624 pretty_output("Enabled", d.info['Enabled'])
625 output("")
626 else:
627 cfuris = Cmd._parse_args(args)
628 for cfuri in cfuris:
629 response = cf.GetDistInfo(cfuri)
630 d = response['distribution']
631 dc = d.info['DistributionConfig']
632 if dc.info.has_key("S3Origin"):
633 origin = S3UriS3.httpurl_to_s3uri(dc.info['S3Origin']['DNSName'])
634 elif dc.info.has_key("CustomOrigin"):
635 origin = "http://%s/" % dc.info['CustomOrigin']['DNSName']
636 else:
637 origin = "<unknown>"
638 pretty_output("Origin", origin)
639 pretty_output("DistId", d.uri())
640 pretty_output("DomainName", d.info['DomainName'])
641 if dc.info.has_key("CNAME"):
642 pretty_output("CNAMEs", ", ".join(dc.info['CNAME']))
643 pretty_output("Status", d.info['Status'])
644 pretty_output("Comment", dc.info['Comment'])
645 pretty_output("Enabled", dc.info['Enabled'])
646 pretty_output("DfltRootObject", dc.info['DefaultRootObject'])
647 pretty_output("Logging", dc.info['Logging'] or "Disabled")
648 pretty_output("Etag", response['headers']['etag'])
649
650 @staticmethod
651 def create(args):
652 cf = CloudFront(Config())
653 buckets = []
654 for arg in args:
655 uri = S3Uri(arg)
656 if uri.type != "s3":
657 raise ParameterError("Bucket can only be created from a s3:// URI instead of: %s" % arg)
658 if uri.object():
659 raise ParameterError("Use s3:// URI with a bucket name only instead of: %s" % arg)
660 if not uri.is_dns_compatible():
661 raise ParameterError("CloudFront can only handle lowercase-named buckets.")
662 buckets.append(uri)
663 if not buckets:
664 raise ParameterError("No valid bucket names found")
665 for uri in buckets:
666 info("Creating distribution from: %s" % uri)
667 response = cf.CreateDistribution(uri, cnames_add = Cmd.options.cf_cnames_add,
668 comment = Cmd.options.cf_comment,
669 logging = Cmd.options.cf_logging,
678159c @mludvig * s3cmd, S3/CloudFront.py, S3/Config.py: Added support
mludvig authored
670 default_root_object = Cmd.options.cf_default_root_object)
d439efb @mludvig ATTENTION -- Mega WhiteSpace conversion !!!
mludvig authored
671 d = response['distribution']
672 dc = d.info['DistributionConfig']
673 output("Distribution created:")
674 pretty_output("Origin", S3UriS3.httpurl_to_s3uri(dc.info['Origin']))
675 pretty_output("DistId", d.uri())
676 pretty_output("DomainName", d.info['DomainName'])
677 pretty_output("CNAMEs", ", ".join(dc.info['CNAME']))
678 pretty_output("Comment", dc.info['Comment'])
679 pretty_output("Status", d.info['Status'])
680 pretty_output("Enabled", dc.info['Enabled'])
681 pretty_output("DefaultRootObject", dc.info['DefaultRootObject'])
682 pretty_output("Etag", response['headers']['etag'])
683
684 @staticmethod
685 def delete(args):
686 cf = CloudFront(Config())
687 cfuris = Cmd._parse_args(args)
688 for cfuri in cfuris:
689 response = cf.DeleteDistribution(cfuri)
690 if response['status'] >= 400:
691 error("Distribution %s could not be deleted: %s" % (cfuri, response['reason']))
692 output("Distribution %s deleted" % cfuri)
693
694 @staticmethod
695 def modify(args):
696 cf = CloudFront(Config())
697 if len(args) > 1:
698 raise ParameterError("Too many parameters. Modify one Distribution at a time.")
699 try:
700 cfuri = Cmd._parse_args(args)[0]
701 except IndexError, e:
702 raise ParameterError("No valid Distribution URI found.")
703 response = cf.ModifyDistribution(cfuri,
704 cnames_add = Cmd.options.cf_cnames_add,
705 cnames_remove = Cmd.options.cf_cnames_remove,
706 comment = Cmd.options.cf_comment,
707 enabled = Cmd.options.cf_enable,
708 logging = Cmd.options.cf_logging,
678159c @mludvig * s3cmd, S3/CloudFront.py, S3/Config.py: Added support
mludvig authored
709 default_root_object = Cmd.options.cf_default_root_object)
d439efb @mludvig ATTENTION -- Mega WhiteSpace conversion !!!
mludvig authored
710 if response['status'] >= 400:
711 error("Distribution %s could not be modified: %s" % (cfuri, response['reason']))
712 output("Distribution modified: %s" % cfuri)
713 response = cf.GetDistInfo(cfuri)
714 d = response['distribution']
715 dc = d.info['DistributionConfig']
716 pretty_output("Origin", S3UriS3.httpurl_to_s3uri(dc.info['Origin']))
717 pretty_output("DistId", d.uri())
718 pretty_output("DomainName", d.info['DomainName'])
719 pretty_output("Status", d.info['Status'])
720 pretty_output("CNAMEs", ", ".join(dc.info['CNAME']))
721 pretty_output("Comment", dc.info['Comment'])
722 pretty_output("Enabled", dc.info['Enabled'])
723 pretty_output("DefaultRootObject", dc.info['DefaultRootObject'])
724 pretty_output("Etag", response['headers']['etag'])
725
726 @staticmethod
727 def invalinfo(args):
728 cf = CloudFront(Config())
729 cfuris = Cmd._parse_args(args)
730 requests = []
731 for cfuri in cfuris:
732 if cfuri.request_id():
733 requests.append(str(cfuri))
734 else:
735 inval_list = cf.GetInvalList(cfuri)
736 try:
737 for i in inval_list['inval_list'].info['InvalidationSummary']:
738 requests.append("/".join(["cf:/", cfuri.dist_id(), i["Id"]]))
739 except:
740 continue
741 for req in requests:
742 cfuri = S3Uri(req)
743 inval_info = cf.GetInvalInfo(cfuri)
744 st = inval_info['inval_status'].info
745 pretty_output("URI", str(cfuri))
746 pretty_output("Status", st['Status'])
747 pretty_output("Created", st['CreateTime'])
748 pretty_output("Nr of paths", len(st['InvalidationBatch']['Path']))
749 pretty_output("Reference", st['InvalidationBatch']['CallerReference'])
750 output("")
751
752 # vim:et:ts=4:sts=4:ai
Something went wrong with that request. Please try again.