OLD | NEW |
(Empty) | |
| 1 # Copyright (c) 2010-2012 Mitch Garnaat http://garnaat.org/ |
| 2 # Copyright (c) 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved |
| 3 # |
| 4 # Permission is hereby granted, free of charge, to any person obtaining a |
| 5 # copy of this software and associated documentation files (the |
| 6 # "Software"), to deal in the Software without restriction, including |
| 7 # without limitation the rights to use, copy, modify, merge, publish, dis- |
| 8 # tribute, sublicense, and/or sell copies of the Software, and to permit |
| 9 # persons to whom the Software is furnished to do so, subject to the fol- |
| 10 # lowing conditions: |
| 11 # |
| 12 # The above copyright notice and this permission notice shall be included |
| 13 # in all copies or substantial portions of the Software. |
| 14 # |
| 15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
| 16 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- |
| 17 # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT |
| 18 # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
| 19 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
| 21 # IN THE SOFTWARE. |
| 22 |
| 23 import uuid |
| 24 |
| 25 from boto.connection import AWSQueryConnection |
| 26 from boto.regioninfo import RegionInfo |
| 27 from boto.compat import json |
| 28 import boto |
| 29 |
| 30 |
| 31 class SNSConnection(AWSQueryConnection): |
| 32 |
| 33 DefaultRegionName = 'us-east-1' |
| 34 DefaultRegionEndpoint = 'sns.us-east-1.amazonaws.com' |
| 35 APIVersion = '2010-03-31' |
| 36 |
| 37 def __init__(self, aws_access_key_id=None, aws_secret_access_key=None, |
| 38 is_secure=True, port=None, proxy=None, proxy_port=None, |
| 39 proxy_user=None, proxy_pass=None, debug=0, |
| 40 https_connection_factory=None, region=None, path='/', |
| 41 security_token=None, validate_certs=True): |
| 42 if not region: |
| 43 region = RegionInfo(self, self.DefaultRegionName, |
| 44 self.DefaultRegionEndpoint, |
| 45 connection_cls=SNSConnection) |
| 46 self.region = region |
| 47 AWSQueryConnection.__init__(self, aws_access_key_id, |
| 48 aws_secret_access_key, |
| 49 is_secure, port, proxy, proxy_port, |
| 50 proxy_user, proxy_pass, |
| 51 self.region.endpoint, debug, |
| 52 https_connection_factory, path, |
| 53 security_token=security_token, |
| 54 validate_certs=validate_certs) |
| 55 |
| 56 def _required_auth_capability(self): |
| 57 return ['sns'] |
| 58 |
| 59 def get_all_topics(self, next_token=None): |
| 60 """ |
| 61 :type next_token: string |
| 62 :param next_token: Token returned by the previous call to |
| 63 this method. |
| 64 |
| 65 """ |
| 66 params = {'ContentType': 'JSON'} |
| 67 if next_token: |
| 68 params['NextToken'] = next_token |
| 69 response = self.make_request('ListTopics', params, '/', 'GET') |
| 70 body = response.read() |
| 71 if response.status == 200: |
| 72 return json.loads(body) |
| 73 else: |
| 74 boto.log.error('%s %s' % (response.status, response.reason)) |
| 75 boto.log.error('%s' % body) |
| 76 raise self.ResponseError(response.status, response.reason, body) |
| 77 |
| 78 def get_topic_attributes(self, topic): |
| 79 """ |
| 80 Get attributes of a Topic |
| 81 |
| 82 :type topic: string |
| 83 :param topic: The ARN of the topic. |
| 84 |
| 85 """ |
| 86 params = {'ContentType': 'JSON', |
| 87 'TopicArn': topic} |
| 88 response = self.make_request('GetTopicAttributes', params, '/', 'GET') |
| 89 body = response.read() |
| 90 if response.status == 200: |
| 91 return json.loads(body) |
| 92 else: |
| 93 boto.log.error('%s %s' % (response.status, response.reason)) |
| 94 boto.log.error('%s' % body) |
| 95 raise self.ResponseError(response.status, response.reason, body) |
| 96 |
| 97 def set_topic_attributes(self, topic, attr_name, attr_value): |
| 98 """ |
| 99 Get attributes of a Topic |
| 100 |
| 101 :type topic: string |
| 102 :param topic: The ARN of the topic. |
| 103 |
| 104 :type attr_name: string |
| 105 :param attr_name: The name of the attribute you want to set. |
| 106 Only a subset of the topic's attributes are mutable. |
| 107 Valid values: Policy | DisplayName |
| 108 |
| 109 :type attr_value: string |
| 110 :param attr_value: The new value for the attribute. |
| 111 |
| 112 """ |
| 113 params = {'ContentType': 'JSON', |
| 114 'TopicArn': topic, |
| 115 'AttributeName': attr_name, |
| 116 'AttributeValue': attr_value} |
| 117 response = self.make_request('SetTopicAttributes', params, '/', 'GET') |
| 118 body = response.read() |
| 119 if response.status == 200: |
| 120 return json.loads(body) |
| 121 else: |
| 122 boto.log.error('%s %s' % (response.status, response.reason)) |
| 123 boto.log.error('%s' % body) |
| 124 raise self.ResponseError(response.status, response.reason, body) |
| 125 |
| 126 def add_permission(self, topic, label, account_ids, actions): |
| 127 """ |
| 128 Adds a statement to a topic's access control policy, granting |
| 129 access for the specified AWS accounts to the specified actions. |
| 130 |
| 131 :type topic: string |
| 132 :param topic: The ARN of the topic. |
| 133 |
| 134 :type label: string |
| 135 :param label: A unique identifier for the new policy statement. |
| 136 |
| 137 :type account_ids: list of strings |
| 138 :param account_ids: The AWS account ids of the users who will be |
| 139 give access to the specified actions. |
| 140 |
| 141 :type actions: list of strings |
| 142 :param actions: The actions you want to allow for each of the |
| 143 specified principal(s). |
| 144 |
| 145 """ |
| 146 params = {'ContentType': 'JSON', |
| 147 'TopicArn': topic, |
| 148 'Label': label} |
| 149 self.build_list_params(params, account_ids, 'AWSAccountId.member') |
| 150 self.build_list_params(params, actions, 'ActionName.member') |
| 151 response = self.make_request('AddPermission', params, '/', 'GET') |
| 152 body = response.read() |
| 153 if response.status == 200: |
| 154 return json.loads(body) |
| 155 else: |
| 156 boto.log.error('%s %s' % (response.status, response.reason)) |
| 157 boto.log.error('%s' % body) |
| 158 raise self.ResponseError(response.status, response.reason, body) |
| 159 |
| 160 def remove_permission(self, topic, label): |
| 161 """ |
| 162 Removes a statement from a topic's access control policy. |
| 163 |
| 164 :type topic: string |
| 165 :param topic: The ARN of the topic. |
| 166 |
| 167 :type label: string |
| 168 :param label: A unique identifier for the policy statement |
| 169 to be removed. |
| 170 |
| 171 """ |
| 172 params = {'ContentType': 'JSON', |
| 173 'TopicArn': topic, |
| 174 'Label': label} |
| 175 response = self.make_request('RemovePermission', params, '/', 'GET') |
| 176 body = response.read() |
| 177 if response.status == 200: |
| 178 return json.loads(body) |
| 179 else: |
| 180 boto.log.error('%s %s' % (response.status, response.reason)) |
| 181 boto.log.error('%s' % body) |
| 182 raise self.ResponseError(response.status, response.reason, body) |
| 183 |
| 184 def create_topic(self, topic): |
| 185 """ |
| 186 Create a new Topic. |
| 187 |
| 188 :type topic: string |
| 189 :param topic: The name of the new topic. |
| 190 |
| 191 """ |
| 192 params = {'ContentType': 'JSON', |
| 193 'Name': topic} |
| 194 response = self.make_request('CreateTopic', params, '/', 'GET') |
| 195 body = response.read() |
| 196 if response.status == 200: |
| 197 return json.loads(body) |
| 198 else: |
| 199 boto.log.error('%s %s' % (response.status, response.reason)) |
| 200 boto.log.error('%s' % body) |
| 201 raise self.ResponseError(response.status, response.reason, body) |
| 202 |
| 203 def delete_topic(self, topic): |
| 204 """ |
| 205 Delete an existing topic |
| 206 |
| 207 :type topic: string |
| 208 :param topic: The ARN of the topic |
| 209 |
| 210 """ |
| 211 params = {'ContentType': 'JSON', |
| 212 'TopicArn': topic} |
| 213 response = self.make_request('DeleteTopic', params, '/', 'GET') |
| 214 body = response.read() |
| 215 if response.status == 200: |
| 216 return json.loads(body) |
| 217 else: |
| 218 boto.log.error('%s %s' % (response.status, response.reason)) |
| 219 boto.log.error('%s' % body) |
| 220 raise self.ResponseError(response.status, response.reason, body) |
| 221 |
| 222 def publish(self, topic, message, subject=None): |
| 223 """ |
| 224 Get properties of a Topic |
| 225 |
| 226 :type topic: string |
| 227 :param topic: The ARN of the new topic. |
| 228 |
| 229 :type message: string |
| 230 :param message: The message you want to send to the topic. |
| 231 Messages must be UTF-8 encoded strings and |
| 232 be at most 4KB in size. |
| 233 |
| 234 :type subject: string |
| 235 :param subject: Optional parameter to be used as the "Subject" |
| 236 line of the email notifications. |
| 237 |
| 238 """ |
| 239 params = {'ContentType': 'JSON', |
| 240 'TopicArn': topic, |
| 241 'Message': message} |
| 242 if subject: |
| 243 params['Subject'] = subject |
| 244 response = self.make_request('Publish', params, '/', 'GET') |
| 245 body = response.read() |
| 246 if response.status == 200: |
| 247 return json.loads(body) |
| 248 else: |
| 249 boto.log.error('%s %s' % (response.status, response.reason)) |
| 250 boto.log.error('%s' % body) |
| 251 raise self.ResponseError(response.status, response.reason, body) |
| 252 |
| 253 def subscribe(self, topic, protocol, endpoint): |
| 254 """ |
| 255 Subscribe to a Topic. |
| 256 |
| 257 :type topic: string |
| 258 :param topic: The ARN of the new topic. |
| 259 |
| 260 :type protocol: string |
| 261 :param protocol: The protocol used to communicate with |
| 262 the subscriber. Current choices are: |
| 263 email|email-json|http|https|sqs |
| 264 |
| 265 :type endpoint: string |
| 266 :param endpoint: The location of the endpoint for |
| 267 the subscriber. |
| 268 * For email, this would be a valid email address |
| 269 * For email-json, this would be a valid email address |
| 270 * For http, this would be a URL beginning with http |
| 271 * For https, this would be a URL beginning with https |
| 272 * For sqs, this would be the ARN of an SQS Queue |
| 273 """ |
| 274 params = {'ContentType': 'JSON', |
| 275 'TopicArn': topic, |
| 276 'Protocol': protocol, |
| 277 'Endpoint': endpoint} |
| 278 response = self.make_request('Subscribe', params, '/', 'GET') |
| 279 body = response.read() |
| 280 if response.status == 200: |
| 281 return json.loads(body) |
| 282 else: |
| 283 boto.log.error('%s %s' % (response.status, response.reason)) |
| 284 boto.log.error('%s' % body) |
| 285 raise self.ResponseError(response.status, response.reason, body) |
| 286 |
| 287 def subscribe_sqs_queue(self, topic, queue): |
| 288 """ |
| 289 Subscribe an SQS queue to a topic. |
| 290 |
| 291 This is convenience method that handles most of the complexity involved |
| 292 in using an SQS queue as an endpoint for an SNS topic. To achieve this |
| 293 the following operations are performed: |
| 294 |
| 295 * The correct ARN is constructed for the SQS queue and that ARN is |
| 296 then subscribed to the topic. |
| 297 * A JSON policy document is contructed that grants permission to |
| 298 the SNS topic to send messages to the SQS queue. |
| 299 * This JSON policy is then associated with the SQS queue using |
| 300 the queue's set_attribute method. If the queue already has |
| 301 a policy associated with it, this process will add a Statement to |
| 302 that policy. If no policy exists, a new policy will be created. |
| 303 |
| 304 :type topic: string |
| 305 :param topic: The ARN of the new topic. |
| 306 |
| 307 :type queue: A boto Queue object |
| 308 :param queue: The queue you wish to subscribe to the SNS Topic. |
| 309 """ |
| 310 t = queue.id.split('/') |
| 311 q_arn = 'arn:aws:sqs:%s:%s:%s' % (queue.connection.region.name, |
| 312 t[1], t[2]) |
| 313 resp = self.subscribe(topic, 'sqs', q_arn) |
| 314 policy = queue.get_attributes('Policy') |
| 315 if 'Version' not in policy: |
| 316 policy['Version'] = '2008-10-17' |
| 317 if 'Statement' not in policy: |
| 318 policy['Statement'] = [] |
| 319 statement = {'Action': 'SQS:SendMessage', |
| 320 'Effect': 'Allow', |
| 321 'Principal': {'AWS': '*'}, |
| 322 'Resource': q_arn, |
| 323 'Sid': str(uuid.uuid4()), |
| 324 'Condition': {'StringLike': {'aws:SourceArn': topic}}} |
| 325 policy['Statement'].append(statement) |
| 326 queue.set_attribute('Policy', json.dumps(policy)) |
| 327 return resp |
| 328 |
| 329 def confirm_subscription(self, topic, token, |
| 330 authenticate_on_unsubscribe=False): |
| 331 """ |
| 332 Get properties of a Topic |
| 333 |
| 334 :type topic: string |
| 335 :param topic: The ARN of the new topic. |
| 336 |
| 337 :type token: string |
| 338 :param token: Short-lived token sent to and endpoint during |
| 339 the Subscribe operation. |
| 340 |
| 341 :type authenticate_on_unsubscribe: bool |
| 342 :param authenticate_on_unsubscribe: Optional parameter indicating |
| 343 that you wish to disable |
| 344 unauthenticated unsubscription |
| 345 of the subscription. |
| 346 |
| 347 """ |
| 348 params = {'ContentType': 'JSON', |
| 349 'TopicArn': topic, |
| 350 'Token': token} |
| 351 if authenticate_on_unsubscribe: |
| 352 params['AuthenticateOnUnsubscribe'] = 'true' |
| 353 response = self.make_request('ConfirmSubscription', params, '/', 'GET') |
| 354 body = response.read() |
| 355 if response.status == 200: |
| 356 return json.loads(body) |
| 357 else: |
| 358 boto.log.error('%s %s' % (response.status, response.reason)) |
| 359 boto.log.error('%s' % body) |
| 360 raise self.ResponseError(response.status, response.reason, body) |
| 361 |
| 362 def unsubscribe(self, subscription): |
| 363 """ |
| 364 Allows endpoint owner to delete subscription. |
| 365 Confirmation message will be delivered. |
| 366 |
| 367 :type subscription: string |
| 368 :param subscription: The ARN of the subscription to be deleted. |
| 369 |
| 370 """ |
| 371 params = {'ContentType': 'JSON', |
| 372 'SubscriptionArn': subscription} |
| 373 response = self.make_request('Unsubscribe', params, '/', 'GET') |
| 374 body = response.read() |
| 375 if response.status == 200: |
| 376 return json.loads(body) |
| 377 else: |
| 378 boto.log.error('%s %s' % (response.status, response.reason)) |
| 379 boto.log.error('%s' % body) |
| 380 raise self.ResponseError(response.status, response.reason, body) |
| 381 |
| 382 def get_all_subscriptions(self, next_token=None): |
| 383 """ |
| 384 Get list of all subscriptions. |
| 385 |
| 386 :type next_token: string |
| 387 :param next_token: Token returned by the previous call to |
| 388 this method. |
| 389 |
| 390 """ |
| 391 params = {'ContentType': 'JSON'} |
| 392 if next_token: |
| 393 params['NextToken'] = next_token |
| 394 response = self.make_request('ListSubscriptions', params, '/', 'GET') |
| 395 body = response.read() |
| 396 if response.status == 200: |
| 397 return json.loads(body) |
| 398 else: |
| 399 boto.log.error('%s %s' % (response.status, response.reason)) |
| 400 boto.log.error('%s' % body) |
| 401 raise self.ResponseError(response.status, response.reason, body) |
| 402 |
| 403 def get_all_subscriptions_by_topic(self, topic, next_token=None): |
| 404 """ |
| 405 Get list of all subscriptions to a specific topic. |
| 406 |
| 407 :type topic: string |
| 408 :param topic: The ARN of the topic for which you wish to |
| 409 find subscriptions. |
| 410 |
| 411 :type next_token: string |
| 412 :param next_token: Token returned by the previous call to |
| 413 this method. |
| 414 |
| 415 """ |
| 416 params = {'ContentType': 'JSON', |
| 417 'TopicArn': topic} |
| 418 if next_token: |
| 419 params['NextToken'] = next_token |
| 420 response = self.make_request('ListSubscriptionsByTopic', params, |
| 421 '/', 'GET') |
| 422 body = response.read() |
| 423 if response.status == 200: |
| 424 return json.loads(body) |
| 425 else: |
| 426 boto.log.error('%s %s' % (response.status, response.reason)) |
| 427 boto.log.error('%s' % body) |
| 428 raise self.ResponseError(response.status, response.reason, body) |
OLD | NEW |