diff options
Diffstat (limited to 'lib/ansible/modules/extras/cloud/amazon/sns_topic.py')
-rw-r--r-- | lib/ansible/modules/extras/cloud/amazon/sns_topic.py | 407 |
1 files changed, 407 insertions, 0 deletions
diff --git a/lib/ansible/modules/extras/cloud/amazon/sns_topic.py b/lib/ansible/modules/extras/cloud/amazon/sns_topic.py new file mode 100644 index 0000000000..f4916693ed --- /dev/null +++ b/lib/ansible/modules/extras/cloud/amazon/sns_topic.py @@ -0,0 +1,407 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# This is a free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This Ansible library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + + +DOCUMENTATION = """ +module: sns_topic +short_description: Manages AWS SNS topics and subscriptions +description: + - The M(sns_topic) module allows you to create, delete, and manage subscriptions for AWS SNS topics. +version_added: 2.0 +author: + - "Joel Thompson (@joelthompson)" + - "Fernando Jose Pando (@nand0p)" +options: + name: + description: + - The name or ARN of the SNS topic to converge + required: True + state: + description: + - Whether to create or destroy an SNS topic + required: False + default: present + choices: ["absent", "present"] + display_name: + description: + - Display name of the topic + required: False + default: None + policy: + description: + - Policy to apply to the SNS topic + required: False + default: None + delivery_policy: + description: + - Delivery policy to apply to the SNS topic + required: False + default: None + subscriptions: + description: + - List of subscriptions to apply to the topic. Note that AWS requires + subscriptions to be confirmed, so you will need to confirm any new + subscriptions. + required: False + default: [] + purge_subscriptions: + description: + - "Whether to purge any subscriptions not listed here. NOTE: AWS does not + allow you to purge any PendingConfirmation subscriptions, so if any + exist and would be purged, they are silently skipped. This means that + somebody could come back later and confirm the subscription. Sorry. + Blame Amazon." + required: False + default: True +extends_documentation_fragment: aws +requirements: [ "boto" ] +""" + +EXAMPLES = """ + +- name: Create alarm SNS topic + sns_topic: + name: "alarms" + state: present + display_name: "alarm SNS topic" + delivery_policy: + http: + defaultHealthyRetryPolicy: + minDelayTarget: 2 + maxDelayTarget: 4 + numRetries: 3 + numMaxDelayRetries: 5 + backoffFunction: "<linear|arithmetic|geometric|exponential>" + disableSubscriptionOverrides: True + defaultThrottlePolicy: + maxReceivesPerSecond: 10 + subscriptions: + - endpoint: "my_email_address@example.com" + protocol: "email" + - endpoint: "my_mobile_number" + protocol: "sms" + +""" + +RETURN = ''' +sns_arn: + description: The ARN of the topic you are modifying + type: string + sample: "arn:aws:sns:us-east-1:123456789012:my_topic_name" + +sns_topic: + description: Dict of sns topic details + type: dict + sample: + name: sns-topic-name + state: present + display_name: default + policy: {} + delivery_policy: {} + subscriptions_new: [] + subscriptions_existing: [] + subscriptions_deleted: [] + subscriptions_added: [] + subscriptions_purge': false + check_mode: false + topic_created: false + topic_deleted: false + attributes_set: [] +''' + +import sys +import time +import json +import re + +try: + import boto.sns + from boto.exception import BotoServerError + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + + +class SnsTopicManager(object): + """ Handles SNS Topic creation and destruction """ + + def __init__(self, + module, + name, + state, + display_name, + policy, + delivery_policy, + subscriptions, + purge_subscriptions, + check_mode, + region, + **aws_connect_params): + + self.region = region + self.aws_connect_params = aws_connect_params + self.connection = self._get_boto_connection() + self.changed = False + self.module = module + self.name = name + self.state = state + self.display_name = display_name + self.policy = policy + self.delivery_policy = delivery_policy + self.subscriptions = subscriptions + self.subscriptions_existing = [] + self.subscriptions_deleted = [] + self.subscriptions_added = [] + self.purge_subscriptions = purge_subscriptions + self.check_mode = check_mode + self.topic_created = False + self.topic_deleted = False + self.arn_topic = None + self.attributes_set = [] + + def _get_boto_connection(self): + try: + return connect_to_aws(boto.sns, self.region, + **self.aws_connect_params) + except BotoServerError, err: + self.module.fail_json(msg=err.message) + + def _get_all_topics(self): + next_token = None + topics = [] + while True: + try: + response = self.connection.get_all_topics(next_token) + except BotoServerError, err: + module.fail_json(msg=err.message) + topics.extend(response['ListTopicsResponse']['ListTopicsResult']['Topics']) + next_token = response['ListTopicsResponse']['ListTopicsResult']['NextToken'] + if not next_token: + break + return [t['TopicArn'] for t in topics] + + + def _arn_topic_lookup(self): + # topic names cannot have colons, so this captures the full topic name + all_topics = self._get_all_topics() + lookup_topic = ':%s' % self.name + for topic in all_topics: + if topic.endswith(lookup_topic): + return topic + + + def _create_topic(self): + self.changed = True + self.topic_created = True + if not self.check_mode: + self.connection.create_topic(self.name) + self.arn_topic = self._arn_topic_lookup() + while not self.arn_topic: + time.sleep(3) + self.arn_topic = self._arn_topic_lookup() + + + def _set_topic_attrs(self): + topic_attributes = self.connection.get_topic_attributes(self.arn_topic) \ + ['GetTopicAttributesResponse'] ['GetTopicAttributesResult'] \ + ['Attributes'] + + if self.display_name and self.display_name != topic_attributes['DisplayName']: + self.changed = True + self.attributes_set.append('display_name') + if not self.check_mode: + self.connection.set_topic_attributes(self.arn_topic, 'DisplayName', + self.display_name) + + if self.policy and self.policy != json.loads(topic_attributes['Policy']): + self.changed = True + self.attributes_set.append('policy') + if not self.check_mode: + self.connection.set_topic_attributes(self.arn_topic, 'Policy', + json.dumps(self.policy)) + + if self.delivery_policy and ('DeliveryPolicy' not in topic_attributes or \ + self.delivery_policy != json.loads(topic_attributes['DeliveryPolicy'])): + self.changed = True + self.attributes_set.append('delivery_policy') + if not self.check_mode: + self.connection.set_topic_attributes(self.arn_topic, 'DeliveryPolicy', + json.dumps(self.delivery_policy)) + + + def _canonicalize_endpoint(self, protocol, endpoint): + if protocol == 'sms': + return re.sub('[^0-9]*', '', endpoint) + return endpoint + + + def _get_topic_subs(self): + next_token = None + while True: + response = self.connection.get_all_subscriptions_by_topic(self.arn_topic, next_token) + self.subscriptions_existing.extend(response['ListSubscriptionsByTopicResponse'] \ + ['ListSubscriptionsByTopicResult']['Subscriptions']) + next_token = response['ListSubscriptionsByTopicResponse'] \ + ['ListSubscriptionsByTopicResult']['NextToken'] + if not next_token: + break + + def _set_topic_subs(self): + subscriptions_existing_list = [] + desired_subscriptions = [(sub['protocol'], + self._canonicalize_endpoint(sub['protocol'], sub['endpoint'])) for sub in + self.subscriptions] + + if self.subscriptions_existing: + for sub in self.subscriptions_existing: + sub_key = (sub['Protocol'], sub['Endpoint']) + subscriptions_existing_list.append(sub_key) + if self.purge_subscriptions and sub_key not in desired_subscriptions and \ + sub['SubscriptionArn'] != 'PendingConfirmation': + self.changed = True + self.subscriptions_deleted.append(sub_key) + if not self.check_mode: + self.connection.unsubscribe(sub['SubscriptionArn']) + + for (protocol, endpoint) in desired_subscriptions: + if (protocol, endpoint) not in subscriptions_existing_list: + self.changed = True + self.subscriptions_added.append(sub) + if not self.check_mode: + self.connection.subscribe(self.arn_topic, protocol, endpoint) + + + def _delete_subscriptions(self): + # NOTE: subscriptions in 'PendingConfirmation' timeout in 3 days + # https://forums.aws.amazon.com/thread.jspa?threadID=85993 + for sub in self.subscriptions_existing: + if sub['SubscriptionArn'] != 'PendingConfirmation': + self.subscriptions_deleted.append(sub['SubscriptionArn']) + self.changed = True + if not self.check_mode: + self.connection.unsubscribe(sub['SubscriptionArn']) + + + def _delete_topic(self): + self.topic_deleted = True + self.changed = True + if not self.check_mode: + self.connection.delete_topic(self.arn_topic) + + + def ensure_ok(self): + self.arn_topic = self._arn_topic_lookup() + if not self.arn_topic: + self._create_topic() + self._set_topic_attrs() + self._get_topic_subs() + self._set_topic_subs() + + def ensure_gone(self): + self.arn_topic = self._arn_topic_lookup() + if self.arn_topic: + self._get_topic_subs() + if self.subscriptions_existing: + self._delete_subscriptions() + self._delete_topic() + + + def get_info(self): + info = { + 'name': self.name, + 'state': self.state, + 'display_name': self.display_name, + 'policy': self.policy, + 'delivery_policy': self.delivery_policy, + 'subscriptions_new': self.subscriptions, + 'subscriptions_existing': self.subscriptions_existing, + 'subscriptions_deleted': self.subscriptions_deleted, + 'subscriptions_added': self.subscriptions_added, + 'subscriptions_purge': self.purge_subscriptions, + 'check_mode': self.check_mode, + 'topic_created': self.topic_created, + 'topic_deleted': self.topic_deleted, + 'attributes_set': self.attributes_set + } + + return info + + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + name=dict(type='str', required=True), + state=dict(type='str', default='present', choices=['present', + 'absent']), + display_name=dict(type='str', required=False), + policy=dict(type='dict', required=False), + delivery_policy=dict(type='dict', required=False), + subscriptions=dict(default=[], type='list', required=False), + purge_subscriptions=dict(type='bool', default=True), + ) + ) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') + + name = module.params.get('name') + state = module.params.get('state') + display_name = module.params.get('display_name') + policy = module.params.get('policy') + delivery_policy = module.params.get('delivery_policy') + subscriptions = module.params.get('subscriptions') + purge_subscriptions = module.params.get('purge_subscriptions') + check_mode = module.check_mode + + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + if not region: + module.fail_json(msg="region must be specified") + + sns_topic = SnsTopicManager(module, + name, + state, + display_name, + policy, + delivery_policy, + subscriptions, + purge_subscriptions, + check_mode, + region, + **aws_connect_params) + + if state == 'present': + sns_topic.ensure_ok() + + elif state == 'absent': + sns_topic.ensure_gone() + + sns_facts = dict(changed=sns_topic.changed, + sns_arn=sns_topic.arn_topic, + sns_topic=sns_topic.get_info()) + + module.exit_json(**sns_facts) + + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() |