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.
- 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
|