s3_cmd_bin 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +28 -0
- data/Rakefile +1 -0
- data/lib/s3_cmd_bin/version.rb +3 -0
- data/lib/s3_cmd_bin.rb +15 -0
- data/resources/ChangeLog +1462 -0
- data/resources/INSTALL +97 -0
- data/resources/LICENSE +339 -0
- data/resources/MANIFEST.in +2 -0
- data/resources/Makefile +4 -0
- data/resources/NEWS +234 -0
- data/resources/README +342 -0
- data/resources/S3/ACL.py +224 -0
- data/resources/S3/ACL.pyc +0 -0
- data/resources/S3/AccessLog.py +92 -0
- data/resources/S3/AccessLog.pyc +0 -0
- data/resources/S3/BidirMap.py +42 -0
- data/resources/S3/BidirMap.pyc +0 -0
- data/resources/S3/CloudFront.py +773 -0
- data/resources/S3/CloudFront.pyc +0 -0
- data/resources/S3/Config.py +294 -0
- data/resources/S3/Config.pyc +0 -0
- data/resources/S3/ConnMan.py +71 -0
- data/resources/S3/ConnMan.pyc +0 -0
- data/resources/S3/Exceptions.py +88 -0
- data/resources/S3/Exceptions.pyc +0 -0
- data/resources/S3/FileDict.py +53 -0
- data/resources/S3/FileDict.pyc +0 -0
- data/resources/S3/FileLists.py +517 -0
- data/resources/S3/FileLists.pyc +0 -0
- data/resources/S3/HashCache.py +53 -0
- data/resources/S3/HashCache.pyc +0 -0
- data/resources/S3/MultiPart.py +137 -0
- data/resources/S3/MultiPart.pyc +0 -0
- data/resources/S3/PkgInfo.py +14 -0
- data/resources/S3/PkgInfo.pyc +0 -0
- data/resources/S3/Progress.py +173 -0
- data/resources/S3/Progress.pyc +0 -0
- data/resources/S3/S3.py +979 -0
- data/resources/S3/S3.pyc +0 -0
- data/resources/S3/S3Uri.py +223 -0
- data/resources/S3/S3Uri.pyc +0 -0
- data/resources/S3/SimpleDB.py +178 -0
- data/resources/S3/SortedDict.py +66 -0
- data/resources/S3/SortedDict.pyc +0 -0
- data/resources/S3/Utils.py +462 -0
- data/resources/S3/Utils.pyc +0 -0
- data/resources/S3/__init__.py +0 -0
- data/resources/S3/__init__.pyc +0 -0
- data/resources/TODO +52 -0
- data/resources/artwork/AtomicClockRadio.ttf +0 -0
- data/resources/artwork/TypeRa.ttf +0 -0
- data/resources/artwork/site-top-full-size.xcf +0 -0
- data/resources/artwork/site-top-label-download.png +0 -0
- data/resources/artwork/site-top-label-s3cmd.png +0 -0
- data/resources/artwork/site-top-label-s3sync.png +0 -0
- data/resources/artwork/site-top-s3tools-logo.png +0 -0
- data/resources/artwork/site-top.jpg +0 -0
- data/resources/artwork/site-top.png +0 -0
- data/resources/artwork/site-top.xcf +0 -0
- data/resources/format-manpage.pl +196 -0
- data/resources/magic +63 -0
- data/resources/run-tests.py +537 -0
- data/resources/s3cmd +2116 -0
- data/resources/s3cmd.1 +435 -0
- data/resources/s3db +55 -0
- data/resources/setup.cfg +2 -0
- data/resources/setup.py +80 -0
- data/resources/testsuite.tar.gz +0 -0
- data/resources/upload-to-sf.sh +7 -0
- data/s3_cmd_bin.gemspec +23 -0
- 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
|