Add boto_sns module and state.

Include integration tests.
This commit is contained in:
Mathias Gug 2015-01-14 15:43:06 -08:00 committed by rallytime
parent 2b97da4015
commit 4542072c4e
4 changed files with 457 additions and 0 deletions

120
salt/modules/boto_sns.py Normal file
View File

@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
import logging
log = logging.getLogger(__name__)
# Import third party libs
try:
import boto
import boto.sns
logging.getLogger('boto').setLevel(logging.CRITICAL)
HAS_BOTO = True
except ImportError:
HAS_BOTO = False
from salt.ext.six import string_types
def __virtual__():
'''
Only load if boto libraries exist.
'''
if not HAS_BOTO:
return False
return True
def get_all_topics(region=None, key=None, keyid=None, profile=None):
cache_key = 'boto_sns.topics_cache'
try:
return __context__[cache_key]
except KeyError:
pass
conn = _get_conn(region, key, keyid, profile)
__context__[cache_key] = {}
# TODO: support >100 SNS topics (via NextToken)
for t in conn.get_all_topics()['ListTopicsResponse']\
['ListTopicsResult']['Topics']:
short_name = t['TopicArn'].split(':')[-1]
__context__[cache_key][short_name] = t['TopicArn']
return __context__[cache_key]
def exists(name, region=None, key=None, keyid=None, profile=None):
'''
Check to see if an SNS topic exists.
CLI example::
salt myminion boto_sns.exists mytopic region=us-east-1
'''
topics = get_all_topics(region=region, key=key, keyid=keyid, profile=profile)
if name.startswith('arn:aws:sns:'):
return name in topics.values()
else:
return name in topics.keys()
def create(name, region=None, key=None, keyid=None, profile=None):
'''
Create an SNS topic.
CLI example to create a topic::
salt myminion boto_sns.create mytopic region=us-east-1
'''
conn = _get_conn(region, key, keyid, profile)
ret = conn.create_topic(name)
log.info('Created SNS topic {0}'.format(name))
return True
def delete(name, region=None, key=None, keyid=None, profile=None):
'''
Delete an SNS topic.
CLI example to delete a topic::
salt myminion boto_sns.delete mytopic region=us-east-1
'''
conn = _get_conn(region, key, keyid, profile)
conn.delete_topic(get_arn(name, region, key, keyid, profile))
log.info('Deleted SNS topic {0}'.format(name))
return True
def get_arn(name, region=None, key=None, keyid=None, profile=None):
if name.startswith('arn:aws:sns:'):
return name
account_id = __salt__['boto_iam.get_account_id']()
return 'arn:aws:sns:{}:{}:{}'.format(_get_region(region), account_id, name)
def _get_region(region=None):
if not region and __salt__['config.option']('sns.region'):
region = __salt__['config.option']('sns.region')
if not region:
region = 'us-east-1'
return region
def _get_conn(region, key, keyid, profile):
'''
Get a boto connection to SNS.
'''
if profile:
if isinstance(profile, string_types):
_profile = __salt__['config.option'](profile)
elif isinstance(profile, dict):
_profile = profile
key = _profile.get('key', None)
keyid = _profile.get('keyid', None)
region = _profile.get('region', None)
region = _get_region(region)
if not key and __salt__['config.option']('sns.key'):
key = __salt__['config.option']('sns.key')
if not keyid and __salt__['config.option']('sns.keyid'):
keyid = __salt__['config.option']('sns.keyid')
conn = boto.sns.connect_to_region(region,
aws_access_key_id=keyid,
aws_secret_access_key=key)
return conn

165
salt/states/boto_sns.py Normal file
View File

@ -0,0 +1,165 @@
# -*- coding: utf-8 -*-
'''
Manage SNS Topics
Create and destroy SNS topics. Be aware that this interacts with Amazon's
services, and so may incur charges.
This module uses ``boto``, which can be installed via package, or pip.
This module accepts explicit SQS credentials but can also utilize
IAM roles assigned to the instance through Instance Profiles. Dynamic
credentials are then automatically obtained from AWS API and no further
configuration is necessary. More information available `here
<http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html>`_.
If IAM roles are not used you need to specify them either in a pillar file or
in the minion's config file:
.. code-block:: yaml
sns.keyid: GKTADJGHEIQSXMKKRBJ08H
sns.key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
It's also possible to specify ``key``, ``keyid`` and ``region`` via a profile, either
passed in as a dict, or as a string to pull from pillars or minion config:
.. code-block:: yaml
myprofile:
keyid: GKTADJGHEIQSXMKKRBJ08H
key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
region: us-east-1
.. code-block:: yaml
mytopic:
boto_sns.present:
- region: us-east-1
- keyid: GKTADJGHEIQSXMKKRBJ08H
- key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
# Using a profile from pillars
mytopic:
boto_sns.present:
- region: us-east-1
- profile: mysnsprofile
# Passing in a profile
mytopic:
boto_sns.present:
- region: us-east-1
- profile:
keyid: GKTADJGHEIQSXMKKRBJ08H
key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
'''
from __future__ import absolute_import
def __virtual__():
'''
Only load if boto is available.
'''
return 'boto_sns' if 'boto_sns.exists' in __salt__ else False
def present(
name,
region=None,
key=None,
keyid=None,
profile=None):
'''
Ensure the SNS topic exists.
name
Name of the SNS topic.
region
Region to connect to.
key
Secret key to be used.
keyid
Access key to be used.
profile
A dict with region, key and keyid, or a pillar key (string)
that contains a dict with region, key and keyid.
'''
ret = {'name': name, 'result': True, 'comment': '', 'changes': {}}
is_present = __salt__['boto_sns.exists'](name, region, key, keyid, profile)
if is_present:
ret['comment'] = 'AWS SNS topic {0} present.'.format(name)
else:
if __opts__['test']:
msg = 'AWS SNS topic {0} is set to be created.'.format(name)
ret['comment'] = msg
ret['result'] = None
return ret
created = __salt__['boto_sns.create'](name, region, key, keyid,
profile)
if created:
msg = 'AWS SNS topic {0} created.'.format(name)
ret['comment'] = msg
ret['changes']['old'] = None
ret['changes']['new'] = {'topic': name}
else:
ret['result'] = False
ret['comment'] = 'Failed to create {0} AWS SNS topic'.format(name)
return ret
return ret
def absent(
name,
region=None,
key=None,
keyid=None,
profile=None):
'''
Ensure the named sns topic is deleted.
name
Name of the SNS topic.
region
Region to connect to.
key
Secret key to be used.
keyid
Access key to be used.
profile
A dict with region, key and keyid, or a pillar key (string)
that contains a dict with region, key and keyid.
'''
ret = {'name': name, 'result': True, 'comment': '', 'changes': {}}
is_present = __salt__['boto_sns.exists'](name, region, key, keyid, profile)
if is_present:
if __opts__['test']:
ret['comment'] = 'AWS SNS topic {0} is set to be removed.'.format(
name)
ret['result'] = None
return ret
deleted = __salt__['boto_sns.delete'](name, region, key, keyid,
profile)
if deleted:
ret['comment'] = 'AWS SNS topic {0} does not exist.'.format(name)
ret['changes']['old'] = {'topic': name}
ret['changes']['new'] = None
else:
ret['result'] = False
ret['comment'] = 'Failed to delete {0} AWS SNS topic.'.format(name)
else:
ret['comment'] = 'AWS SNS topic {0} does not exist.'.format(name)
return ret

View File

@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
'''
Validate the boto_sns module
'''
from salttesting import skipIf
from salttesting.helpers import ensure_in_syspath
ensure_in_syspath('../../')
import integration
NO_BOTO_MODULE = True
BOTO_NOT_CONFIGURED = True
try:
import boto
NO_BOTO_MODULE = False
try:
boto.connect_iam()
BOTO_NOT_CONFIGURED = False
except boto.exception.NoAuthHandlerFound:
pass
except ImportError:
pass
@skipIf(
NO_BOTO_MODULE,
'Please install the boto library before running boto integration tests.'
)
@skipIf(
BOTO_NOT_CONFIGURED,
'Please setup boto AWS credentials before running boto integration tests.'
)
class BotoSNSTest(integration.ModuleCase):
def test_exists(self):
ret = self.run_function('boto_sns.exists', ['nonexistent'])
self.assertFalse(ret)
def test_create(self):
ret = self.run_function('boto_sns.create', ['my-test-topic'])
self.assertTrue(ret)
def test_delete(self):
ret = self.run_function('boto_sns.delete', ['my-test-topic'])
self.assertTrue(ret)
def test_get_all_topics(self):
self.run_function('boto_sns.create', ['my-test-topic'])
self.run_function('boto_sns.create', ['my-second-test-topic'])
ret = self.run_function('boto_sns.get_all_topics')
self.assertIn('my-test-topic', ret.keys())
self.assertIn(self._get_arn('my-test-topic'), ret.values())
self.assertIn('my-second-test-topic', ret.keys())
self.assertIn(self._get_arn('my-second-test-topic'), ret.values())
def _get_arn(self, name):
return 'arn:aws:sns:us-east-1:{}:{}'.format(self.account_id, name)
@property
def account_id(self):
if not hasattr(self, '_account_id'):
account_id = self.run_function('boto_iam.get_account_id')
setattr(self, '_account_id', account_id)
return self._account_id

View File

@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
"""
Tests for the boto_sns state
"""
from salttesting import skipIf
from salttesting.helpers import ensure_in_syspath
ensure_in_syspath('../../')
import integration
NO_BOTO_MODULE = True
BOTO_NOT_CONFIGURED = True
try:
import boto
NO_BOTO_MODULE = False
try:
boto.connect_iam()
BOTO_NOT_CONFIGURED = False
except boto.exception.NoAuthHandlerFound:
pass
except ImportError:
pass
@skipIf(
NO_BOTO_MODULE,
'Please install the boto library before running boto integration tests.'
)
@skipIf(
BOTO_NOT_CONFIGURED,
'Please setup boto AWS credentials before running boto integration tests.'
)
class BotoSNSTest(integration.ModuleCase,
integration.SaltReturnAssertsMixIn):
def setUp(self):
self.run_function('boto_sns.delete', name='my-state-test-topic')
def tearDown(self):
self.run_function('boto_sns.delete', name='my-state-test-topic')
def test_present_not_exist(self):
ret = self.run_state('boto_sns.present',
name='my-state-test-topic')
self.assertSaltTrueReturn(ret)
self.assertInSaltReturn('my-state-test-topic', ret, 'name')
self.assertInSaltComment('AWS SNS topic my-state-test-topic created.', ret)
self.assertSaltStateChangesEqual(ret, {'old': None, 'new': {'topic': 'my-state-test-topic'}})
def test_present_already_exist(self):
self.run_state('boto_sns.present',
name='my-state-test-topic')
ret = self.run_state('boto_sns.present',
name='my-state-test-topic')
self.assertSaltTrueReturn(ret)
self.assertInSaltReturn('my-state-test-topic', ret, 'name')
self.assertInSaltComment('AWS SNS topic my-state-test-topic present.', ret)
self.assertSaltStateChangesEqual(ret, {})
def test_present_test_mode(self):
ret = self.run_state('boto_sns.present',
name='my-state-test-topic',
test=True)
self.assertSaltNoneReturn(ret)
self.assertInSaltReturn('my-state-test-topic', ret, 'name')
self.assertInSaltComment('AWS SNS topic my-state-test-topic is set to be created.', ret)
self.assertSaltStateChangesEqual(ret, {})
ret = self.run_function('boto_sns.exists', name='my-state-test-topic')
self.assertFalse(ret)
def test_absent_not_exist(self):
ret = self.run_state('boto_sns.absent',
name='my-state-test-topic')
self.assertSaltTrueReturn(ret)
self.assertInSaltReturn('my-state-test-topic', ret, 'name')
self.assertInSaltComment('AWS SNS topic my-state-test-topic does not exist.', ret)
self.assertSaltStateChangesEqual(ret, {})
def test_absent_already_exists(self):
self.run_state('boto_sns.present',
name='my-state-test-topic')
ret = self.run_state('boto_sns.absent',
name='my-state-test-topic')
self.assertSaltTrueReturn(ret)
self.assertInSaltReturn('my-state-test-topic', ret, 'name')
self.assertInSaltComment('AWS SNS topic my-state-test-topic does not exist.', ret)
self.assertSaltStateChangesEqual(ret, {'new': None, 'old': {'topic': 'my-state-test-topic'}})
def test_absent_test_mode(self):
self.run_state('boto_sns.present',
name='my-state-test-topic')
ret = self.run_state('boto_sns.absent',
name='my-state-test-topic',
test=True)
self.assertSaltNoneReturn(ret)
self.assertInSaltReturn('my-state-test-topic', ret, 'name')
self.assertInSaltComment('AWS SNS topic my-state-test-topic is set to be removed.', ret)
self.assertSaltStateChangesEqual(ret, {})
ret = self.run_function('boto_sns.exists', name='my-state-test-topic')
self.assertTrue(ret)
if __name__ == '__main__':
from integration import run_tests
run_tests(BotoSNSTest, needs_daemon=False)