From 437a62836f71b725900bee28d845c1a0aca15129 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Tue, 3 Nov 2015 23:01:50 -0500 Subject: [PATCH] Add sns_topic module to manage AWS SNS topics This adds an sns_topic module which allows you to create and delete AWS SNS topics as well as subscriptions to those topics. --- cloud/amazon/sns_topic.py | 261 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100755 cloud/amazon/sns_topic.py diff --git a/cloud/amazon/sns_topic.py b/cloud/amazon/sns_topic.py new file mode 100755 index 00000000000..a9de7b88f10 --- /dev/null +++ b/cloud/amazon/sns_topic.py @@ -0,0 +1,261 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +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)" +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 + policy: + description: + - Policy to apply to the SNS topic + required: False + delivery_policy: + description: + - Delivery policy to apply to the SNS topic + required: False + 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. + 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. + 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: "" + disableSubscriptionOverrides: True + defaultThrottlePolicy: + maxReceivesPerSecond: 10 + subscriptions: + - endpoint: "my_email_address@example.com" + protocol: "email" + - endpoint: "my_mobile_number" + protocol: "sms" + +""" + +import sys +import time +import json +import re + +try: + import boto + import boto.sns +except ImportError: + print "failed=True msg='boto required for this module'" + sys.exit(1) + + +def canonicalize_endpoint(protocol, endpoint): + if protocol == 'sms': + import re + return re.sub('[^0-9]*', '', endpoint) + return endpoint + + + +def get_all_topics(connection): + next_token = None + topics = [] + while True: + response = connection.get_all_topics(next_token) + 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(connection, short_topic): + # topic names cannot have colons, so this captures the full topic name + all_topics = get_all_topics(connection) + lookup_topic = ':%s' % short_topic + for topic in all_topics: + if topic.endswith(lookup_topic): + return topic + return None + +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(type='list', required=False), + purge_subscriptions=dict(type='bool', default=True), + ) + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + 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 + changed = False + + topic_created = False + attributes_set = [] + subscriptions_added = [] + subscriptions_deleted = [] + + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + if not region: + module.fail_json(msg="region must be specified") + try: + connection = connect_to_aws(boto.sns, region, **aws_connect_params) + except boto.exception.NoAuthHandlerFound, e: + module.fail_json(msg=str(e)) + + # topics cannot contain ':', so thats the decider + if ':' in name: + all_topics = get_all_topics(connection) + if name in all_topics: + arn_topic = name + elif state == 'absent': + module.exit_json(changed=False) + else: + module.fail_json(msg="specified an ARN for a topic but it doesn't" + " exist") + else: + arn_topic = arn_topic_lookup(connection, name) + if not arn_topic: + if state == 'absent': + module.exit_json(changed=False) + elif check_mode: + module.exit_json(changed=True, topic_created=True, + subscriptions_added=subscriptions, + subscriptions_deleted=[]) + + changed=True + topic_created = True + connection.create_topic(name) + arn_topic = arn_topic_lookup(connection, name) + while not arn_topic: + time.sleep(3) + arn_topic = arn_topic_lookup(connection, name) + + if arn_topic and state == "absent": + if not check_mode: + connection.delete_topic(arn_topic) + module.exit_json(changed=True) + + topic_attributes = connection.get_topic_attributes(arn_topic) \ + ['GetTopicAttributesResponse'] ['GetTopicAttributesResult'] \ + ['Attributes'] + if display_name and display_name != topic_attributes['DisplayName']: + changed = True + attributes_set.append('display_name') + if not check_mode: + connection.set_topic_attributes(arn_topic, 'DisplayName', + display_name) + + if policy and policy != json.loads(topic_attributes['policy']): + changed = True + attributes_set.append('policy') + if not check_mode: + connection.set_topic_attributes(arn_topic, 'Policy', + json.dumps(policy)) + + if delivery_policy and ('DeliveryPolicy' not in topic_attributes or \ + delivery_policy != json.loads(topic_attributes['DeliveryPolicy'])): + changed = True + attributes_set.append('delivery_policy') + if not check_mode: + connection.set_topic_attributes(arn_topic, 'DeliveryPolicy', + json.dumps(delivery_policy)) + + + next_token = None + aws_subscriptions = [] + while True: + response = connection.get_all_subscriptions_by_topic(arn_topic, + next_token) + aws_subscriptions.extend(response['ListSubscriptionsByTopicResponse'] \ + ['ListSubscriptionsByTopicResult']['Subscriptions']) + next_token = response['ListSubscriptionsByTopicResponse'] \ + ['ListSubscriptionsByTopicResult']['NextToken'] + if not next_token: + break + + desired_subscriptions = [(sub['protocol'], + canonicalize_endpoint(sub['protocol'], sub['endpoint'])) for sub in + subscriptions] + aws_subscriptions_list = [] + + for sub in aws_subscriptions: + sub_key = (sub['Protocol'], sub['Endpoint']) + aws_subscriptions_list.append(sub_key) + if purge_subscriptions and sub_key not in desired_subscriptions and \ + sub['SubscriptionArn'] != 'PendingConfirmation': + changed = True + subscriptions_deleted.append(sub_key) + if not check_mode: + connection.unsubscribe(sub['SubscriptionArn']) + + for (protocol, endpoint) in desired_subscriptions: + if (protocol, endpoint) not in aws_subscriptions_list: + changed = True + subscriptions_added.append(sub) + if not check_mode: + connection.subscribe(arn_topic, protocol, endpoint) + + module.exit_json(changed=changed, topic_created=topic_created, + attributes_set=attributes_set, + subscriptions_added=subscriptions_added, + subscriptions_deleted=subscriptions_deleted, sns_arn=arn_topic) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +main()