s3_cmd_bin 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. data/.gitignore +17 -0
  2. data/Gemfile +3 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +28 -0
  5. data/Rakefile +1 -0
  6. data/lib/s3_cmd_bin/version.rb +3 -0
  7. data/lib/s3_cmd_bin.rb +15 -0
  8. data/resources/ChangeLog +1462 -0
  9. data/resources/INSTALL +97 -0
  10. data/resources/LICENSE +339 -0
  11. data/resources/MANIFEST.in +2 -0
  12. data/resources/Makefile +4 -0
  13. data/resources/NEWS +234 -0
  14. data/resources/README +342 -0
  15. data/resources/S3/ACL.py +224 -0
  16. data/resources/S3/ACL.pyc +0 -0
  17. data/resources/S3/AccessLog.py +92 -0
  18. data/resources/S3/AccessLog.pyc +0 -0
  19. data/resources/S3/BidirMap.py +42 -0
  20. data/resources/S3/BidirMap.pyc +0 -0
  21. data/resources/S3/CloudFront.py +773 -0
  22. data/resources/S3/CloudFront.pyc +0 -0
  23. data/resources/S3/Config.py +294 -0
  24. data/resources/S3/Config.pyc +0 -0
  25. data/resources/S3/ConnMan.py +71 -0
  26. data/resources/S3/ConnMan.pyc +0 -0
  27. data/resources/S3/Exceptions.py +88 -0
  28. data/resources/S3/Exceptions.pyc +0 -0
  29. data/resources/S3/FileDict.py +53 -0
  30. data/resources/S3/FileDict.pyc +0 -0
  31. data/resources/S3/FileLists.py +517 -0
  32. data/resources/S3/FileLists.pyc +0 -0
  33. data/resources/S3/HashCache.py +53 -0
  34. data/resources/S3/HashCache.pyc +0 -0
  35. data/resources/S3/MultiPart.py +137 -0
  36. data/resources/S3/MultiPart.pyc +0 -0
  37. data/resources/S3/PkgInfo.py +14 -0
  38. data/resources/S3/PkgInfo.pyc +0 -0
  39. data/resources/S3/Progress.py +173 -0
  40. data/resources/S3/Progress.pyc +0 -0
  41. data/resources/S3/S3.py +979 -0
  42. data/resources/S3/S3.pyc +0 -0
  43. data/resources/S3/S3Uri.py +223 -0
  44. data/resources/S3/S3Uri.pyc +0 -0
  45. data/resources/S3/SimpleDB.py +178 -0
  46. data/resources/S3/SortedDict.py +66 -0
  47. data/resources/S3/SortedDict.pyc +0 -0
  48. data/resources/S3/Utils.py +462 -0
  49. data/resources/S3/Utils.pyc +0 -0
  50. data/resources/S3/__init__.py +0 -0
  51. data/resources/S3/__init__.pyc +0 -0
  52. data/resources/TODO +52 -0
  53. data/resources/artwork/AtomicClockRadio.ttf +0 -0
  54. data/resources/artwork/TypeRa.ttf +0 -0
  55. data/resources/artwork/site-top-full-size.xcf +0 -0
  56. data/resources/artwork/site-top-label-download.png +0 -0
  57. data/resources/artwork/site-top-label-s3cmd.png +0 -0
  58. data/resources/artwork/site-top-label-s3sync.png +0 -0
  59. data/resources/artwork/site-top-s3tools-logo.png +0 -0
  60. data/resources/artwork/site-top.jpg +0 -0
  61. data/resources/artwork/site-top.png +0 -0
  62. data/resources/artwork/site-top.xcf +0 -0
  63. data/resources/format-manpage.pl +196 -0
  64. data/resources/magic +63 -0
  65. data/resources/run-tests.py +537 -0
  66. data/resources/s3cmd +2116 -0
  67. data/resources/s3cmd.1 +435 -0
  68. data/resources/s3db +55 -0
  69. data/resources/setup.cfg +2 -0
  70. data/resources/setup.py +80 -0
  71. data/resources/testsuite.tar.gz +0 -0
  72. data/resources/upload-to-sf.sh +7 -0
  73. data/s3_cmd_bin.gemspec +23 -0
  74. metadata +152 -0
@@ -0,0 +1,773 @@
1
+ ## Amazon CloudFront support
2
+ ## Author: Michal Ludvig <michal@logix.cz>
3
+ ## http://www.logix.cz/michal
4
+ ## License: GPL Version 2
5
+
6
+ import sys
7
+ import time
8
+ import httplib
9
+ import random
10
+ from datetime import datetime
11
+ from logging import debug, info, warning, error
12
+
13
+ try:
14
+ import xml.etree.ElementTree as ET
15
+ except ImportError:
16
+ import elementtree.ElementTree as ET
17
+
18
+ from Config import Config
19
+ from Exceptions import *
20
+ from Utils import getTreeFromXml, appendXmlTextNode, getDictFromTree, dateS3toPython, sign_string, getBucketFromHostname, getHostnameFromBucket
21
+ from S3Uri import S3Uri, S3UriS3
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 }
26
+
27
+ def output(message):
28
+ sys.stdout.write(message + "\n")
29
+
30
+ def pretty_output(label, message):
31
+ #label = ("%s " % label).ljust(20, ".")
32
+ label = ("%s:" % label).ljust(15)
33
+ output("%s %s" % (label, message))
34
+
35
+ class DistributionSummary(object):
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'])
65
+
66
+ class DistributionList(object):
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))
92
+
93
+ class Distribution(object):
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'])
121
+
122
+ class DistributionConfig(object):
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><S3Origin><DNSName/></S3Origin><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
+ s3org = appendXmlTextNode("S3Origin", '', tree)
178
+ appendXmlTextNode("DNSName", self.info['S3Origin']['DNSName'], s3org)
179
+ appendXmlTextNode("CallerReference", self.info['CallerReference'], tree)
180
+ for cname in self.info['CNAME']:
181
+ appendXmlTextNode("CNAME", cname.lower(), tree)
182
+ if self.info['Comment']:
183
+ appendXmlTextNode("Comment", self.info['Comment'], tree)
184
+ appendXmlTextNode("Enabled", str(self.info['Enabled']).lower(), tree)
185
+ # don't create a empty DefaultRootObject element as it would result in a MalformedXML error
186
+ if str(self.info['DefaultRootObject']):
187
+ appendXmlTextNode("DefaultRootObject", str(self.info['DefaultRootObject']), tree)
188
+ if self.info['Logging']:
189
+ logging_el = ET.Element("Logging")
190
+ appendXmlTextNode("Bucket", getHostnameFromBucket(self.info['Logging'].bucket()), logging_el)
191
+ appendXmlTextNode("Prefix", self.info['Logging'].object(), logging_el)
192
+ tree.append(logging_el)
193
+ return ET.tostring(tree)
194
+
195
+ class Invalidation(object):
196
+ ## Example:
197
+ ##
198
+ ## <Invalidation xmlns="http://cloudfront.amazonaws.com/doc/2010-11-01/">
199
+ ## <Id>id</Id>
200
+ ## <Status>status</Status>
201
+ ## <CreateTime>date</CreateTime>
202
+ ## <InvalidationBatch>
203
+ ## <Path>/image1.jpg</Path>
204
+ ## <Path>/image2.jpg</Path>
205
+ ## <Path>/videos/movie.flv</Path>
206
+ ## <CallerReference>my-batch</CallerReference>
207
+ ## </InvalidationBatch>
208
+ ## </Invalidation>
209
+
210
+ def __init__(self, xml):
211
+ tree = getTreeFromXml(xml)
212
+ if tree.tag != "Invalidation":
213
+ raise ValueError("Expected <Invalidation /> xml, got: <%s />" % tree.tag)
214
+ self.parse(tree)
215
+
216
+ def parse(self, tree):
217
+ self.info = getDictFromTree(tree)
218
+
219
+ def __str__(self):
220
+ return str(self.info)
221
+
222
+ class InvalidationList(object):
223
+ ## Example:
224
+ ##
225
+ ## <InvalidationList>
226
+ ## <Marker/>
227
+ ## <NextMarker>Invalidation ID</NextMarker>
228
+ ## <MaxItems>2</MaxItems>
229
+ ## <IsTruncated>true</IsTruncated>
230
+ ## <InvalidationSummary>
231
+ ## <Id>[Second Invalidation ID]</Id>
232
+ ## <Status>Completed</Status>
233
+ ## </InvalidationSummary>
234
+ ## <InvalidationSummary>
235
+ ## <Id>[First Invalidation ID]</Id>
236
+ ## <Status>Completed</Status>
237
+ ## </InvalidationSummary>
238
+ ## </InvalidationList>
239
+
240
+ def __init__(self, xml):
241
+ tree = getTreeFromXml(xml)
242
+ if tree.tag != "InvalidationList":
243
+ raise ValueError("Expected <InvalidationList /> xml, got: <%s />" % tree.tag)
244
+ self.parse(tree)
245
+
246
+ def parse(self, tree):
247
+ self.info = getDictFromTree(tree)
248
+
249
+ def __str__(self):
250
+ return str(self.info)
251
+
252
+ class InvalidationBatch(object):
253
+ ## Example:
254
+ ##
255
+ ## <InvalidationBatch>
256
+ ## <Path>/image1.jpg</Path>
257
+ ## <Path>/image2.jpg</Path>
258
+ ## <Path>/videos/movie.flv</Path>
259
+ ## <Path>/sound%20track.mp3</Path>
260
+ ## <CallerReference>my-batch</CallerReference>
261
+ ## </InvalidationBatch>
262
+
263
+ def __init__(self, reference = None, distribution = None, paths = []):
264
+ if reference:
265
+ self.reference = reference
266
+ else:
267
+ if not distribution:
268
+ distribution="0"
269
+ self.reference = "%s.%s.%s" % (distribution,
270
+ datetime.strftime(datetime.now(),"%Y%m%d%H%M%S"),
271
+ random.randint(1000,9999))
272
+ self.paths = []
273
+ self.add_objects(paths)
274
+
275
+ def add_objects(self, paths):
276
+ self.paths.extend(paths)
277
+
278
+ def get_reference(self):
279
+ return self.reference
280
+
281
+ def __str__(self):
282
+ tree = ET.Element("InvalidationBatch")
283
+
284
+ for path in self.paths:
285
+ if len(path) < 1 or path[0] != "/":
286
+ path = "/" + path
287
+ appendXmlTextNode("Path", path, tree)
288
+ appendXmlTextNode("CallerReference", self.reference, tree)
289
+ return ET.tostring(tree)
290
+
291
+ class CloudFront(object):
292
+ operations = {
293
+ "CreateDist" : { 'method' : "POST", 'resource' : "" },
294
+ "DeleteDist" : { 'method' : "DELETE", 'resource' : "/%(dist_id)s" },
295
+ "GetList" : { 'method' : "GET", 'resource' : "" },
296
+ "GetDistInfo" : { 'method' : "GET", 'resource' : "/%(dist_id)s" },
297
+ "GetDistConfig" : { 'method' : "GET", 'resource' : "/%(dist_id)s/config" },
298
+ "SetDistConfig" : { 'method' : "PUT", 'resource' : "/%(dist_id)s/config" },
299
+ "Invalidate" : { 'method' : "POST", 'resource' : "/%(dist_id)s/invalidation" },
300
+ "GetInvalList" : { 'method' : "GET", 'resource' : "/%(dist_id)s/invalidation" },
301
+ "GetInvalInfo" : { 'method' : "GET", 'resource' : "/%(dist_id)s/invalidation/%(request_id)s" },
302
+ }
303
+
304
+ ## Maximum attempts of re-issuing failed requests
305
+ _max_retries = 5
306
+ dist_list = None
307
+
308
+ def __init__(self, config):
309
+ self.config = config
310
+
311
+ ## --------------------------------------------------
312
+ ## Methods implementing CloudFront API
313
+ ## --------------------------------------------------
314
+
315
+ def GetList(self):
316
+ response = self.send_request("GetList")
317
+ response['dist_list'] = DistributionList(response['data'])
318
+ if response['dist_list'].info['IsTruncated']:
319
+ raise NotImplementedError("List is truncated. Ask s3cmd author to add support.")
320
+ ## TODO: handle Truncated
321
+ return response
322
+
323
+ def CreateDistribution(self, uri, cnames_add = [], comment = None, logging = None, default_root_object = None):
324
+ dist_config = DistributionConfig()
325
+ dist_config.info['Enabled'] = True
326
+ dist_config.info['S3Origin']['DNSName'] = uri.host_name()
327
+ dist_config.info['CallerReference'] = str(uri)
328
+ dist_config.info['DefaultRootObject'] = default_root_object
329
+ if comment == None:
330
+ dist_config.info['Comment'] = uri.public_url()
331
+ else:
332
+ dist_config.info['Comment'] = comment
333
+ for cname in cnames_add:
334
+ if dist_config.info['CNAME'].count(cname) == 0:
335
+ dist_config.info['CNAME'].append(cname)
336
+ if logging:
337
+ dist_config.info['Logging'] = S3UriS3(logging)
338
+ request_body = str(dist_config)
339
+ debug("CreateDistribution(): request_body: %s" % request_body)
340
+ response = self.send_request("CreateDist", body = request_body)
341
+ response['distribution'] = Distribution(response['data'])
342
+ return response
343
+
344
+ def ModifyDistribution(self, cfuri, cnames_add = [], cnames_remove = [],
345
+ comment = None, enabled = None, logging = None,
346
+ default_root_object = None):
347
+ if cfuri.type != "cf":
348
+ raise ValueError("Expected CFUri instead of: %s" % cfuri)
349
+ # Get current dist status (enabled/disabled) and Etag
350
+ info("Checking current status of %s" % cfuri)
351
+ response = self.GetDistConfig(cfuri)
352
+ dc = response['dist_config']
353
+ if enabled != None:
354
+ dc.info['Enabled'] = enabled
355
+ if comment != None:
356
+ dc.info['Comment'] = comment
357
+ if default_root_object != None:
358
+ dc.info['DefaultRootObject'] = default_root_object
359
+ for cname in cnames_add:
360
+ if dc.info['CNAME'].count(cname) == 0:
361
+ dc.info['CNAME'].append(cname)
362
+ for cname in cnames_remove:
363
+ while dc.info['CNAME'].count(cname) > 0:
364
+ dc.info['CNAME'].remove(cname)
365
+ if logging != None:
366
+ if logging == False:
367
+ dc.info['Logging'] = False
368
+ else:
369
+ dc.info['Logging'] = S3UriS3(logging)
370
+ response = self.SetDistConfig(cfuri, dc, response['headers']['etag'])
371
+ return response
372
+
373
+ def DeleteDistribution(self, cfuri):
374
+ if cfuri.type != "cf":
375
+ raise ValueError("Expected CFUri instead of: %s" % cfuri)
376
+ # Get current dist status (enabled/disabled) and Etag
377
+ info("Checking current status of %s" % cfuri)
378
+ response = self.GetDistConfig(cfuri)
379
+ if response['dist_config'].info['Enabled']:
380
+ info("Distribution is ENABLED. Disabling first.")
381
+ response['dist_config'].info['Enabled'] = False
382
+ response = self.SetDistConfig(cfuri, response['dist_config'],
383
+ response['headers']['etag'])
384
+ warning("Waiting for Distribution to become disabled.")
385
+ warning("This may take several minutes, please wait.")
386
+ while True:
387
+ response = self.GetDistInfo(cfuri)
388
+ d = response['distribution']
389
+ if d.info['Status'] == "Deployed" and d.info['Enabled'] == False:
390
+ info("Distribution is now disabled")
391
+ break
392
+ warning("Still waiting...")
393
+ time.sleep(10)
394
+ headers = {}
395
+ headers['if-match'] = response['headers']['etag']
396
+ response = self.send_request("DeleteDist", dist_id = cfuri.dist_id(),
397
+ headers = headers)
398
+ return response
399
+
400
+ def GetDistInfo(self, cfuri):
401
+ if cfuri.type != "cf":
402
+ raise ValueError("Expected CFUri instead of: %s" % cfuri)
403
+ response = self.send_request("GetDistInfo", dist_id = cfuri.dist_id())
404
+ response['distribution'] = Distribution(response['data'])
405
+ return response
406
+
407
+ def GetDistConfig(self, cfuri):
408
+ if cfuri.type != "cf":
409
+ raise ValueError("Expected CFUri instead of: %s" % cfuri)
410
+ response = self.send_request("GetDistConfig", dist_id = cfuri.dist_id())
411
+ response['dist_config'] = DistributionConfig(response['data'])
412
+ return response
413
+
414
+ def SetDistConfig(self, cfuri, dist_config, etag = None):
415
+ if etag == None:
416
+ debug("SetDistConfig(): Etag not set. Fetching it first.")
417
+ etag = self.GetDistConfig(cfuri)['headers']['etag']
418
+ debug("SetDistConfig(): Etag = %s" % etag)
419
+ request_body = str(dist_config)
420
+ debug("SetDistConfig(): request_body: %s" % request_body)
421
+ headers = {}
422
+ headers['if-match'] = etag
423
+ response = self.send_request("SetDistConfig", dist_id = cfuri.dist_id(),
424
+ body = request_body, headers = headers)
425
+ return response
426
+
427
+ def InvalidateObjects(self, uri, paths, default_index_file, invalidate_default_index_on_cf, invalidate_default_index_root_on_cf):
428
+ # joseprio: if the user doesn't want to invalidate the default index
429
+ # path, or if the user wants to invalidate the root of the default
430
+ # index, we need to process those paths
431
+ if default_index_file is not None and (not invalidate_default_index_on_cf or invalidate_default_index_root_on_cf):
432
+ new_paths = []
433
+ default_index_suffix = '/' + default_index_file
434
+ for path in paths:
435
+ if path.endswith(default_index_suffix) or path == default_index_file:
436
+ if invalidate_default_index_on_cf:
437
+ new_paths.append(path)
438
+ if invalidate_default_index_root_on_cf:
439
+ new_paths.append(path[:-len(default_index_file)])
440
+ else:
441
+ new_paths.append(path)
442
+ paths = new_paths
443
+
444
+ # uri could be either cf:// or s3:// uri
445
+ cfuri = self.get_dist_name_for_bucket(uri)
446
+ if len(paths) > 999:
447
+ try:
448
+ tmp_filename = Utils.mktmpfile()
449
+ f = open(tmp_filename, "w")
450
+ f.write("\n".join(paths)+"\n")
451
+ f.close()
452
+ warning("Request to invalidate %d paths (max 999 supported)" % len(paths))
453
+ warning("All the paths are now saved in: %s" % tmp_filename)
454
+ except:
455
+ pass
456
+ raise ParameterError("Too many paths to invalidate")
457
+ invalbatch = InvalidationBatch(distribution = cfuri.dist_id(), paths = paths)
458
+ debug("InvalidateObjects(): request_body: %s" % invalbatch)
459
+ response = self.send_request("Invalidate", dist_id = cfuri.dist_id(),
460
+ body = str(invalbatch))
461
+ response['dist_id'] = cfuri.dist_id()
462
+ if response['status'] == 201:
463
+ inval_info = Invalidation(response['data']).info
464
+ response['request_id'] = inval_info['Id']
465
+ debug("InvalidateObjects(): response: %s" % response)
466
+ return response
467
+
468
+ def GetInvalList(self, cfuri):
469
+ if cfuri.type != "cf":
470
+ raise ValueError("Expected CFUri instead of: %s" % cfuri)
471
+ response = self.send_request("GetInvalList", dist_id = cfuri.dist_id())
472
+ response['inval_list'] = InvalidationList(response['data'])
473
+ return response
474
+
475
+ def GetInvalInfo(self, cfuri):
476
+ if cfuri.type != "cf":
477
+ raise ValueError("Expected CFUri instead of: %s" % cfuri)
478
+ if cfuri.request_id() is None:
479
+ raise ValueError("Expected CFUri with Request ID")
480
+ response = self.send_request("GetInvalInfo", dist_id = cfuri.dist_id(), request_id = cfuri.request_id())
481
+ response['inval_status'] = Invalidation(response['data'])
482
+ return response
483
+
484
+ ## --------------------------------------------------
485
+ ## Low-level methods for handling CloudFront requests
486
+ ## --------------------------------------------------
487
+
488
+ def send_request(self, op_name, dist_id = None, request_id = None, body = None, headers = {}, retries = _max_retries):
489
+ operation = self.operations[op_name]
490
+ if body:
491
+ headers['content-type'] = 'text/plain'
492
+ request = self.create_request(operation, dist_id, request_id, headers)
493
+ conn = self.get_connection()
494
+ debug("send_request(): %s %s" % (request['method'], request['resource']))
495
+ conn.request(request['method'], request['resource'], body, request['headers'])
496
+ http_response = conn.getresponse()
497
+ response = {}
498
+ response["status"] = http_response.status
499
+ response["reason"] = http_response.reason
500
+ response["headers"] = dict(http_response.getheaders())
501
+ response["data"] = http_response.read()
502
+ conn.close()
503
+
504
+ debug("CloudFront: response: %r" % response)
505
+
506
+ if response["status"] >= 500:
507
+ e = CloudFrontError(response)
508
+ if retries:
509
+ warning(u"Retrying failed request: %s" % op_name)
510
+ warning(unicode(e))
511
+ warning("Waiting %d sec..." % self._fail_wait(retries))
512
+ time.sleep(self._fail_wait(retries))
513
+ return self.send_request(op_name, dist_id, body, retries - 1)
514
+ else:
515
+ raise e
516
+
517
+ if response["status"] < 200 or response["status"] > 299:
518
+ raise CloudFrontError(response)
519
+
520
+ return response
521
+
522
+ def create_request(self, operation, dist_id = None, request_id = None, headers = None):
523
+ resource = cloudfront_resource + (
524
+ operation['resource'] % { 'dist_id' : dist_id, 'request_id' : request_id })
525
+
526
+ if not headers:
527
+ headers = {}
528
+
529
+ if headers.has_key("date"):
530
+ if not headers.has_key("x-amz-date"):
531
+ headers["x-amz-date"] = headers["date"]
532
+ del(headers["date"])
533
+
534
+ if not headers.has_key("x-amz-date"):
535
+ headers["x-amz-date"] = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime())
536
+
537
+ if len(self.config.access_token)>0:
538
+ self.config.refresh_role()
539
+ headers['x-amz-security-token']=self.config.access_token
540
+
541
+ signature = self.sign_request(headers)
542
+ headers["Authorization"] = "AWS "+self.config.access_key+":"+signature
543
+
544
+ request = {}
545
+ request['resource'] = resource
546
+ request['headers'] = headers
547
+ request['method'] = operation['method']
548
+
549
+ return request
550
+
551
+ def sign_request(self, headers):
552
+ string_to_sign = headers['x-amz-date']
553
+ signature = sign_string(string_to_sign)
554
+ debug(u"CloudFront.sign_request('%s') = %s" % (string_to_sign, signature))
555
+ return signature
556
+
557
+ def get_connection(self):
558
+ if self.config.proxy_host != "":
559
+ raise ParameterError("CloudFront commands don't work from behind a HTTP proxy")
560
+ return httplib.HTTPSConnection(self.config.cloudfront_host)
561
+
562
+ def _fail_wait(self, retries):
563
+ # Wait a few seconds. The more it fails the more we wait.
564
+ return (self._max_retries - retries + 1) * 3
565
+
566
+ def get_dist_name_for_bucket(self, uri):
567
+ if (uri.type == "cf"):
568
+ return uri
569
+ if (uri.type != "s3"):
570
+ raise ParameterError("CloudFront or S3 URI required instead of: %s" % arg)
571
+
572
+ debug("_get_dist_name_for_bucket(%r)" % uri)
573
+ if CloudFront.dist_list is None:
574
+ response = self.GetList()
575
+ CloudFront.dist_list = {}
576
+ for d in response['dist_list'].dist_summs:
577
+ if d.info.has_key("S3Origin"):
578
+ CloudFront.dist_list[getBucketFromHostname(d.info['S3Origin']['DNSName'])[0]] = d.uri()
579
+ elif d.info.has_key("CustomOrigin"):
580
+ # Aral: This used to skip over distributions with CustomOrigin, however, we mustn't
581
+ # do this since S3 buckets that are set up as websites use custom origins.
582
+ # Thankfully, the custom origin URLs they use start with the URL of the
583
+ # S3 bucket. Here, we make use this naming convention to support this use case.
584
+ distListIndex = getBucketFromHostname(d.info['CustomOrigin']['DNSName'])[0];
585
+ distListIndex = distListIndex[:len(uri.bucket())]
586
+ CloudFront.dist_list[distListIndex] = d.uri()
587
+ else:
588
+ # Aral: I'm not sure when this condition will be reached, but keeping it in there.
589
+ continue
590
+ debug("dist_list: %s" % CloudFront.dist_list)
591
+ try:
592
+ return CloudFront.dist_list[uri.bucket()]
593
+ except Exception, e:
594
+ debug(e)
595
+ raise ParameterError("Unable to translate S3 URI to CloudFront distribution name: %s" % arg)
596
+
597
+ class Cmd(object):
598
+ """
599
+ Class that implements CloudFront commands
600
+ """
601
+
602
+ class Options(object):
603
+ cf_cnames_add = []
604
+ cf_cnames_remove = []
605
+ cf_comment = None
606
+ cf_enable = None
607
+ cf_logging = None
608
+ cf_default_root_object = None
609
+
610
+ def option_list(self):
611
+ return [opt for opt in dir(self) if opt.startswith("cf_")]
612
+
613
+ def update_option(self, option, value):
614
+ setattr(Cmd.options, option, value)
615
+
616
+ options = Options()
617
+
618
+ @staticmethod
619
+ def _parse_args(args):
620
+ cf = CloudFront(Config())
621
+ cfuris = []
622
+ for arg in args:
623
+ uri = cf.get_dist_name_for_bucket(S3Uri(arg))
624
+ cfuris.append(uri)
625
+ return cfuris
626
+
627
+ @staticmethod
628
+ def info(args):
629
+ cf = CloudFront(Config())
630
+ if not args:
631
+ response = cf.GetList()
632
+ for d in response['dist_list'].dist_summs:
633
+ if d.info.has_key("S3Origin"):
634
+ origin = S3UriS3.httpurl_to_s3uri(d.info['S3Origin']['DNSName'])
635
+ elif d.info.has_key("CustomOrigin"):
636
+ origin = "http://%s/" % d.info['CustomOrigin']['DNSName']
637
+ else:
638
+ origin = "<unknown>"
639
+ pretty_output("Origin", origin)
640
+ pretty_output("DistId", d.uri())
641
+ pretty_output("DomainName", d.info['DomainName'])
642
+ if d.info.has_key("CNAME"):
643
+ pretty_output("CNAMEs", ", ".join(d.info['CNAME']))
644
+ pretty_output("Status", d.info['Status'])
645
+ pretty_output("Enabled", d.info['Enabled'])
646
+ output("")
647
+ else:
648
+ cfuris = Cmd._parse_args(args)
649
+ for cfuri in cfuris:
650
+ response = cf.GetDistInfo(cfuri)
651
+ d = response['distribution']
652
+ dc = d.info['DistributionConfig']
653
+ if dc.info.has_key("S3Origin"):
654
+ origin = S3UriS3.httpurl_to_s3uri(dc.info['S3Origin']['DNSName'])
655
+ elif dc.info.has_key("CustomOrigin"):
656
+ origin = "http://%s/" % dc.info['CustomOrigin']['DNSName']
657
+ else:
658
+ origin = "<unknown>"
659
+ pretty_output("Origin", origin)
660
+ pretty_output("DistId", d.uri())
661
+ pretty_output("DomainName", d.info['DomainName'])
662
+ if dc.info.has_key("CNAME"):
663
+ pretty_output("CNAMEs", ", ".join(dc.info['CNAME']))
664
+ pretty_output("Status", d.info['Status'])
665
+ pretty_output("Comment", dc.info['Comment'])
666
+ pretty_output("Enabled", dc.info['Enabled'])
667
+ pretty_output("DfltRootObject", dc.info['DefaultRootObject'])
668
+ pretty_output("Logging", dc.info['Logging'] or "Disabled")
669
+ pretty_output("Etag", response['headers']['etag'])
670
+
671
+ @staticmethod
672
+ def create(args):
673
+ cf = CloudFront(Config())
674
+ buckets = []
675
+ for arg in args:
676
+ uri = S3Uri(arg)
677
+ if uri.type != "s3":
678
+ raise ParameterError("Bucket can only be created from a s3:// URI instead of: %s" % arg)
679
+ if uri.object():
680
+ raise ParameterError("Use s3:// URI with a bucket name only instead of: %s" % arg)
681
+ if not uri.is_dns_compatible():
682
+ raise ParameterError("CloudFront can only handle lowercase-named buckets.")
683
+ buckets.append(uri)
684
+ if not buckets:
685
+ raise ParameterError("No valid bucket names found")
686
+ for uri in buckets:
687
+ info("Creating distribution from: %s" % uri)
688
+ response = cf.CreateDistribution(uri, cnames_add = Cmd.options.cf_cnames_add,
689
+ comment = Cmd.options.cf_comment,
690
+ logging = Cmd.options.cf_logging,
691
+ default_root_object = Cmd.options.cf_default_root_object)
692
+ d = response['distribution']
693
+ dc = d.info['DistributionConfig']
694
+ output("Distribution created:")
695
+ pretty_output("Origin", S3UriS3.httpurl_to_s3uri(dc.info['S3Origin']['DNSName']))
696
+ pretty_output("DistId", d.uri())
697
+ pretty_output("DomainName", d.info['DomainName'])
698
+ pretty_output("CNAMEs", ", ".join(dc.info['CNAME']))
699
+ pretty_output("Comment", dc.info['Comment'])
700
+ pretty_output("Status", d.info['Status'])
701
+ pretty_output("Enabled", dc.info['Enabled'])
702
+ pretty_output("DefaultRootObject", dc.info['DefaultRootObject'])
703
+ pretty_output("Etag", response['headers']['etag'])
704
+
705
+ @staticmethod
706
+ def delete(args):
707
+ cf = CloudFront(Config())
708
+ cfuris = Cmd._parse_args(args)
709
+ for cfuri in cfuris:
710
+ response = cf.DeleteDistribution(cfuri)
711
+ if response['status'] >= 400:
712
+ error("Distribution %s could not be deleted: %s" % (cfuri, response['reason']))
713
+ output("Distribution %s deleted" % cfuri)
714
+
715
+ @staticmethod
716
+ def modify(args):
717
+ cf = CloudFront(Config())
718
+ if len(args) > 1:
719
+ raise ParameterError("Too many parameters. Modify one Distribution at a time.")
720
+ try:
721
+ cfuri = Cmd._parse_args(args)[0]
722
+ except IndexError, e:
723
+ raise ParameterError("No valid Distribution URI found.")
724
+ response = cf.ModifyDistribution(cfuri,
725
+ cnames_add = Cmd.options.cf_cnames_add,
726
+ cnames_remove = Cmd.options.cf_cnames_remove,
727
+ comment = Cmd.options.cf_comment,
728
+ enabled = Cmd.options.cf_enable,
729
+ logging = Cmd.options.cf_logging,
730
+ default_root_object = Cmd.options.cf_default_root_object)
731
+ if response['status'] >= 400:
732
+ error("Distribution %s could not be modified: %s" % (cfuri, response['reason']))
733
+ output("Distribution modified: %s" % cfuri)
734
+ response = cf.GetDistInfo(cfuri)
735
+ d = response['distribution']
736
+ dc = d.info['DistributionConfig']
737
+ pretty_output("Origin", S3UriS3.httpurl_to_s3uri(dc.info['S3Origin']['DNSName']))
738
+ pretty_output("DistId", d.uri())
739
+ pretty_output("DomainName", d.info['DomainName'])
740
+ pretty_output("Status", d.info['Status'])
741
+ pretty_output("CNAMEs", ", ".join(dc.info['CNAME']))
742
+ pretty_output("Comment", dc.info['Comment'])
743
+ pretty_output("Enabled", dc.info['Enabled'])
744
+ pretty_output("DefaultRootObject", dc.info['DefaultRootObject'])
745
+ pretty_output("Etag", response['headers']['etag'])
746
+
747
+ @staticmethod
748
+ def invalinfo(args):
749
+ cf = CloudFront(Config())
750
+ cfuris = Cmd._parse_args(args)
751
+ requests = []
752
+ for cfuri in cfuris:
753
+ if cfuri.request_id():
754
+ requests.append(str(cfuri))
755
+ else:
756
+ inval_list = cf.GetInvalList(cfuri)
757
+ try:
758
+ for i in inval_list['inval_list'].info['InvalidationSummary']:
759
+ requests.append("/".join(["cf:/", cfuri.dist_id(), i["Id"]]))
760
+ except:
761
+ continue
762
+ for req in requests:
763
+ cfuri = S3Uri(req)
764
+ inval_info = cf.GetInvalInfo(cfuri)
765
+ st = inval_info['inval_status'].info
766
+ pretty_output("URI", str(cfuri))
767
+ pretty_output("Status", st['Status'])
768
+ pretty_output("Created", st['CreateTime'])
769
+ pretty_output("Nr of paths", len(st['InvalidationBatch']['Path']))
770
+ pretty_output("Reference", st['InvalidationBatch']['CallerReference'])
771
+ output("")
772
+
773
+ # vim:et:ts=4:sts=4:ai