Browse code

* s3cmd, S3/CloudFront.py, S3/Config.py: Support access logging for CloudFront distributions. * S3/S3.py, S3/Utils.py: Moved some functions to Utils.py to make them available to CloudFront.py * NEWS: Document the above.

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
Showing 7 changed files
... ...
@@ -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
... ...
@@ -1,6 +1,8 @@
1 1
 s3cmd 0.9.9.92 -  ???
2 2
 ==============
3 3
 * Added [accesslog] command. (needs manpage!)
4
+* Added access logging for CloudFront distributions 
5
+  using [cfmodify --log]
4 6
 * Added --acl-grant and --acl-revoke (by Timothee Linden).
5 7
 
6 8
 s3cmd 0.9.9.91 -  2009-10-08
... ...
@@ -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: