* s3cmd, S3/CloudFront.py, S3/Config.py: Support for CloudFront
invalidation using [sync --cf-invalidate] command.
git-svn-id: https://s3tools.svn.sourceforge.net/svnroot/s3tools/s3cmd/trunk@473 830e0280-6d2a-0410-9c65-932aecc39d9d
... | ... |
@@ -6,6 +6,8 @@ |
6 | 6 |
import sys |
7 | 7 |
import time |
8 | 8 |
import httplib |
9 |
+import random |
|
10 |
+from datetime import datetime |
|
9 | 11 |
from logging import debug, info, warning, error |
10 | 12 |
|
11 | 13 |
try: |
... | ... |
@@ -17,6 +19,10 @@ from Config import Config |
17 | 17 |
from Exceptions import * |
18 | 18 |
from Utils import getTreeFromXml, appendXmlTextNode, getDictFromTree, dateS3toPython, sign_string, getBucketFromHostname, getHostnameFromBucket |
19 | 19 |
from S3Uri import S3Uri, S3UriS3 |
20 |
+from FileLists import fetch_remote_list |
|
21 |
+ |
|
22 |
+cloudfront_api_version = "2010-11-01" |
|
23 |
+cloudfront_resource = "/%(api_ver)s/distribution" % { 'api_ver' : cloudfront_api_version } |
|
20 | 24 |
|
21 | 25 |
def output(message): |
22 | 26 |
sys.stdout.write(message + "\n") |
... | ... |
@@ -34,7 +40,12 @@ class DistributionSummary(object): |
34 | 34 |
## <Status>Deployed</Status> |
35 | 35 |
## <LastModifiedTime>2009-01-16T11:49:02.189Z</LastModifiedTime> |
36 | 36 |
## <DomainName>blahblahblah.cloudfront.net</DomainName> |
37 |
- ## <Origin>example.bucket.s3.amazonaws.com</Origin> |
|
37 |
+ ## <S3Origin> |
|
38 |
+ ## <DNSName>example.bucket.s3.amazonaws.com</DNSName> |
|
39 |
+ ## </S3Origin> |
|
40 |
+ ## <CNAME>cdn.example.com</CNAME> |
|
41 |
+ ## <CNAME>img.example.com</CNAME> |
|
42 |
+ ## <Comment>What Ever</Comment> |
|
38 | 43 |
## <Enabled>true</Enabled> |
39 | 44 |
## </DistributionSummary> |
40 | 45 |
|
... | ... |
@@ -46,6 +57,8 @@ class DistributionSummary(object): |
46 | 46 |
def parse(self, tree): |
47 | 47 |
self.info = getDictFromTree(tree) |
48 | 48 |
self.info['Enabled'] = (self.info['Enabled'].lower() == "true") |
49 |
+ if self.info.has_key("CNAME") and type(self.info['CNAME']) != list: |
|
50 |
+ self.info['CNAME'] = [self.info['CNAME']] |
|
49 | 51 |
|
50 | 52 |
def uri(self): |
51 | 53 |
return S3Uri("cf://%s" % self.info['Id']) |
... | ... |
@@ -121,7 +134,7 @@ class DistributionConfig(object): |
121 | 121 |
## </DistributionConfig> |
122 | 122 |
|
123 | 123 |
EMPTY_CONFIG = "<DistributionConfig><Origin/><CallerReference/><Enabled>true</Enabled></DistributionConfig>" |
124 |
- xmlns = "http://cloudfront.amazonaws.com/doc/2010-07-15/" |
|
124 |
+ xmlns = "http://cloudfront.amazonaws.com/doc/%(api_ver)s/" % { 'api_ver' : cloudfront_api_version } |
|
125 | 125 |
def __init__(self, xml = None, tree = None): |
126 | 126 |
if xml is None: |
127 | 127 |
xml = DistributionConfig.EMPTY_CONFIG |
... | ... |
@@ -178,6 +191,45 @@ class DistributionConfig(object): |
178 | 178 |
tree.append(logging_el) |
179 | 179 |
return ET.tostring(tree) |
180 | 180 |
|
181 |
+class InvalidationBatch(object): |
|
182 |
+ ## Example: |
|
183 |
+ ## |
|
184 |
+ ## <InvalidationBatch> |
|
185 |
+ ## <Path>/image1.jpg</Path> |
|
186 |
+ ## <Path>/image2.jpg</Path> |
|
187 |
+ ## <Path>/videos/movie.flv</Path> |
|
188 |
+ ## <Path>/sound%20track.mp3</Path> |
|
189 |
+ ## <CallerReference>my-batch</CallerReference> |
|
190 |
+ ## </InvalidationBatch> |
|
191 |
+ |
|
192 |
+ def __init__(self, reference = None, distribution = None, paths = []): |
|
193 |
+ if reference: |
|
194 |
+ self.reference = reference |
|
195 |
+ else: |
|
196 |
+ if not distribution: |
|
197 |
+ distribution="0" |
|
198 |
+ self.reference = "%s.%s.%s" % (distribution, |
|
199 |
+ datetime.strftime(datetime.now(),"%Y%m%d%H%M%S"), |
|
200 |
+ random.randint(1000,9999)) |
|
201 |
+ self.paths = [] |
|
202 |
+ self.add_objects(paths) |
|
203 |
+ |
|
204 |
+ def add_objects(self, paths): |
|
205 |
+ self.paths.extend(paths) |
|
206 |
+ |
|
207 |
+ def get_reference(self): |
|
208 |
+ return self.reference |
|
209 |
+ |
|
210 |
+ def __str__(self): |
|
211 |
+ tree = ET.Element("InvalidationBatch") |
|
212 |
+ |
|
213 |
+ for path in self.paths: |
|
214 |
+ if path[0] != "/": |
|
215 |
+ path = "/" + path |
|
216 |
+ appendXmlTextNode("Path", path, tree) |
|
217 |
+ appendXmlTextNode("CallerReference", self.reference, tree) |
|
218 |
+ return ET.tostring(tree) |
|
219 |
+ |
|
181 | 220 |
class CloudFront(object): |
182 | 221 |
operations = { |
183 | 222 |
"CreateDist" : { 'method' : "POST", 'resource' : "" }, |
... | ... |
@@ -186,6 +238,9 @@ class CloudFront(object): |
186 | 186 |
"GetDistInfo" : { 'method' : "GET", 'resource' : "/%(dist_id)s" }, |
187 | 187 |
"GetDistConfig" : { 'method' : "GET", 'resource' : "/%(dist_id)s/config" }, |
188 | 188 |
"SetDistConfig" : { 'method' : "PUT", 'resource' : "/%(dist_id)s/config" }, |
189 |
+ "Invalidate" : { 'method' : "POST", 'resource' : "/%(dist_id)s/invalidation" }, |
|
190 |
+ "GetInvalList" : { 'method' : "GET", 'resource' : "/%(dist_id)s/invalidation" }, |
|
191 |
+ "GetInvalStatus" : { 'method' : "GET", 'resource' : "/%(dist_id)s/invalidation/%(invalidation_id)s" }, |
|
189 | 192 |
} |
190 | 193 |
|
191 | 194 |
## Maximum attempts of re-issuing failed requests |
... | ... |
@@ -311,6 +366,27 @@ class CloudFront(object): |
311 | 311 |
body = request_body, headers = headers) |
312 | 312 |
return response |
313 | 313 |
|
314 |
+ def InvalidateObjects(self, uri, paths): |
|
315 |
+ # uri could be either cf:// or s3:// uri |
|
316 |
+ cfuri = self.get_dist_name_for_bucket(uri) |
|
317 |
+ if len(paths) > 999: |
|
318 |
+ try: |
|
319 |
+ tmp_filename = Utils.mktmpfile() |
|
320 |
+ f = open(tmp_filename, "w") |
|
321 |
+ f.write("\n".join(paths)+"\n") |
|
322 |
+ f.close() |
|
323 |
+ warning("Request to invalidate %d paths (max 999 supported)" % len(paths)) |
|
324 |
+ warning("All the paths are now saved in: %s" % tmp_filename) |
|
325 |
+ except: |
|
326 |
+ pass |
|
327 |
+ raise ParameterError("Too many paths to invalidate") |
|
328 |
+ invalbatch = InvalidationBatch(distribution = cfuri.dist_id(), paths = paths) |
|
329 |
+ debug("InvalidateObjects(): request_body: %s" % invalbatch) |
|
330 |
+ response = self.send_request("Invalidate", dist_id = cfuri.dist_id(), |
|
331 |
+ body = str(invalbatch)) |
|
332 |
+ debug("InvalidateObjects(): response: %s" % response) |
|
333 |
+ return response, invalbatch.get_reference() |
|
334 |
+ |
|
314 | 335 |
## -------------------------------------------------- |
315 | 336 |
## Low-level methods for handling CloudFront requests |
316 | 337 |
## -------------------------------------------------- |
... | ... |
@@ -350,7 +426,7 @@ class CloudFront(object): |
350 | 350 |
return response |
351 | 351 |
|
352 | 352 |
def create_request(self, operation, dist_id = None, headers = None): |
353 |
- resource = self.config.cloudfront_resource + ( |
|
353 |
+ resource = cloudfront_resource + ( |
|
354 | 354 |
operation['resource'] % { 'dist_id' : dist_id }) |
355 | 355 |
|
356 | 356 |
if not headers: |
... | ... |
@@ -400,9 +476,13 @@ class CloudFront(object): |
400 | 400 |
response = self.GetList() |
401 | 401 |
CloudFront.dist_list = {} |
402 | 402 |
for d in response['dist_list'].dist_summs: |
403 |
- CloudFront.dist_list[getBucketFromHostname(d.info['Origin'])[0]] = d.uri() |
|
403 |
+ CloudFront.dist_list[getBucketFromHostname(d.info['S3Origin']['DNSName'])[0]] = d.uri() |
|
404 | 404 |
debug("dist_list: %s" % CloudFront.dist_list) |
405 |
- return CloudFront.dist_list[uri.bucket()] |
|
405 |
+ try: |
|
406 |
+ return CloudFront.dist_list[uri.bucket()] |
|
407 |
+ except Exception, e: |
|
408 |
+ debug(e) |
|
409 |
+ raise ParameterError("Unable to translate S3 URI to CloudFront distribution name: %s" % arg) |
|
406 | 410 |
|
407 | 411 |
class Cmd(object): |
408 | 412 |
""" |
... | ... |
@@ -430,11 +510,7 @@ class Cmd(object): |
430 | 430 |
cf = CloudFront(Config()) |
431 | 431 |
cfuris = [] |
432 | 432 |
for arg in args: |
433 |
- try: |
|
434 |
- uri = cf.get_dist_name_for_bucket(S3Uri(arg)) |
|
435 |
- except Exception, e: |
|
436 |
- debug(e) |
|
437 |
- raise ParameterError("Unable to translate S3 URI to CloudFront distribution name: %s" % arg) |
|
433 |
+ uri = cf.get_dist_name_for_bucket(S3Uri(arg)) |
|
438 | 434 |
cfuris.append(uri) |
439 | 435 |
return cfuris |
440 | 436 |
|
... | ... |
@@ -444,9 +520,17 @@ class Cmd(object): |
444 | 444 |
if not args: |
445 | 445 |
response = cf.GetList() |
446 | 446 |
for d in response['dist_list'].dist_summs: |
447 |
- pretty_output("Origin", S3UriS3.httpurl_to_s3uri(d.info['Origin'])) |
|
447 |
+ if d.info.has_key("S3Origin"): |
|
448 |
+ origin = S3UriS3.httpurl_to_s3uri(d.info['S3Origin']['DNSName']) |
|
449 |
+ elif d.info.has_key("CustomOrigin"): |
|
450 |
+ origin = "http://%s/" % d.info['CustomOrigin']['DNSName'] |
|
451 |
+ else: |
|
452 |
+ origin = "<unknown>" |
|
453 |
+ pretty_output("Origin", origin) |
|
448 | 454 |
pretty_output("DistId", d.uri()) |
449 | 455 |
pretty_output("DomainName", d.info['DomainName']) |
456 |
+ if d.info.has_key("CNAME"): |
|
457 |
+ pretty_output("CNAMEs", ", ".join(d.info['CNAME'])) |
|
450 | 458 |
pretty_output("Status", d.info['Status']) |
451 | 459 |
pretty_output("Enabled", d.info['Enabled']) |
452 | 460 |
output("") |
... | ... |
@@ -456,11 +540,18 @@ class Cmd(object): |
456 | 456 |
response = cf.GetDistInfo(cfuri) |
457 | 457 |
d = response['distribution'] |
458 | 458 |
dc = d.info['DistributionConfig'] |
459 |
- pretty_output("Origin", S3UriS3.httpurl_to_s3uri(dc.info['Origin'])) |
|
459 |
+ if dc.info.has_key("S3Origin"): |
|
460 |
+ origin = S3UriS3.httpurl_to_s3uri(dc.info['S3Origin']['DNSName']) |
|
461 |
+ elif dc.info.has_key("CustomOrigin"): |
|
462 |
+ origin = "http://%s/" % dc.info['CustomOrigin']['DNSName'] |
|
463 |
+ else: |
|
464 |
+ origin = "<unknown>" |
|
465 |
+ pretty_output("Origin", origin) |
|
460 | 466 |
pretty_output("DistId", d.uri()) |
461 | 467 |
pretty_output("DomainName", d.info['DomainName']) |
468 |
+ if dc.info.has_key("CNAME"): |
|
469 |
+ pretty_output("CNAMEs", ", ".join(dc.info['CNAME'])) |
|
462 | 470 |
pretty_output("Status", d.info['Status']) |
463 |
- pretty_output("CNAMEs", ", ".join(dc.info['CNAME'])) |
|
464 | 471 |
pretty_output("Comment", dc.info['Comment']) |
465 | 472 |
pretty_output("Enabled", dc.info['Enabled']) |
466 | 473 |
pretty_output("DfltRootObject", dc.info['DefaultRootObject']) |
... | ... |
@@ -542,3 +633,7 @@ class Cmd(object): |
542 | 542 |
pretty_output("Enabled", dc.info['Enabled']) |
543 | 543 |
pretty_output("DefaultRootObject", dc.info['DefaultRootObject']) |
544 | 544 |
pretty_output("Etag", response['headers']['etag']) |
545 |
+ |
|
546 |
+ @staticmethod |
|
547 |
+ def invalidate(args): |
|
548 |
+ cf = CloudFront(Config()) |
... | ... |
@@ -19,7 +19,6 @@ class Config(object): |
19 | 19 |
host_bucket = "%(bucket)s.s3.amazonaws.com" |
20 | 20 |
simpledb_host = "sdb.amazonaws.com" |
21 | 21 |
cloudfront_host = "cloudfront.amazonaws.com" |
22 |
- cloudfront_resource = "/2010-07-15/distribution" |
|
23 | 22 |
verbosity = logging.WARNING |
24 | 23 |
progress_meter = True |
25 | 24 |
progress_class = Progress.ProgressCR |
... | ... |
@@ -76,6 +75,7 @@ class Config(object): |
76 | 76 |
reduced_redundancy = False |
77 | 77 |
follow_symlinks = False |
78 | 78 |
socket_timeout = 10 |
79 |
+ invalidate_on_cf = False |
|
79 | 80 |
|
80 | 81 |
## Creating a singleton |
81 | 82 |
def __new__(self, configfile = None): |
... | ... |
@@ -841,6 +841,7 @@ def cmd_sync_local2remote(args): |
841 | 841 |
s3.object_delete(uri) |
842 | 842 |
output(u"deleted: '%s'" % uri) |
843 | 843 |
|
844 |
+ uploaded_objects_list = [] |
|
844 | 845 |
total_size = 0 |
845 | 846 |
total_elapsed = 0.0 |
846 | 847 |
timestamp_start = time.time() |
... | ... |
@@ -872,6 +873,7 @@ def cmd_sync_local2remote(args): |
872 | 872 |
(item['full_name_unicode'], uri, response["size"], response["elapsed"], |
873 | 873 |
speed_fmt[0], speed_fmt[1], seq_label)) |
874 | 874 |
total_size += response["size"] |
875 |
+ uploaded_objects_list.append(uri.object()) |
|
875 | 876 |
|
876 | 877 |
total_elapsed = time.time() - timestamp_start |
877 | 878 |
total_speed = total_elapsed and total_size/total_elapsed or 0.0 |
... | ... |
@@ -885,6 +887,16 @@ def cmd_sync_local2remote(args): |
885 | 885 |
else: |
886 | 886 |
info(outstr) |
887 | 887 |
|
888 |
+ if cfg.invalidate_on_cf: |
|
889 |
+ if len(uploaded_objects_list) == 0: |
|
890 |
+ info("Nothing to invalidate in CloudFront") |
|
891 |
+ else: |
|
892 |
+ # 'uri' from the last iteration is still valid at this point |
|
893 |
+ cf = CloudFront(cfg) |
|
894 |
+ result, inval_id = cf.InvalidateObjects(uri, uploaded_objects_list) |
|
895 |
+ print result |
|
896 |
+ output("Created invalidation request: %s" % inval_id) |
|
897 |
+ |
|
888 | 898 |
def cmd_sync(args): |
889 | 899 |
if (len(args) < 2): |
890 | 900 |
raise ParameterError("Too few parameters! Expected: %s" % commands['sync']['param']) |
... | ... |
@@ -1438,6 +1450,7 @@ def main(): |
1438 | 1438 |
optparser.add_option( "--no-progress", dest="progress_meter", action="store_false", help="Don't display progress meter (default on non-TTY).") |
1439 | 1439 |
optparser.add_option( "--enable", dest="enable", action="store_true", help="Enable given CloudFront distribution (only for [cfmodify] command)") |
1440 | 1440 |
optparser.add_option( "--disable", dest="enable", action="store_false", help="Enable given CloudFront distribution (only for [cfmodify] command)") |
1441 |
+ optparser.add_option( "--cf-invalidate", dest="invalidate_on_cf", action="store_true", help="Invalidate the uploaded filed in CloudFront. Also see [cfinval] command.") |
|
1441 | 1442 |
optparser.add_option( "--cf-add-cname", dest="cf_cnames_add", action="append", metavar="CNAME", help="Add given CNAME to a CloudFront distribution (only for [cfcreate] and [cfmodify] commands)") |
1442 | 1443 |
optparser.add_option( "--cf-remove-cname", dest="cf_cnames_remove", action="append", metavar="CNAME", help="Remove given CNAME from a CloudFront distribution (only for [cfmodify] command)") |
1443 | 1444 |
optparser.add_option( "--cf-comment", dest="cf_comment", action="store", metavar="COMMENT", help="Set COMMENT for a given CloudFront distribution (only for [cfcreate] and [cfmodify] commands)") |
... | ... |
@@ -1684,6 +1697,7 @@ if __name__ == '__main__': |
1684 | 1684 |
from S3.Utils import * |
1685 | 1685 |
from S3.Progress import Progress |
1686 | 1686 |
from S3.CloudFront import Cmd as CfCmd |
1687 |
+ from S3.CloudFront import CloudFront |
|
1687 | 1688 |
from S3.FileLists import * |
1688 | 1689 |
|
1689 | 1690 |
main() |