git-svn-id: https://s3tools.svn.sourceforge.net/svnroot/s3tools/s3cmd/branches/s3cmd-airlock@342 830e0280-6d2a-0410-9c65-932aecc39d9d
Michal Ludvig authored on 2009/01/16 22:31:06... | ... |
@@ -1,5 +1,11 @@ |
1 | 1 |
2009-01-17 Michal Ludvig <michal@logix.cz> |
2 | 2 |
|
3 |
+ * S3/CloudFront.py: Initial support for creating new Distros, |
|
4 |
+ implemented parser for ListAllDists response. |
|
5 |
+ * s3cmd: enabled 'cfcreate' command. |
|
6 |
+ |
|
7 |
+2009-01-17 Michal Ludvig <michal@logix.cz> |
|
8 |
+ |
|
3 | 9 |
* S3/Utils.py: Added getDictFromTree() and appendXmlTextNode() |
4 | 10 |
* S3/S3Uri.py: Added some convenience methods to S3UriS3() |
5 | 11 |
|
... | ... |
@@ -3,6 +3,7 @@ |
3 | 3 |
## http://www.logix.cz/michal |
4 | 4 |
## License: GPL Version 2 |
5 | 5 |
|
6 |
+import sys |
|
6 | 7 |
import base64 |
7 | 8 |
import time |
8 | 9 |
import httplib |
... | ... |
@@ -15,21 +16,150 @@ except ImportError: |
15 | 15 |
import sha as sha1 |
16 | 16 |
import hmac |
17 | 17 |
|
18 |
-from Config import Config |
|
19 |
-from Exceptions import * |
|
20 |
- |
|
21 | 18 |
try: |
22 | 19 |
import xml.etree.ElementTree as ET |
23 | 20 |
except ImportError: |
24 | 21 |
import elementtree.ElementTree as ET |
25 | 22 |
|
23 |
+from Config import Config |
|
24 |
+from Exceptions import * |
|
25 |
+from Utils import getTreeFromXml, appendXmlTextNode, getDictFromTree, dateS3toPython |
|
26 |
+from S3Uri import S3Uri |
|
27 |
+ |
|
28 |
+def output(message): |
|
29 |
+ sys.stdout.write(message + "\n") |
|
30 |
+ |
|
31 |
+def pretty_output(label, message): |
|
32 |
+ #label = ("%s " % label).ljust(20, ".") |
|
33 |
+ label = ("%s:" % label).ljust(15) |
|
34 |
+ output("%s %s" % (label, message)) |
|
35 |
+ |
|
36 |
+class DistributionSummary(object): |
|
37 |
+ ## Example: |
|
38 |
+ ## |
|
39 |
+ ## <DistributionSummary> |
|
40 |
+ ## <Id>1234567890ABC</Id> |
|
41 |
+ ## <Status>Deployed</Status> |
|
42 |
+ ## <LastModifiedTime>2009-01-16T11:49:02.189Z</LastModifiedTime> |
|
43 |
+ ## <DomainName>blahblahblah.cloudfront.net</DomainName> |
|
44 |
+ ## <Origin>example.bucket.s3.amazonaws.com</Origin> |
|
45 |
+ ## <Enabled>true</Enabled> |
|
46 |
+ ## </DistributionSummary> |
|
47 |
+ |
|
48 |
+ def __init__(self, tree): |
|
49 |
+ if tree.tag != "DistributionSummary": |
|
50 |
+ raise ValueError("Expected <DistributionSummary /> xml, got: <%s />" % tree.tag) |
|
51 |
+ self.parse(tree) |
|
52 |
+ |
|
53 |
+ def parse(self, tree): |
|
54 |
+ self.info = getDictFromTree(tree) |
|
55 |
+ self.info['Enabled'] = (self.info['Enabled'].lower() == "true") |
|
56 |
+ |
|
57 |
+class DistributionList(object): |
|
58 |
+ ## Example: |
|
59 |
+ ## |
|
60 |
+ ## <DistributionList xmlns="http://cloudfront.amazonaws.com/doc/2008-06-30/"> |
|
61 |
+ ## <Marker /> |
|
62 |
+ ## <MaxItems>100</MaxItems> |
|
63 |
+ ## <IsTruncated>false</IsTruncated> |
|
64 |
+ ## <DistributionSummary> |
|
65 |
+ ## ... handled by DistributionSummary() class ... |
|
66 |
+ ## </DistributionSummary> |
|
67 |
+ ## </DistributionList> |
|
68 |
+ |
|
69 |
+ def __init__(self, xml): |
|
70 |
+ tree = getTreeFromXml(xml) |
|
71 |
+ if tree.tag != "DistributionList": |
|
72 |
+ raise ValueError("Expected <DistributionList /> xml, got: <%s />" % tree.tag) |
|
73 |
+ self.parse(tree) |
|
74 |
+ |
|
75 |
+ def parse(self, tree): |
|
76 |
+ self.info = getDictFromTree(tree) |
|
77 |
+ ## Normalise some items |
|
78 |
+ self.info['IsTruncated'] = (self.info['IsTruncated'].lower() == "true") |
|
79 |
+ |
|
80 |
+ self.dist_summs = [] |
|
81 |
+ for dist_summ in tree.findall(".//DistributionSummary"): |
|
82 |
+ self.dist_summs.append(DistributionSummary(dist_summ)) |
|
83 |
+ |
|
26 | 84 |
class Distribution(object): |
27 |
- pass |
|
85 |
+ ## Example: |
|
86 |
+ ## |
|
87 |
+ ## <Distribution xmlns="http://cloudfront.amazonaws.com/doc/2008-06-30/"> |
|
88 |
+ ## <Id>1234567890ABC</Id> |
|
89 |
+ ## <Status>InProgress</Status> |
|
90 |
+ ## <LastModifiedTime>2009-01-16T13:07:11.319Z</LastModifiedTime> |
|
91 |
+ ## <DomainName>blahblahblah.cloudfront.net</DomainName> |
|
92 |
+ ## <DistributionConfig> |
|
93 |
+ ## ... handled by DistributionConfig() class ... |
|
94 |
+ ## </DistributionConfig> |
|
95 |
+ ## </Distribution> |
|
96 |
+ |
|
97 |
+ def __init__(self, xml): |
|
98 |
+ tree = getTreeFromXml(xml) |
|
99 |
+ if tree.tag != "Distribution": |
|
100 |
+ raise ValueError("Expected <Distribution /> xml, got: <%s />" % tree.tag) |
|
101 |
+ self.parse(tree) |
|
102 |
+ |
|
103 |
+ def parse(self, tree): |
|
104 |
+ self.info = getDictFromTree(tree) |
|
105 |
+ ## Normalise some items |
|
106 |
+ self.info['LastModifiedTime'] = dateS3toPython(self.info['LastModifiedTime']) |
|
107 |
+ |
|
108 |
+ self.info['DistributionConfig'] = DistributionConfig(tree = tree.find(".//DistributionConfig")) |
|
109 |
+ |
|
110 |
+class DistributionConfig(object): |
|
111 |
+ ## Example: |
|
112 |
+ ## |
|
113 |
+ ## <DistributionConfig> |
|
114 |
+ ## <Origin>somebucket.s3.amazonaws.com</Origin> |
|
115 |
+ ## <CallerReference>s3://somebucket/</CallerReference> |
|
116 |
+ ## <Comment>http://somebucket.s3.amazonaws.com/</Comment> |
|
117 |
+ ## <Enabled>true</Enabled> |
|
118 |
+ ## </DistributionConfig> |
|
119 |
+ |
|
120 |
+ EMPTY_CONFIG = "<DistributionConfig></DistributionConfig>" |
|
121 |
+ xmlns = "http://cloudfront.amazonaws.com/doc/2008-06-30/" |
|
122 |
+ def __init__(self, xml = None, tree = None): |
|
123 |
+ if not xml: |
|
124 |
+ xml = DistributionConfig.EMPTY_CONFIG |
|
125 |
+ |
|
126 |
+ if not tree: |
|
127 |
+ tree = getTreeFromXml(xml) |
|
128 |
+ |
|
129 |
+ if tree.tag != "DistributionConfig": |
|
130 |
+ raise ValueError("Expected <DistributionConfig /> xml, got: <%s />" % tree.tag) |
|
131 |
+ self.parse(tree) |
|
132 |
+ |
|
133 |
+ def parse(self, tree): |
|
134 |
+ self.Origin = tree.findtext(".//Origin") or "" |
|
135 |
+ self.CallerReference = tree.findtext(".//CallerReference") or "" |
|
136 |
+ self.Comment = tree.findtext(".//Comment") or "" |
|
137 |
+ self.Cnames = [] |
|
138 |
+ for cname in tree.findall(".//CNAME"): |
|
139 |
+ self.Cnames.append(cname.text.lower()) |
|
140 |
+ enabled = tree.findtext(".//Enabled") or "" |
|
141 |
+ self.Enabled = (enabled.lower() == "true") |
|
142 |
+ |
|
143 |
+ def __str__(self): |
|
144 |
+ tree = getTreeFromXml(DistributionConfig.EMPTY_CONFIG) |
|
145 |
+ tree.attrib['xmlns'] = DistributionConfig.xmlns |
|
146 |
+ |
|
147 |
+ ## Retain the order of the following calls! |
|
148 |
+ appendXmlTextNode("Origin", self.Origin, tree) |
|
149 |
+ appendXmlTextNode("CallerReference", self.CallerReference, tree) |
|
150 |
+ if self.Comment: |
|
151 |
+ appendXmlTextNode("Comment", self.Comment, tree) |
|
152 |
+ for cname in self.Cnames: |
|
153 |
+ appendXmlTextNode("CNAME", cname.lower(), tree) |
|
154 |
+ appendXmlTextNode("Enabled", str(self.Enabled).lower(), tree) |
|
155 |
+ |
|
156 |
+ return ET.tostring(tree) |
|
28 | 157 |
|
29 | 158 |
class CloudFront(object): |
30 | 159 |
operations = { |
31 |
- "Create" : { 'method' : "PUT", 'resource' : "" }, |
|
32 |
- "Delete" : { 'method' : "DELETE", 'resource' : "/%(dist_id)s" }, |
|
160 |
+ "CreateDist" : { 'method' : "POST", 'resource' : "" }, |
|
161 |
+ "DeleteDist" : { 'method' : "DELETE", 'resource' : "/%(dist_id)s" }, |
|
33 | 162 |
"GetList" : { 'method' : "GET", 'resource' : "" }, |
34 | 163 |
"GetDistInfo" : { 'method' : "GET", 'resource' : "/%(dist_id)s" }, |
35 | 164 |
"GetDistConfig" : { 'method' : "GET", 'resource' : "/%(dist_id)s/config" }, |
... | ... |
@@ -48,6 +178,24 @@ class CloudFront(object): |
48 | 48 |
|
49 | 49 |
def GetList(self): |
50 | 50 |
response = self.send_request("GetList") |
51 |
+ response['dist_list'] = DistributionList(response['data']) |
|
52 |
+ if response['dist_list'].info['IsTruncated']: |
|
53 |
+ raise NotImplementedError("List is truncated. Ask s3cmd author to add support.") |
|
54 |
+ ## TODO: handle Truncated |
|
55 |
+ return response |
|
56 |
+ |
|
57 |
+ def CreateDistribution(self, uri, cnames = []): |
|
58 |
+ dist_conf = DistributionConfig() |
|
59 |
+ dist_conf.Enabled = True |
|
60 |
+ dist_conf.Origin = uri.host_name() |
|
61 |
+ dist_conf.CallerReference = str(uri) |
|
62 |
+ dist_conf.Comment = uri.public_url() |
|
63 |
+ if cnames: |
|
64 |
+ dist_conf.Cnames = cnames |
|
65 |
+ request_body = str(dist_conf) |
|
66 |
+ debug("CreateDistribution(): request_body: %s" % request_body) |
|
67 |
+ response = self.send_request("CreateDist", body = request_body) |
|
68 |
+ response['distribution'] = Distribution(response['data']) |
|
51 | 69 |
return response |
52 | 70 |
|
53 | 71 |
## -------------------------------------------------- |
... | ... |
@@ -56,8 +204,12 @@ class CloudFront(object): |
56 | 56 |
|
57 | 57 |
def send_request(self, op_name, dist_id = None, body = None, retries = _max_retries): |
58 | 58 |
operation = self.operations[op_name] |
59 |
- request = self.create_request(operation, dist_id) |
|
59 |
+ headers = {} |
|
60 |
+ if body: |
|
61 |
+ headers['content-type'] = 'text/plain' |
|
62 |
+ request = self.create_request(operation, dist_id, headers) |
|
60 | 63 |
conn = self.get_connection() |
64 |
+ debug("send_request(): %s %s" % (request['method'], request['resource'])) |
|
61 | 65 |
conn.request(request['method'], request['resource'], body, request['headers']) |
62 | 66 |
http_response = conn.getresponse() |
63 | 67 |
response = {} |
... | ... |
@@ -69,7 +221,7 @@ class CloudFront(object): |
69 | 69 |
|
70 | 70 |
debug("CloudFront: response: %r" % response) |
71 | 71 |
|
72 |
- if response["status"] >= 400: |
|
72 |
+ if response["status"] >= 500: |
|
73 | 73 |
e = CloudFrontError(response) |
74 | 74 |
if retries: |
75 | 75 |
warning(u"Retrying failed request: %s" % op_name) |
... | ... |
@@ -129,8 +281,43 @@ class Cmd(object): |
129 | 129 |
""" |
130 | 130 |
Class that implements CloudFront commands |
131 | 131 |
""" |
132 |
- |
|
132 |
+ |
|
133 | 133 |
@staticmethod |
134 | 134 |
def list(args): |
135 | 135 |
cf = CloudFront(Config()) |
136 | 136 |
response = cf.GetList() |
137 |
+ for d in response['dist_list'].dist_summs: |
|
138 |
+ pretty_output("Origin", d.info['Origin']) |
|
139 |
+ pretty_output("DomainName", d.info['DomainName']) |
|
140 |
+ pretty_output("Id", d.info['Id']) |
|
141 |
+ pretty_output("Status", d.info['Status']) |
|
142 |
+ pretty_output("Enabled", d.info['Enabled']) |
|
143 |
+ output("") |
|
144 |
+ |
|
145 |
+ @staticmethod |
|
146 |
+ def create(args): |
|
147 |
+ cf = CloudFront(Config()) |
|
148 |
+ buckets = [] |
|
149 |
+ for arg in args: |
|
150 |
+ uri = S3Uri(arg) |
|
151 |
+ if uri.type != "s3": |
|
152 |
+ raise ParameterError("Bucket can only be created from a s3:// URI instead of: %s" % arg) |
|
153 |
+ if uri.object(): |
|
154 |
+ raise ParameterError("Use s3:// URI with a bucket name only instead of: %s" % arg) |
|
155 |
+ if not uri.is_dns_compatible(): |
|
156 |
+ raise ParameterError("CloudFront can only handle lowercase-named buckets.") |
|
157 |
+ buckets.append(uri) |
|
158 |
+ if not buckets: |
|
159 |
+ raise ParameterError("No valid bucket names found") |
|
160 |
+ for uri in buckets: |
|
161 |
+ info("Creating distribution from: %s" % uri) |
|
162 |
+ response = cf.CreateDistribution(uri) |
|
163 |
+ d = response['distribution'] |
|
164 |
+ dc = d.info['DistributionConfig'] |
|
165 |
+ output("Distribution created:") |
|
166 |
+ #pretty_output("Origin", dc.info['Origin']) |
|
167 |
+ pretty_output("Origin", dc.Origin) |
|
168 |
+ pretty_output("DomainName", d.info['DomainName']) |
|
169 |
+ pretty_output("Id", d.info['Id']) |
|
170 |
+ pretty_output("Status", d.info['Status']) |
|
171 |
+ pretty_output("Enabled", dc.Enabled) |
... | ... |
@@ -1119,7 +1119,7 @@ def get_commands_list(): |
1119 | 1119 |
{"cmd":"setacl", "label":"Modify Access control list for Bucket or Object", "param":"s3://BUCKET[/OBJECT]", "func":cmd_setacl, "argc":1}, |
1120 | 1120 |
## CloudFront commands |
1121 | 1121 |
{"cmd":"cflist", "label":"List CloudFront distribution points", "param":"", "func":CfCmd.list, "argc":0}, |
1122 |
- #{"cmd":"cfcreate", "label":"Create CloudFront distribution point", "param":"s3://BUCKET", "func":cmd_cf_create, "argc":1}, |
|
1122 |
+ {"cmd":"cfcreate", "label":"Create CloudFront distribution point", "param":"s3://BUCKET", "func":CfCmd.create, "argc":1}, |
|
1123 | 1123 |
#{"cmd":"cfdelete", "label":"Delete CloudFront distribution point", "param":"cf://DIST_ID", "func":cmd_cf_delete, "argc":1}, |
1124 | 1124 |
#{"cmd":"cfinfo", "label":"Display CloudFront distribution point parameters", "param":"cf://DIST_ID", "func":cmd_cf_info, "argc":1}, |
1125 | 1125 |
#{"cmd":"cfmodify", "label":"Change CloudFront distribution point parameters", "param":"cf://DIST_ID", "func":cmd_cf_modify, "argc":1}, |