* 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() |