1# Copyright (c) 2006-2009 Mitch Garnaat http://garnaat.org/
2#
3# Permission is hereby granted, free of charge, to any person obtaining a
4# copy of this software and associated documentation files (the
5# "Software"), to deal in the Software without restriction, including
6# without limitation the rights to use, copy, modify, merge, publish, dis-
7# tribute, sublicense, and/or sell copies of the Software, and to permit
8# persons to whom the Software is furnished to do so, subject to the fol-
9# lowing conditions:
10#
11# The above copyright notice and this permission notice shall be included
12# in all copies or substantial portions of the Software.
13#
14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
16# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
17# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
18# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20# IN THE SOFTWARE.
21#
22
23import xml.sax
24import time
25import boto
26from boto.connection import AWSAuthConnection
27from boto import handler
28from boto.cloudfront.distribution import Distribution, DistributionSummary, DistributionConfig
29from boto.cloudfront.distribution import StreamingDistribution, StreamingDistributionSummary, StreamingDistributionConfig
30from boto.cloudfront.identity import OriginAccessIdentity
31from boto.cloudfront.identity import OriginAccessIdentitySummary
32from boto.cloudfront.identity import OriginAccessIdentityConfig
33from boto.cloudfront.invalidation import InvalidationBatch, InvalidationSummary, InvalidationListResultSet
34from boto.resultset import ResultSet
35from boto.cloudfront.exception import CloudFrontServerError
36
37
38class CloudFrontConnection(AWSAuthConnection):
39
40    DefaultHost = 'cloudfront.amazonaws.com'
41    Version = '2010-11-01'
42
43    def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
44                 port=None, proxy=None, proxy_port=None,
45                 host=DefaultHost, debug=0, security_token=None,
46                 validate_certs=True, profile_name=None, https_connection_factory=None):
47        super(CloudFrontConnection, self).__init__(host,
48                                   aws_access_key_id, aws_secret_access_key,
49                                   True, port, proxy, proxy_port, debug=debug,
50                                   security_token=security_token,
51                                   validate_certs=validate_certs,
52                                   https_connection_factory=https_connection_factory,
53                                   profile_name=profile_name)
54
55    def get_etag(self, response):
56        response_headers = response.msg
57        for key in response_headers.keys():
58            if key.lower() == 'etag':
59                return response_headers[key]
60        return None
61
62    def _required_auth_capability(self):
63        return ['cloudfront']
64
65    # Generics
66
67    def _get_all_objects(self, resource, tags, result_set_class=None,
68                         result_set_kwargs=None):
69        if not tags:
70            tags = [('DistributionSummary', DistributionSummary)]
71        response = self.make_request('GET', '/%s/%s' % (self.Version,
72                                                        resource))
73        body = response.read()
74        boto.log.debug(body)
75        if response.status >= 300:
76            raise CloudFrontServerError(response.status, response.reason, body)
77        rs_class = result_set_class or ResultSet
78        rs_kwargs = result_set_kwargs or dict()
79        rs = rs_class(tags, **rs_kwargs)
80        h = handler.XmlHandler(rs, self)
81        xml.sax.parseString(body, h)
82        return rs
83
84    def _get_info(self, id, resource, dist_class):
85        uri = '/%s/%s/%s' % (self.Version, resource, id)
86        response = self.make_request('GET', uri)
87        body = response.read()
88        boto.log.debug(body)
89        if response.status >= 300:
90            raise CloudFrontServerError(response.status, response.reason, body)
91        d = dist_class(connection=self)
92        response_headers = response.msg
93        for key in response_headers.keys():
94            if key.lower() == 'etag':
95                d.etag = response_headers[key]
96        h = handler.XmlHandler(d, self)
97        xml.sax.parseString(body, h)
98        return d
99
100    def _get_config(self, id, resource, config_class):
101        uri = '/%s/%s/%s/config' % (self.Version, resource, id)
102        response = self.make_request('GET', uri)
103        body = response.read()
104        boto.log.debug(body)
105        if response.status >= 300:
106            raise CloudFrontServerError(response.status, response.reason, body)
107        d = config_class(connection=self)
108        d.etag = self.get_etag(response)
109        h = handler.XmlHandler(d, self)
110        xml.sax.parseString(body, h)
111        return d
112
113    def _set_config(self, distribution_id, etag, config):
114        if isinstance(config, StreamingDistributionConfig):
115            resource = 'streaming-distribution'
116        else:
117            resource = 'distribution'
118        uri = '/%s/%s/%s/config' % (self.Version, resource, distribution_id)
119        headers = {'If-Match': etag, 'Content-Type': 'text/xml'}
120        response = self.make_request('PUT', uri, headers, config.to_xml())
121        body = response.read()
122        boto.log.debug(body)
123        if response.status != 200:
124            raise CloudFrontServerError(response.status, response.reason, body)
125        return self.get_etag(response)
126
127    def _create_object(self, config, resource, dist_class):
128        response = self.make_request('POST', '/%s/%s' % (self.Version,
129                                                         resource),
130                                     {'Content-Type': 'text/xml'},
131                                     data=config.to_xml())
132        body = response.read()
133        boto.log.debug(body)
134        if response.status == 201:
135            d = dist_class(connection=self)
136            h = handler.XmlHandler(d, self)
137            xml.sax.parseString(body, h)
138            d.etag = self.get_etag(response)
139            return d
140        else:
141            raise CloudFrontServerError(response.status, response.reason, body)
142
143    def _delete_object(self, id, etag, resource):
144        uri = '/%s/%s/%s' % (self.Version, resource, id)
145        response = self.make_request('DELETE', uri, {'If-Match': etag})
146        body = response.read()
147        boto.log.debug(body)
148        if response.status != 204:
149            raise CloudFrontServerError(response.status, response.reason, body)
150
151    # Distributions
152
153    def get_all_distributions(self):
154        tags = [('DistributionSummary', DistributionSummary)]
155        return self._get_all_objects('distribution', tags)
156
157    def get_distribution_info(self, distribution_id):
158        return self._get_info(distribution_id, 'distribution', Distribution)
159
160    def get_distribution_config(self, distribution_id):
161        return self._get_config(distribution_id, 'distribution',
162                                DistributionConfig)
163
164    def set_distribution_config(self, distribution_id, etag, config):
165        return self._set_config(distribution_id, etag, config)
166
167    def create_distribution(self, origin, enabled, caller_reference='',
168                            cnames=None, comment='', trusted_signers=None):
169        config = DistributionConfig(origin=origin, enabled=enabled,
170                                    caller_reference=caller_reference,
171                                    cnames=cnames, comment=comment,
172                                    trusted_signers=trusted_signers)
173        return self._create_object(config, 'distribution', Distribution)
174
175    def delete_distribution(self, distribution_id, etag):
176        return self._delete_object(distribution_id, etag, 'distribution')
177
178    # Streaming Distributions
179
180    def get_all_streaming_distributions(self):
181        tags = [('StreamingDistributionSummary', StreamingDistributionSummary)]
182        return self._get_all_objects('streaming-distribution', tags)
183
184    def get_streaming_distribution_info(self, distribution_id):
185        return self._get_info(distribution_id, 'streaming-distribution',
186                              StreamingDistribution)
187
188    def get_streaming_distribution_config(self, distribution_id):
189        return self._get_config(distribution_id, 'streaming-distribution',
190                                StreamingDistributionConfig)
191
192    def set_streaming_distribution_config(self, distribution_id, etag, config):
193        return self._set_config(distribution_id, etag, config)
194
195    def create_streaming_distribution(self, origin, enabled,
196                                      caller_reference='',
197                                      cnames=None, comment='',
198                                      trusted_signers=None):
199        config = StreamingDistributionConfig(origin=origin, enabled=enabled,
200                                             caller_reference=caller_reference,
201                                             cnames=cnames, comment=comment,
202                                             trusted_signers=trusted_signers)
203        return self._create_object(config, 'streaming-distribution',
204                                   StreamingDistribution)
205
206    def delete_streaming_distribution(self, distribution_id, etag):
207        return self._delete_object(distribution_id, etag,
208                                   'streaming-distribution')
209
210    # Origin Access Identity
211
212    def get_all_origin_access_identity(self):
213        tags = [('CloudFrontOriginAccessIdentitySummary',
214               OriginAccessIdentitySummary)]
215        return self._get_all_objects('origin-access-identity/cloudfront', tags)
216
217    def get_origin_access_identity_info(self, access_id):
218        return self._get_info(access_id, 'origin-access-identity/cloudfront',
219                              OriginAccessIdentity)
220
221    def get_origin_access_identity_config(self, access_id):
222        return self._get_config(access_id,
223                                'origin-access-identity/cloudfront',
224                                OriginAccessIdentityConfig)
225
226    def set_origin_access_identity_config(self, access_id,
227                                          etag, config):
228        return self._set_config(access_id, etag, config)
229
230    def create_origin_access_identity(self, caller_reference='', comment=''):
231        config = OriginAccessIdentityConfig(caller_reference=caller_reference,
232                                            comment=comment)
233        return self._create_object(config, 'origin-access-identity/cloudfront',
234                                   OriginAccessIdentity)
235
236    def delete_origin_access_identity(self, access_id, etag):
237        return self._delete_object(access_id, etag,
238                                   'origin-access-identity/cloudfront')
239
240    # Object Invalidation
241
242    def create_invalidation_request(self, distribution_id, paths,
243                                    caller_reference=None):
244        """Creates a new invalidation request
245            :see: http://goo.gl/8vECq
246        """
247        # We allow you to pass in either an array or
248        # an InvalidationBatch object
249        if not isinstance(paths, InvalidationBatch):
250            paths = InvalidationBatch(paths)
251        paths.connection = self
252        uri = '/%s/distribution/%s/invalidation' % (self.Version,
253                                                    distribution_id)
254        response = self.make_request('POST', uri,
255                                     {'Content-Type': 'text/xml'},
256                                     data=paths.to_xml())
257        body = response.read()
258        if response.status == 201:
259            h = handler.XmlHandler(paths, self)
260            xml.sax.parseString(body, h)
261            return paths
262        else:
263            raise CloudFrontServerError(response.status, response.reason, body)
264
265    def invalidation_request_status(self, distribution_id,
266                                     request_id, caller_reference=None):
267        uri = '/%s/distribution/%s/invalidation/%s' % (self.Version,
268                                                       distribution_id,
269                                                       request_id)
270        response = self.make_request('GET', uri, {'Content-Type': 'text/xml'})
271        body = response.read()
272        if response.status == 200:
273            paths = InvalidationBatch([])
274            h = handler.XmlHandler(paths, self)
275            xml.sax.parseString(body, h)
276            return paths
277        else:
278            raise CloudFrontServerError(response.status, response.reason, body)
279
280    def get_invalidation_requests(self, distribution_id, marker=None,
281                                  max_items=None):
282        """
283        Get all invalidation requests for a given CloudFront distribution.
284        This returns an instance of an InvalidationListResultSet that
285        automatically handles all of the result paging, etc. from CF - you just
286        need to keep iterating until there are no more results.
287
288        :type distribution_id: string
289        :param distribution_id: The id of the CloudFront distribution
290
291        :type marker: string
292        :param marker: Use this only when paginating results and only in
293                       follow-up request after you've received a response where
294                       the results are truncated. Set this to the value of the
295                       Marker element in the response you just received.
296
297        :type max_items: int
298        :param max_items: Use this only when paginating results and only in a
299                          follow-up request to indicate the maximum number of
300                          invalidation requests you want in the response. You
301                          will need to pass the next_marker property from the
302                          previous InvalidationListResultSet response in the
303                          follow-up request in order to get the next 'page' of
304                          results.
305
306        :rtype: :class:`boto.cloudfront.invalidation.InvalidationListResultSet`
307        :returns: An InvalidationListResultSet iterator that lists invalidation
308                  requests for a given CloudFront distribution. Automatically
309                  handles paging the results.
310        """
311        uri = 'distribution/%s/invalidation' % distribution_id
312        params = dict()
313        if marker:
314            params['Marker'] = marker
315        if max_items:
316            params['MaxItems'] = max_items
317        if params:
318            uri += '?%s=%s' % params.popitem()
319            for k, v in params.items():
320                uri += '&%s=%s' % (k, v)
321        tags=[('InvalidationSummary', InvalidationSummary)]
322        rs_class = InvalidationListResultSet
323        rs_kwargs = dict(connection=self, distribution_id=distribution_id,
324                         max_items=max_items, marker=marker)
325        return self._get_all_objects(uri, tags, result_set_class=rs_class,
326                                     result_set_kwargs=rs_kwargs)
327