s3_cmd_bin 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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