git-svn-id: https://s3tools.svn.sourceforge.net/svnroot/s3tools/s3cmd/trunk@415 830e0280-6d2a-0410-9c65-932aecc39d9d
Michal Ludvig authored on 2010/06/12 20:02:24... | ... |
@@ -1,3 +1,11 @@ |
1 |
+2010-06-12 Michal Ludvig <mludvig@logix.net.nz> |
|
2 |
+ |
|
3 |
+ * s3cmd, S3/CloudFront.py, S3/Config.py: Support access |
|
4 |
+ logging for CloudFront distributions. |
|
5 |
+ * S3/S3.py, S3/Utils.py: Moved some functions to Utils.py |
|
6 |
+ to make them available to CloudFront.py |
|
7 |
+ * NEWS: Document the above. |
|
8 |
+ |
|
1 | 9 |
2010-05-27 Michal Ludvig <mludvig@logix.net.nz> |
2 | 10 |
|
3 | 11 |
* S3/S3.py: Fix bucket listing for buckets with |
... | ... |
@@ -15,7 +15,7 @@ except ImportError: |
15 | 15 |
|
16 | 16 |
from Config import Config |
17 | 17 |
from Exceptions import * |
18 |
-from Utils import getTreeFromXml, appendXmlTextNode, getDictFromTree, dateS3toPython, sign_string |
|
18 |
+from Utils import getTreeFromXml, appendXmlTextNode, getDictFromTree, dateS3toPython, sign_string, getBucketFromHostname, getHostnameFromBucket |
|
19 | 19 |
from S3Uri import S3Uri, S3UriS3 |
20 | 20 |
|
21 | 21 |
def output(message): |
... | ... |
@@ -53,7 +53,7 @@ class DistributionSummary(object): |
53 | 53 |
class DistributionList(object): |
54 | 54 |
## Example: |
55 | 55 |
## |
56 |
- ## <DistributionList xmlns="http://cloudfront.amazonaws.com/doc/2008-06-30/"> |
|
56 |
+ ## <DistributionList xmlns="http://cloudfront.amazonaws.com/doc/2010-06-01/"> |
|
57 | 57 |
## <Marker /> |
58 | 58 |
## <MaxItems>100</MaxItems> |
59 | 59 |
## <IsTruncated>false</IsTruncated> |
... | ... |
@@ -80,7 +80,7 @@ class DistributionList(object): |
80 | 80 |
class Distribution(object): |
81 | 81 |
## Example: |
82 | 82 |
## |
83 |
- ## <Distribution xmlns="http://cloudfront.amazonaws.com/doc/2008-06-30/"> |
|
83 |
+ ## <Distribution xmlns="http://cloudfront.amazonaws.com/doc/2010-06-01/"> |
|
84 | 84 |
## <Id>1234567890ABC</Id> |
85 | 85 |
## <Status>InProgress</Status> |
86 | 86 |
## <LastModifiedTime>2009-01-16T13:07:11.319Z</LastModifiedTime> |
... | ... |
@@ -114,10 +114,14 @@ class DistributionConfig(object): |
114 | 114 |
## <CallerReference>s3://somebucket/</CallerReference> |
115 | 115 |
## <Comment>http://somebucket.s3.amazonaws.com/</Comment> |
116 | 116 |
## <Enabled>true</Enabled> |
117 |
+ ## <Logging> |
|
118 |
+ ## <Bucket>bu.ck.et</Bucket> |
|
119 |
+ ## <Prefix>/cf-somebucket/</Prefix> |
|
120 |
+ ## </Logging> |
|
117 | 121 |
## </DistributionConfig> |
118 | 122 |
|
119 | 123 |
EMPTY_CONFIG = "<DistributionConfig><Origin/><CallerReference/><Enabled>true</Enabled></DistributionConfig>" |
120 |
- xmlns = "http://cloudfront.amazonaws.com/doc/2008-06-30/" |
|
124 |
+ xmlns = "http://cloudfront.amazonaws.com/doc/2010-06-01/" |
|
121 | 125 |
def __init__(self, xml = None, tree = None): |
122 | 126 |
if not xml: |
123 | 127 |
xml = DistributionConfig.EMPTY_CONFIG |
... | ... |
@@ -139,6 +143,16 @@ class DistributionConfig(object): |
139 | 139 |
self.info['CNAME'] = [cname.lower() for cname in self.info['CNAME']] |
140 | 140 |
if not self.info.has_key("Comment"): |
141 | 141 |
self.info['Comment'] = "" |
142 |
+ ## Figure out logging - complex node not parsed by getDictFromTree() |
|
143 |
+ logging_nodes = tree.findall(".//Logging") |
|
144 |
+ if logging_nodes: |
|
145 |
+ logging_dict = getDictFromTree(logging_nodes[0]) |
|
146 |
+ logging_dict['Bucket'], success = getBucketFromHostname(logging_dict['Bucket']) |
|
147 |
+ if not success: |
|
148 |
+ warning("Logging to unparsable bucket name: %s" % logging_dict['Bucket']) |
|
149 |
+ self.info['Logging'] = S3UriS3("s3://%(Bucket)s/%(Prefix)s" % logging_dict) |
|
150 |
+ else: |
|
151 |
+ self.info['Logging'] = None |
|
142 | 152 |
|
143 | 153 |
def __str__(self): |
144 | 154 |
tree = ET.Element("DistributionConfig") |
... | ... |
@@ -152,7 +166,11 @@ class DistributionConfig(object): |
152 | 152 |
if self.info['Comment']: |
153 | 153 |
appendXmlTextNode("Comment", self.info['Comment'], tree) |
154 | 154 |
appendXmlTextNode("Enabled", str(self.info['Enabled']).lower(), tree) |
155 |
- |
|
155 |
+ if self.info['Logging']: |
|
156 |
+ logging_el = ET.Element("Logging") |
|
157 |
+ appendXmlTextNode("Bucket", getHostnameFromBucket(self.info['Logging'].bucket()), logging_el) |
|
158 |
+ appendXmlTextNode("Prefix", self.info['Logging'].object(), logging_el) |
|
159 |
+ tree.append(logging_el) |
|
156 | 160 |
return ET.tostring(tree) |
157 | 161 |
|
158 | 162 |
class CloudFront(object): |
... | ... |
@@ -183,7 +201,7 @@ class CloudFront(object): |
183 | 183 |
## TODO: handle Truncated |
184 | 184 |
return response |
185 | 185 |
|
186 |
- def CreateDistribution(self, uri, cnames_add = [], comment = None): |
|
186 |
+ def CreateDistribution(self, uri, cnames_add = [], comment = None, logging = None): |
|
187 | 187 |
dist_config = DistributionConfig() |
188 | 188 |
dist_config.info['Enabled'] = True |
189 | 189 |
dist_config.info['Origin'] = uri.host_name() |
... | ... |
@@ -195,6 +213,8 @@ class CloudFront(object): |
195 | 195 |
for cname in cnames_add: |
196 | 196 |
if dist_config.info['CNAME'].count(cname) == 0: |
197 | 197 |
dist_config.info['CNAME'].append(cname) |
198 |
+ if logging != None: |
|
199 |
+ dist_config.info['Logging'] = S3UriS3(logging) |
|
198 | 200 |
request_body = str(dist_config) |
199 | 201 |
debug("CreateDistribution(): request_body: %s" % request_body) |
200 | 202 |
response = self.send_request("CreateDist", body = request_body) |
... | ... |
@@ -202,7 +222,7 @@ class CloudFront(object): |
202 | 202 |
return response |
203 | 203 |
|
204 | 204 |
def ModifyDistribution(self, cfuri, cnames_add = [], cnames_remove = [], |
205 |
- comment = None, enabled = None): |
|
205 |
+ comment = None, enabled = None, logging = None): |
|
206 | 206 |
if cfuri.type != "cf": |
207 | 207 |
raise ValueError("Expected CFUri instead of: %s" % cfuri) |
208 | 208 |
# Get current dist status (enabled/disabled) and Etag |
... | ... |
@@ -219,6 +239,8 @@ class CloudFront(object): |
219 | 219 |
for cname in cnames_remove: |
220 | 220 |
while dc.info['CNAME'].count(cname) > 0: |
221 | 221 |
dc.info['CNAME'].remove(cname) |
222 |
+ if logging != None: |
|
223 |
+ dc.info['Logging'] = S3UriS3(logging) |
|
222 | 224 |
response = self.SetDistConfig(cfuri, dc, response['headers']['etag']) |
223 | 225 |
return response |
224 | 226 |
|
... | ... |
@@ -364,6 +386,7 @@ class Cmd(object): |
364 | 364 |
cf_cnames_remove = [] |
365 | 365 |
cf_comment = None |
366 | 366 |
cf_enable = None |
367 |
+ cf_logging = None |
|
367 | 368 |
|
368 | 369 |
def option_list(self): |
369 | 370 |
return [opt for opt in dir(self) if opt.startswith("cf_")] |
... | ... |
@@ -402,6 +425,7 @@ class Cmd(object): |
402 | 402 |
pretty_output("CNAMEs", ", ".join(dc.info['CNAME'])) |
403 | 403 |
pretty_output("Comment", dc.info['Comment']) |
404 | 404 |
pretty_output("Enabled", dc.info['Enabled']) |
405 |
+ pretty_output("Logging", dc.info['Logging'] or "Disabled") |
|
405 | 406 |
pretty_output("Etag", response['headers']['etag']) |
406 | 407 |
|
407 | 408 |
@staticmethod |
... | ... |
@@ -422,7 +446,8 @@ class Cmd(object): |
422 | 422 |
for uri in buckets: |
423 | 423 |
info("Creating distribution from: %s" % uri) |
424 | 424 |
response = cf.CreateDistribution(uri, cnames_add = Cmd.options.cf_cnames_add, |
425 |
- comment = Cmd.options.cf_comment) |
|
425 |
+ comment = Cmd.options.cf_comment, |
|
426 |
+ logging = Cmd.options.cf_logging) |
|
426 | 427 |
d = response['distribution'] |
427 | 428 |
dc = d.info['DistributionConfig'] |
428 | 429 |
output("Distribution created:") |
... | ... |
@@ -462,7 +487,8 @@ class Cmd(object): |
462 | 462 |
cnames_add = Cmd.options.cf_cnames_add, |
463 | 463 |
cnames_remove = Cmd.options.cf_cnames_remove, |
464 | 464 |
comment = Cmd.options.cf_comment, |
465 |
- enabled = Cmd.options.cf_enable) |
|
465 |
+ enabled = Cmd.options.cf_enable, |
|
466 |
+ logging = Cmd.options.cf_logging) |
|
466 | 467 |
if response['status'] >= 400: |
467 | 468 |
error("Distribution %s could not be modified: %s" % (cfuri, response['reason'])) |
468 | 469 |
output("Distribution modified: %s" % cfuri) |
... | ... |
@@ -19,7 +19,7 @@ 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 = "/2008-06-30/distribution" |
|
22 |
+ cloudfront_resource = "/2010-06-01/distribution" |
|
23 | 23 |
verbosity = logging.WARNING |
24 | 24 |
progress_meter = True |
25 | 25 |
progress_class = Progress.ProgressCR |
... | ... |
@@ -135,11 +135,11 @@ class S3(object): |
135 | 135 |
return httplib.HTTPConnection(self.get_hostname(bucket)) |
136 | 136 |
|
137 | 137 |
def get_hostname(self, bucket): |
138 |
- if bucket and self.check_bucket_name_dns_conformity(bucket): |
|
138 |
+ if bucket and check_bucket_name_dns_conformity(bucket): |
|
139 | 139 |
if self.redir_map.has_key(bucket): |
140 | 140 |
host = self.redir_map[bucket] |
141 | 141 |
else: |
142 |
- host = self.config.host_bucket % { 'bucket' : bucket } |
|
142 |
+ host = getHostnameFromBucket(bucket) |
|
143 | 143 |
else: |
144 | 144 |
host = self.config.host_base |
145 | 145 |
debug('get_hostname(%s): %s' % (bucket, host)) |
... | ... |
@@ -149,7 +149,7 @@ class S3(object): |
149 | 149 |
self.redir_map[bucket] = redir_hostname |
150 | 150 |
|
151 | 151 |
def format_uri(self, resource): |
152 |
- if resource['bucket'] and not self.check_bucket_name_dns_conformity(resource['bucket']): |
|
152 |
+ if resource['bucket'] and not check_bucket_name_dns_conformity(resource['bucket']): |
|
153 | 153 |
uri = "/%s%s" % (resource['bucket'], resource['uri']) |
154 | 154 |
else: |
155 | 155 |
uri = resource['uri'] |
... | ... |
@@ -224,9 +224,9 @@ class S3(object): |
224 | 224 |
body += bucket_location |
225 | 225 |
body += "</LocationConstraint></CreateBucketConfiguration>" |
226 | 226 |
debug("bucket_location: " + body) |
227 |
- self.check_bucket_name(bucket, dns_strict = True) |
|
227 |
+ check_bucket_name(bucket, dns_strict = True) |
|
228 | 228 |
else: |
229 |
- self.check_bucket_name(bucket, dns_strict = False) |
|
229 |
+ check_bucket_name(bucket, dns_strict = False) |
|
230 | 230 |
if self.config.acl_public: |
231 | 231 |
headers["x-amz-acl"] = "public-read" |
232 | 232 |
request = self.create_request("BUCKET_CREATE", bucket = bucket, headers = headers) |
... | ... |
@@ -753,39 +753,4 @@ class S3(object): |
753 | 753 |
warning("MD5 signatures do not match: computed=%s, received=%s" % ( |
754 | 754 |
response["md5"], response["headers"]["etag"])) |
755 | 755 |
return response |
756 |
- |
|
757 |
- @staticmethod |
|
758 |
- def check_bucket_name(bucket, dns_strict = True): |
|
759 |
- if dns_strict: |
|
760 |
- invalid = re.search("([^a-z0-9\.-])", bucket) |
|
761 |
- if invalid: |
|
762 |
- raise ParameterError("Bucket name '%s' contains disallowed character '%s'. The only supported ones are: lowercase us-ascii letters (a-z), digits (0-9), dot (.) and hyphen (-)." % (bucket, invalid.groups()[0])) |
|
763 |
- else: |
|
764 |
- invalid = re.search("([^A-Za-z0-9\._-])", bucket) |
|
765 |
- if invalid: |
|
766 |
- raise ParameterError("Bucket name '%s' contains disallowed character '%s'. The only supported ones are: us-ascii letters (a-z, A-Z), digits (0-9), dot (.), hyphen (-) and underscore (_)." % (bucket, invalid.groups()[0])) |
|
767 |
- |
|
768 |
- if len(bucket) < 3: |
|
769 |
- raise ParameterError("Bucket name '%s' is too short (min 3 characters)" % bucket) |
|
770 |
- if len(bucket) > 255: |
|
771 |
- raise ParameterError("Bucket name '%s' is too long (max 255 characters)" % bucket) |
|
772 |
- if dns_strict: |
|
773 |
- if len(bucket) > 63: |
|
774 |
- raise ParameterError("Bucket name '%s' is too long (max 63 characters)" % bucket) |
|
775 |
- if re.search("-\.", bucket): |
|
776 |
- raise ParameterError("Bucket name '%s' must not contain sequence '-.' for DNS compatibility" % bucket) |
|
777 |
- if re.search("\.\.", bucket): |
|
778 |
- raise ParameterError("Bucket name '%s' must not contain sequence '..' for DNS compatibility" % bucket) |
|
779 |
- if not re.search("^[0-9a-z]", bucket): |
|
780 |
- raise ParameterError("Bucket name '%s' must start with a letter or a digit" % bucket) |
|
781 |
- if not re.search("[0-9a-z]$", bucket): |
|
782 |
- raise ParameterError("Bucket name '%s' must end with a letter or a digit" % bucket) |
|
783 |
- return True |
|
784 |
- |
|
785 |
- @staticmethod |
|
786 |
- def check_bucket_name_dns_conformity(bucket): |
|
787 |
- try: |
|
788 |
- return S3.check_bucket_name(bucket, dns_strict = True) |
|
789 |
- except ParameterError: |
|
790 |
- return False |
|
791 | 756 |
__all__.append("S3") |
... | ... |
@@ -119,7 +119,9 @@ def appendXmlTextNode(tag_name, text, parent): |
119 | 119 |
created Node to 'parent' element if given. |
120 | 120 |
Returns the newly created Node. |
121 | 121 |
""" |
122 |
- parent.append(xmlTextNode(tag_name, text)) |
|
122 |
+ el = xmlTextNode(tag_name, text) |
|
123 |
+ parent.append(el) |
|
124 |
+ return el |
|
123 | 125 |
__all__.append("appendXmlTextNode") |
124 | 126 |
|
125 | 127 |
def dateS3toPython(date): |
... | ... |
@@ -317,3 +319,60 @@ def sign_string(string_to_sign): |
317 | 317 |
#debug("signature: %s" % signature) |
318 | 318 |
return signature |
319 | 319 |
__all__.append("sign_string") |
320 |
+ |
|
321 |
+def check_bucket_name(bucket, dns_strict = True): |
|
322 |
+ if dns_strict: |
|
323 |
+ invalid = re.search("([^a-z0-9\.-])", bucket) |
|
324 |
+ if invalid: |
|
325 |
+ raise ParameterError("Bucket name '%s' contains disallowed character '%s'. The only supported ones are: lowercase us-ascii letters (a-z), digits (0-9), dot (.) and hyphen (-)." % (bucket, invalid.groups()[0])) |
|
326 |
+ else: |
|
327 |
+ invalid = re.search("([^A-Za-z0-9\._-])", bucket) |
|
328 |
+ if invalid: |
|
329 |
+ raise ParameterError("Bucket name '%s' contains disallowed character '%s'. The only supported ones are: us-ascii letters (a-z, A-Z), digits (0-9), dot (.), hyphen (-) and underscore (_)." % (bucket, invalid.groups()[0])) |
|
330 |
+ |
|
331 |
+ if len(bucket) < 3: |
|
332 |
+ raise ParameterError("Bucket name '%s' is too short (min 3 characters)" % bucket) |
|
333 |
+ if len(bucket) > 255: |
|
334 |
+ raise ParameterError("Bucket name '%s' is too long (max 255 characters)" % bucket) |
|
335 |
+ if dns_strict: |
|
336 |
+ if len(bucket) > 63: |
|
337 |
+ raise ParameterError("Bucket name '%s' is too long (max 63 characters)" % bucket) |
|
338 |
+ if re.search("-\.", bucket): |
|
339 |
+ raise ParameterError("Bucket name '%s' must not contain sequence '-.' for DNS compatibility" % bucket) |
|
340 |
+ if re.search("\.\.", bucket): |
|
341 |
+ raise ParameterError("Bucket name '%s' must not contain sequence '..' for DNS compatibility" % bucket) |
|
342 |
+ if not re.search("^[0-9a-z]", bucket): |
|
343 |
+ raise ParameterError("Bucket name '%s' must start with a letter or a digit" % bucket) |
|
344 |
+ if not re.search("[0-9a-z]$", bucket): |
|
345 |
+ raise ParameterError("Bucket name '%s' must end with a letter or a digit" % bucket) |
|
346 |
+ return True |
|
347 |
+__all__.append("check_bucket_name") |
|
348 |
+ |
|
349 |
+def check_bucket_name_dns_conformity(bucket): |
|
350 |
+ try: |
|
351 |
+ return check_bucket_name(bucket, dns_strict = True) |
|
352 |
+ except ParameterError: |
|
353 |
+ return False |
|
354 |
+__all__.append("check_bucket_name_dns_conformity") |
|
355 |
+ |
|
356 |
+def getBucketFromHostname(hostname): |
|
357 |
+ """ |
|
358 |
+ bucket, success = getBucketFromHostname(hostname) |
|
359 |
+ |
|
360 |
+ Only works for hostnames derived from bucket names |
|
361 |
+ using Config.host_bucket pattern. |
|
362 |
+ |
|
363 |
+ Returns bucket name and a boolean success flag. |
|
364 |
+ """ |
|
365 |
+ |
|
366 |
+ # Create RE pattern from Config.host_bucket |
|
367 |
+ pattern = Config.Config().host_bucket % { 'bucket' : '(?P<bucket>.*)' } |
|
368 |
+ m = re.match(pattern, hostname) |
|
369 |
+ if not m: |
|
370 |
+ return (hostname, False) |
|
371 |
+ return m.groups()[0], True |
|
372 |
+__all__.append("getBucketFromHostname") |
|
373 |
+ |
|
374 |
+def getHostnameFromBucket(bucket): |
|
375 |
+ return Config.Config().host_bucket % { 'bucket' : bucket } |
|
376 |
+__all__.append("getHostnameFromBucket") |
... | ... |
@@ -1599,7 +1599,7 @@ def main(): |
1599 | 1599 |
optparser.add_option( "--bucket-location", dest="bucket_location", help="Datacentre to create bucket in. As of now the datacenters are: US (default), EU, us-west-1, and ap-southeast-1") |
1600 | 1600 |
optparser.add_option( "--reduced-redundancy", "--rr", dest="reduced_redundancy", action="store_true", help="Store object with 'Reduced redundancy'. Lower per-GB price. [put, cp, mv]") |
1601 | 1601 |
|
1602 |
- optparser.add_option( "--log-target-prefix", dest="log_target_prefix", help="Target prefix for access logs (S3 URI)") |
|
1602 |
+ optparser.add_option( "--log-target-prefix", dest="log_target_prefix", help="Target prefix for access logs (S3 URI) (for [cfmodify] and [accesslog] commands)") |
|
1603 | 1603 |
|
1604 | 1604 |
optparser.add_option("-m", "--mime-type", dest="default_mime_type", type="mimetype", metavar="MIME/TYPE", help="Default MIME-type to be set for objects stored.") |
1605 | 1605 |
optparser.add_option("-M", "--guess-mime-type", dest="guess_mime_type", action="store_true", help="Guess MIME-type of files by their extension. Falls back to default MIME-Type as specified by --mime-type option") |
... | ... |
@@ -1719,6 +1719,9 @@ def main(): |
1719 | 1719 |
## CloudFront's cf_enable and Config's enable share the same --enable switch |
1720 | 1720 |
options.cf_enable = options.enable |
1721 | 1721 |
|
1722 |
+ ## CloudFront's cf_logging and Config's log_target_prefix share the same --log-target-prefix switch |
|
1723 |
+ options.cf_logging = options.log_target_prefix |
|
1724 |
+ |
|
1722 | 1725 |
## Update CloudFront options if some were set |
1723 | 1726 |
for option in CfCmd.options.option_list(): |
1724 | 1727 |
try: |