mirror of
https://github.com/valitydev/salt.git
synced 2024-11-07 08:58:59 +00:00
Merge pull request #29573 from kraney/boto_lambda
An implementation of support for AWS Lambda
This commit is contained in:
commit
3514765d33
782
salt/modules/boto_lambda.py
Normal file
782
salt/modules/boto_lambda.py
Normal file
@ -0,0 +1,782 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
Connection module for Amazon Lambda
|
||||
|
||||
.. versionadded:: Boron
|
||||
|
||||
:configuration: This module accepts explicit Lambda credentials but can also
|
||||
utilize IAM roles assigned to the instance trough Instance Profiles.
|
||||
Dynamic credentials are then automatically obtained from AWS API and no
|
||||
further configuration is necessary. More Information available at:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
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 or
|
||||
in the minion's config file:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
lambda.keyid: GKTADJGHEIQSXMKKRBJ08H
|
||||
lambda.key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
|
||||
|
||||
A region may also be specified in the configuration:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
lambda.region: us-east-1
|
||||
|
||||
If a region is not specified, the default is us-east-1.
|
||||
|
||||
It's also possible to specify key, keyid and region via a profile, either
|
||||
as a passed in dict, or as a string to pull from pillars or minion config:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
myprofile:
|
||||
keyid: GKTADJGHEIQSXMKKRBJ08H
|
||||
key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
|
||||
region: us-east-1
|
||||
|
||||
.. versionchanged:: 2015.8.0
|
||||
All methods now return a dictionary. Create and delete methods return:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
created: true
|
||||
|
||||
or
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
created: false
|
||||
error:
|
||||
message: error message
|
||||
|
||||
Request methods (e.g., `describe_function`) return:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
function:
|
||||
- {...}
|
||||
- {...}
|
||||
|
||||
or
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
error:
|
||||
message: error message
|
||||
|
||||
:depends: boto3
|
||||
|
||||
'''
|
||||
# keep lint from choking on _get_conn and _cache_id
|
||||
#pylint: disable=E0602
|
||||
|
||||
# Import Python libs
|
||||
from __future__ import absolute_import
|
||||
import logging
|
||||
from distutils.version import LooseVersion as _LooseVersion # pylint: disable=import-error,no-name-in-module
|
||||
|
||||
# Import Salt libs
|
||||
import salt.utils.boto3
|
||||
import salt.utils.compat
|
||||
import salt.utils
|
||||
from salt.exceptions import SaltInvocationError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Import third party libs
|
||||
|
||||
# pylint: disable=import-error
|
||||
try:
|
||||
#pylint: disable=unused-import
|
||||
import boto
|
||||
import boto3
|
||||
#pylint: enable=unused-import
|
||||
from botocore.exceptions import ClientError
|
||||
logging.getLogger('boto').setLevel(logging.CRITICAL)
|
||||
logging.getLogger('boto3').setLevel(logging.CRITICAL)
|
||||
HAS_BOTO = True
|
||||
except ImportError:
|
||||
HAS_BOTO = False
|
||||
# pylint: enable=import-error
|
||||
|
||||
|
||||
def __virtual__():
|
||||
'''
|
||||
Only load if boto libraries exist and if boto libraries are greater than
|
||||
a given version.
|
||||
'''
|
||||
required_boto_version = '2.8.0'
|
||||
required_boto3_version = '1.2.1'
|
||||
# the boto_lambda execution module relies on the connect_to_region() method
|
||||
# which was added in boto 2.8.0
|
||||
# https://github.com/boto/boto/commit/33ac26b416fbb48a60602542b4ce15dcc7029f12
|
||||
if not HAS_BOTO:
|
||||
return False
|
||||
elif _LooseVersion(boto.__version__) < _LooseVersion(required_boto_version):
|
||||
return False
|
||||
elif _LooseVersion(boto3.__version__) < _LooseVersion(required_boto3_version):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def __init__(opts):
|
||||
salt.utils.compat.pack_dunder(__name__)
|
||||
if HAS_BOTO:
|
||||
__utils__['boto3.assign_funcs'](__name__, 'lambda')
|
||||
|
||||
|
||||
def _find_function(name,
|
||||
region=None, key=None, keyid=None, profile=None):
|
||||
|
||||
'''
|
||||
Given function name, find and return matching Lambda information.
|
||||
'''
|
||||
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
|
||||
|
||||
for funcs in salt.utils.boto3.paged_call(conn.list_functions):
|
||||
for func in funcs['Functions']:
|
||||
if func['FunctionName'] == name:
|
||||
return func
|
||||
return None
|
||||
|
||||
|
||||
def function_exists(FunctionName, region=None, key=None,
|
||||
keyid=None, profile=None):
|
||||
'''
|
||||
Given a function name, check to see if the given function name exists.
|
||||
|
||||
Returns True if the given function exists and returns False if the given
|
||||
function does not exist.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt myminion boto_lambda.function_exists myfunction
|
||||
|
||||
'''
|
||||
|
||||
try:
|
||||
func = _find_function(FunctionName,
|
||||
region=region, key=key, keyid=keyid, profile=profile)
|
||||
return {'exists': bool(func)}
|
||||
except ClientError as e:
|
||||
return {'error': salt.utils.boto3.get_error(e)}
|
||||
|
||||
|
||||
def _get_role_arn(name, region=None, key=None, keyid=None, profile=None):
|
||||
if name.startswith('arn:aws:iam:'):
|
||||
return name
|
||||
|
||||
account_id = __salt__['boto_iam.get_account_id'](
|
||||
region=region, key=key, keyid=keyid, profile=profile
|
||||
)
|
||||
return 'arn:aws:iam::{0}:role/{1}'.format(account_id, name)
|
||||
|
||||
|
||||
def _filedata(infile):
|
||||
with salt.utils.fopen(infile, 'rb') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def create_function(FunctionName, Runtime, Role, Handler, ZipFile=None,
|
||||
S3Bucket=None, S3Key=None, S3ObjectVersion=None,
|
||||
Description="", Timeout=3, MemorySize=128, Publish=False,
|
||||
region=None, key=None, keyid=None, profile=None):
|
||||
'''
|
||||
Given a valid config, create a function.
|
||||
|
||||
Returns {created: true} if the function was created and returns
|
||||
{created: False} if the function was not created.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt myminion boto_lamba.create_function my_function python2.7 my_role my_file.my_function my_function.zip
|
||||
|
||||
'''
|
||||
|
||||
role_arn = _get_role_arn(Role, region=region, key=key, keyid=keyid, profile=profile)
|
||||
try:
|
||||
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
|
||||
if ZipFile:
|
||||
if S3Bucket or S3Key or S3ObjectVersion:
|
||||
raise SaltInvocationError('Either ZipFile must be specified, or '
|
||||
'S3Bucket and S3Key must be provided.')
|
||||
code = {
|
||||
'ZipFile': _filedata(ZipFile),
|
||||
}
|
||||
else:
|
||||
if not S3Bucket or not S3Key:
|
||||
raise SaltInvocationError('Either ZipFile must be specified, or '
|
||||
'S3Bucket and S3Key must be provided.')
|
||||
code = {
|
||||
'S3Bucket': S3Bucket,
|
||||
'S3Key': S3Key,
|
||||
}
|
||||
if S3ObjectVersion:
|
||||
code['S3ObjectVersion'] = S3ObjectVersion
|
||||
func = conn.create_function(FunctionName=FunctionName, Runtime=Runtime, Role=role_arn, Handler=Handler,
|
||||
Code=code, Description=Description, Timeout=Timeout, MemorySize=MemorySize,
|
||||
Publish=Publish)
|
||||
if func:
|
||||
log.info('The newly created function name is {0}'.format(func['FunctionName']))
|
||||
|
||||
return {'created': True, 'name': func['FunctionName']}
|
||||
else:
|
||||
log.warning('Function was not created')
|
||||
return {'created': False}
|
||||
except ClientError as e:
|
||||
return {'created': False, 'error': salt.utils.boto3.get_error(e)}
|
||||
|
||||
|
||||
def delete_function(FunctionName, Qualifier=None, region=None, key=None, keyid=None, profile=None):
|
||||
'''
|
||||
Given a function name and optional version qualifier, delete it.
|
||||
|
||||
Returns {deleted: true} if the function was deleted and returns
|
||||
{deleted: false} if the function was not deleted.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt myminion boto_lambda.delete_function myfunction
|
||||
|
||||
'''
|
||||
|
||||
try:
|
||||
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
|
||||
if Qualifier:
|
||||
r = conn.delete_function(FunctionName=FunctionName, Qualifier=Qualifier)
|
||||
else:
|
||||
r = conn.delete_function(FunctionName=FunctionName)
|
||||
return {'deleted': True}
|
||||
except ClientError as e:
|
||||
return {'deleted': False, 'error': salt.utils.boto3.get_error(e)}
|
||||
|
||||
|
||||
def describe_function(FunctionName, region=None, key=None,
|
||||
keyid=None, profile=None):
|
||||
'''
|
||||
Given a function name describe its properties.
|
||||
|
||||
Returns a dictionary of interesting properties.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt myminion boto_lambda.describe_function myfunction
|
||||
|
||||
'''
|
||||
|
||||
try:
|
||||
func = _find_function(FunctionName,
|
||||
region=region, key=key, keyid=keyid, profile=profile)
|
||||
if func:
|
||||
keys = ('FunctionName', 'Runtime', 'Role', 'Handler', 'CodeSha256',
|
||||
'CodeSize', 'Description', 'Timeout', 'MemorySize', 'FunctionArn',
|
||||
'LastModified')
|
||||
return {'function': dict([(k, func.get(k)) for k in keys])}
|
||||
else:
|
||||
return {'function': None}
|
||||
except ClientError as e:
|
||||
return {'error': salt.utils.boto3.get_error(e)}
|
||||
|
||||
|
||||
def update_function_config(FunctionName, Role=None, Handler=None,
|
||||
Description=None, Timeout=None, MemorySize=None,
|
||||
region=None, key=None, keyid=None, profile=None):
|
||||
'''
|
||||
Update the named lambda function to the configuration.
|
||||
|
||||
Returns {updated: true} if the function was updated and returns
|
||||
{updated: False} if the function was not updated.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt myminion boto_lamba.update_function_config my_function my_role my_file.my_function "my lambda function"
|
||||
|
||||
'''
|
||||
|
||||
args = dict(FunctionName=FunctionName)
|
||||
for val, var in {
|
||||
'Handler': Handler,
|
||||
'Description': Description,
|
||||
'Timeout': Timeout,
|
||||
'MemorySize': MemorySize,
|
||||
}.iteritems():
|
||||
if var:
|
||||
args[val] = var
|
||||
if Role:
|
||||
role_arn = _get_role_arn(Role, region, key, keyid, profile)
|
||||
args['Role'] = role_arn
|
||||
try:
|
||||
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
|
||||
r = conn.update_function_configuration(*args)
|
||||
if r:
|
||||
keys = ('FunctionName', 'Runtime', 'Role', 'Handler', 'CodeSha256',
|
||||
'CodeSize', 'Description', 'Timeout', 'MemorySize', 'FunctionArn',
|
||||
'LastModified')
|
||||
return {'updated': True, 'function': dict([(k, r.get(k)) for k in keys])}
|
||||
else:
|
||||
log.warning('Function was not updated')
|
||||
return {'updated': False}
|
||||
except ClientError as e:
|
||||
return {'updated': False, 'error': salt.utils.boto3.get_error(e)}
|
||||
|
||||
|
||||
def update_function_code(FunctionName, ZipFile=None, S3Bucket=None, S3Key=None,
|
||||
S3ObjectVersion=None, Publish=False,
|
||||
region=None, key=None, keyid=None, profile=None):
|
||||
'''
|
||||
Upload the given code to the named lambda function.
|
||||
|
||||
Returns {updated: true} if the function was updated and returns
|
||||
{updated: False} if the function was not updated.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt myminion boto_lamba.update_function_code my_function ZipFile=function.zip
|
||||
|
||||
'''
|
||||
|
||||
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
|
||||
try:
|
||||
if ZipFile:
|
||||
if S3Bucket or S3Key or S3ObjectVersion:
|
||||
raise SaltInvocationError('Either ZipFile must be specified, or '
|
||||
'S3Bucket and S3Key must be provided.')
|
||||
r = conn.update_function_code(FunctionName=FunctionName,
|
||||
ZipFile=_filedata(ZipFile),
|
||||
Publish=Publish)
|
||||
else:
|
||||
if not S3Bucket or not S3Key:
|
||||
raise SaltInvocationError('Either ZipFile must be specified, or '
|
||||
'S3Bucket and S3Key must be provided.')
|
||||
args = {
|
||||
'S3Bucket': S3Bucket,
|
||||
'S3Key': S3Key,
|
||||
}
|
||||
if S3ObjectVersion:
|
||||
args['S3ObjectVersion'] = S3ObjectVersion
|
||||
r = conn.update_function_code(FunctionName=FunctionName,
|
||||
Publish=Publish, **args)
|
||||
if r:
|
||||
keys = ('FunctionName', 'Runtime', 'Role', 'Handler', 'CodeSha256',
|
||||
'CodeSize', 'Description', 'Timeout', 'MemorySize', 'FunctionArn',
|
||||
'LastModified')
|
||||
return {'updated': True, 'function': dict([(k, r.get(k)) for k in keys])}
|
||||
else:
|
||||
log.warning('Function was not updated')
|
||||
return {'updated': False}
|
||||
except ClientError as e:
|
||||
return {'updated': False, 'error': salt.utils.boto3.get_error(e)}
|
||||
|
||||
|
||||
def list_function_versions(FunctionName,
|
||||
region=None, key=None, keyid=None, profile=None):
|
||||
'''
|
||||
List the versions available for the given function.
|
||||
|
||||
Returns list of function versions
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
versions:
|
||||
- {...}
|
||||
- {...}
|
||||
|
||||
'''
|
||||
try:
|
||||
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
|
||||
vers = []
|
||||
for ret in salt.utils.boto3.paged_call(conn.list_versions_by_function,
|
||||
FunctionName=FunctionName):
|
||||
vers.extend(ret['Versions'])
|
||||
if not bool(vers):
|
||||
log.warning('No versions found')
|
||||
return {'Versions': vers}
|
||||
except ClientError as e:
|
||||
return {'error': salt.utils.boto3.get_error(e)}
|
||||
|
||||
|
||||
def create_alias(FunctionName, Name, FunctionVersion, Description="",
|
||||
region=None, key=None, keyid=None, profile=None):
|
||||
'''
|
||||
Given a valid config, create an alias to a function.
|
||||
|
||||
Returns {created: true} if the alias was created and returns
|
||||
{created: False} if the alias was not created.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt myminion boto_lamba.create_alias my_function my_alias $LATEST "An alias"
|
||||
|
||||
'''
|
||||
try:
|
||||
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
|
||||
alias = conn.create_alias(FunctionName=FunctionName, Name=Name,
|
||||
FunctionVersion=FunctionVersion, Description=Description)
|
||||
if alias:
|
||||
log.info('The newly created alias name is {0}'.format(alias['Name']))
|
||||
|
||||
return {'created': True, 'name': alias['Name']}
|
||||
else:
|
||||
log.warning('Alias was not created')
|
||||
return {'created': False}
|
||||
except ClientError as e:
|
||||
return {'created': False, 'error': salt.utils.boto3.get_error(e)}
|
||||
|
||||
|
||||
def delete_alias(FunctionName, Name, region=None, key=None, keyid=None, profile=None):
|
||||
'''
|
||||
Given a function name and alias name, delete the alias.
|
||||
|
||||
Returns {deleted: true} if the alias was deleted and returns
|
||||
{deleted: false} if the alias was not deleted.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt myminion boto_lambda.delete_alias myfunction myalias
|
||||
|
||||
'''
|
||||
|
||||
try:
|
||||
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
|
||||
conn.delete_alias(FunctionName=FunctionName, Name=Name)
|
||||
return {'deleted': True}
|
||||
except ClientError as e:
|
||||
return {'deleted': False, 'error': salt.utils.boto3.get_error(e)}
|
||||
|
||||
|
||||
def _find_alias(FunctionName, Name, FunctionVersion=None,
|
||||
region=None, key=None, keyid=None, profile=None):
|
||||
|
||||
'''
|
||||
Given function name and alias name, find and return matching alias information.
|
||||
'''
|
||||
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
|
||||
|
||||
args = {
|
||||
'FunctionName': FunctionName
|
||||
}
|
||||
if FunctionVersion:
|
||||
args['FunctionVersion'] = FunctionVersion
|
||||
|
||||
for aliases in salt.utils.boto3.paged_call(conn.list_aliases, *args):
|
||||
for alias in aliases.get('Aliases'):
|
||||
if alias['Name'] == Name:
|
||||
return alias
|
||||
return None
|
||||
|
||||
|
||||
def alias_exists(FunctionName, Name, region=None, key=None,
|
||||
keyid=None, profile=None):
|
||||
'''
|
||||
Given a function name and alias name, check to see if the given alias exists.
|
||||
|
||||
Returns True if the given alias exists and returns False if the given
|
||||
alias does not exist.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt myminion boto_lambda.alias_exists myfunction myalias
|
||||
|
||||
'''
|
||||
|
||||
try:
|
||||
alias = _find_alias(FunctionName, Name,
|
||||
region=region, key=key, keyid=keyid, profile=profile)
|
||||
return {'exists': bool(alias)}
|
||||
except ClientError as e:
|
||||
return {'error': salt.utils.boto3.get_error(e)}
|
||||
|
||||
|
||||
def describe_alias(FunctionName, Name, region=None, key=None,
|
||||
keyid=None, profile=None):
|
||||
'''
|
||||
Given a function name and alias name describe the properties of the alias.
|
||||
|
||||
Returns a dictionary of interesting properties.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt myminion boto_lambda.describe_alias myalias
|
||||
|
||||
'''
|
||||
|
||||
try:
|
||||
alias = _find_alias(FunctionName, Name,
|
||||
region=region, key=key, keyid=keyid, profile=profile)
|
||||
if alias:
|
||||
keys = ('AliasArn', 'Name', 'FunctionVersion', 'Description')
|
||||
return {'alias': dict([(k, alias.get(k)) for k in keys])}
|
||||
else:
|
||||
return {'alias': None}
|
||||
except ClientError as e:
|
||||
return {'error': salt.utils.boto3.get_error(e)}
|
||||
|
||||
|
||||
def update_alias(FunctionName, Name, FunctionVersion=None, Description=None,
|
||||
region=None, key=None, keyid=None, profile=None):
|
||||
'''
|
||||
Update the named alias to the configuration.
|
||||
|
||||
Returns {updated: true} if the alias was updated and returns
|
||||
{updated: False} if the alias was not updated.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt myminion boto_lamba.update_alias my_lambda my_alias $LATEST
|
||||
|
||||
'''
|
||||
|
||||
try:
|
||||
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
|
||||
args = {}
|
||||
if FunctionVersion:
|
||||
args['FunctionVersion'] = FunctionVersion
|
||||
if Description:
|
||||
args['Description'] = Description
|
||||
r = conn.update_alias(FunctionName=FunctionName, Name=Name, **args)
|
||||
if r:
|
||||
keys = ('Name', 'FunctionVersion', 'Description')
|
||||
return {'updated': True, 'alias': dict([(k, r.get(k)) for k in keys])}
|
||||
else:
|
||||
log.warning('Alias was not updated')
|
||||
return {'updated': False}
|
||||
except ClientError as e:
|
||||
return {'created': False, 'error': salt.utils.boto3.get_error(e)}
|
||||
|
||||
|
||||
def create_event_source_mapping(EventSourceArn, FunctionName, StartingPosition,
|
||||
Enabled=True, BatchSize=100,
|
||||
region=None, key=None, keyid=None, profile=None):
|
||||
'''
|
||||
Identifies a stream as an event source for a Lambda function. It can be
|
||||
either an Amazon Kinesis stream or an Amazon DynamoDB stream. AWS Lambda
|
||||
invokes the specified function when records are posted to the stream.
|
||||
|
||||
Returns {created: true} if the event source mapping was created and returns
|
||||
{created: False} if the event source mapping was not created.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt myminion boto_lamba.create_event_source_mapping arn::::eventsource myfunction LATEST
|
||||
|
||||
'''
|
||||
try:
|
||||
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
|
||||
obj = conn.create_event_source_mapping(EventSourceArn=EventSourceArn,
|
||||
FunctionName=FunctionName,
|
||||
Enabled=Enabled,
|
||||
BatchSize=BatchSize,
|
||||
StartingPosition=StartingPosition)
|
||||
if obj:
|
||||
log.info('The newly created event source mapping ID is {0}'.format(obj['UUID']))
|
||||
|
||||
return {'created': True, 'id': obj['UUID']}
|
||||
else:
|
||||
log.warning('Event source mapping was not created')
|
||||
return {'created': False}
|
||||
except ClientError as e:
|
||||
return {'created': False, 'error': salt.utils.boto3.get_error(e)}
|
||||
|
||||
|
||||
def get_event_source_mapping_ids(EventSourceArn, FunctionName,
|
||||
region=None, key=None, keyid=None, profile=None):
|
||||
'''
|
||||
Given an event source and function name, return a list of mapping IDs
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt myminion boto_lambda.get_event_source_mapping_ids arn:::: myfunction
|
||||
|
||||
'''
|
||||
|
||||
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
|
||||
try:
|
||||
mappings = []
|
||||
for maps in salt.utils.boto3.paged_call(conn.list_event_source_mappings,
|
||||
EventSourceArn=EventSourceArn,
|
||||
FunctionName=FunctionName):
|
||||
mappings.extend([mapping['UUID'] for mapping in maps['EventSourceMappings']])
|
||||
return mappings
|
||||
except ClientError as e:
|
||||
return {'error': salt.utils.boto3.get_error(e)}
|
||||
|
||||
|
||||
def _get_ids(UUID=None, EventSourceArn=None, FunctionName=None,
|
||||
region=None, key=None, keyid=None, profile=None):
|
||||
if UUID:
|
||||
if EventSourceArn or FunctionName:
|
||||
raise SaltInvocationError('Either UUID must be specified, or '
|
||||
'EventSourceArn and FunctionName must be provided.')
|
||||
return [UUID]
|
||||
else:
|
||||
if not EventSourceArn or not FunctionName:
|
||||
raise SaltInvocationError('Either UUID must be specified, or '
|
||||
'EventSourceArn and FunctionName must be provided.')
|
||||
return get_event_source_mapping_ids(EventSourceArn=EventSourceArn,
|
||||
FunctionName=FunctionName,
|
||||
region=region, key=key, keyid=keyid, profile=profile)
|
||||
|
||||
|
||||
def delete_event_source_mapping(UUID=None, EventSourceArn=None, FunctionName=None,
|
||||
region=None, key=None, keyid=None, profile=None):
|
||||
'''
|
||||
Given an event source mapping ID or an event source ARN and FunctionName,
|
||||
delete the event source mapping
|
||||
|
||||
Returns {deleted: true} if the mapping was deleted and returns
|
||||
{deleted: false} if the mapping was not deleted.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt myminion boto_lambda.delete_event_source_mapping 260c423d-e8b5-4443-8d6a-5e91b9ecd0fa
|
||||
|
||||
'''
|
||||
ids = _get_ids(UUID, EventSourceArn=EventSourceArn,
|
||||
FunctionName=FunctionName)
|
||||
try:
|
||||
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
|
||||
for id in ids:
|
||||
conn.delete_event_source_mapping(UUID=id)
|
||||
return {'deleted': True}
|
||||
except ClientError as e:
|
||||
return {'deleted': False, 'error': salt.utils.boto3.get_error(e)}
|
||||
|
||||
|
||||
def event_source_mapping_exists(UUID=None, EventSourceArn=None,
|
||||
FunctionName=None,
|
||||
region=None, key=None, keyid=None, profile=None):
|
||||
'''
|
||||
Given an event source mapping ID or an event source ARN and FunctionName,
|
||||
check whether the mapping exists.
|
||||
|
||||
Returns True if the given alias exists and returns False if the given
|
||||
alias does not exist.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt myminion boto_lambda.alias_exists myfunction myalias
|
||||
|
||||
'''
|
||||
|
||||
desc = describe_event_source_mapping(UUID=UUID,
|
||||
EventSourceArn=EventSourceArn,
|
||||
FunctionName=FunctionName,
|
||||
region=region, key=key,
|
||||
keyid=keyid, profile=profile)
|
||||
if 'error' in desc:
|
||||
return desc
|
||||
return {'exists': bool(desc.get('event_source_mapping'))}
|
||||
|
||||
|
||||
def describe_event_source_mapping(UUID=None, EventSourceArn=None,
|
||||
FunctionName=None,
|
||||
region=None, key=None, keyid=None, profile=None):
|
||||
'''
|
||||
Given an event source mapping ID or an event source ARN and FunctionName,
|
||||
obtain the current settings of that mapping.
|
||||
|
||||
Returns a dictionary of interesting properties.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt myminion boto_lambda.describe_event_source_mapping uuid
|
||||
|
||||
'''
|
||||
|
||||
ids = _get_ids(UUID, EventSourceArn=EventSourceArn,
|
||||
FunctionName=FunctionName)
|
||||
if len(ids) < 1:
|
||||
return {'event_source_mapping': None}
|
||||
|
||||
UUID = ids[0]
|
||||
try:
|
||||
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
|
||||
desc = conn.get_event_source_mapping(UUID=UUID)
|
||||
if desc:
|
||||
keys = ('UUID', 'BatchSize', 'EventSourceArn',
|
||||
'FunctionArn', 'LastModified', 'LastProcessingResult',
|
||||
'State', 'StateTransitionReason')
|
||||
return {'event_source_mapping': dict([(k, desc.get(k)) for k in keys])}
|
||||
else:
|
||||
return {'event_source_mapping': None}
|
||||
except ClientError as e:
|
||||
return {'error': salt.utils.boto3.get_error(e)}
|
||||
|
||||
|
||||
def update_event_source_mapping(UUID,
|
||||
FunctionName=None, Enabled=None, BatchSize=None,
|
||||
region=None, key=None, keyid=None, profile=None):
|
||||
'''
|
||||
Update the event source mapping identified by the UUID.
|
||||
|
||||
Returns {updated: true} if the alias was updated and returns
|
||||
{updated: False} if the alias was not updated.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt myminion boto_lamba.update_event_source_mapping uuid FunctionName=new_function
|
||||
|
||||
'''
|
||||
|
||||
try:
|
||||
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
|
||||
args = {}
|
||||
if FunctionName is not None:
|
||||
args['FunctionName'] = FunctionName
|
||||
if Enabled is not None:
|
||||
args['Enabled'] = Enabled
|
||||
if BatchSize is not None:
|
||||
args['BatchSize'] = BatchSize
|
||||
r = conn.update_event_source_mapping(UUID=UUID, **args)
|
||||
if r:
|
||||
keys = ('UUID', 'BatchSize', 'EventSourceArn',
|
||||
'FunctionArn', 'LastModified', 'LastProcessingResult',
|
||||
'State', 'StateTransitionReason')
|
||||
return {'updated': True, 'event_source_mapping': dict([(k, r.get(k)) for k in keys])}
|
||||
else:
|
||||
log.warning('Mapping was not updated')
|
||||
return {'updated': False}
|
||||
except ClientError as e:
|
||||
return {'created': False, 'error': salt.utils.boto3.get_error(e)}
|
741
salt/states/boto_lambda.py
Normal file
741
salt/states/boto_lambda.py
Normal file
@ -0,0 +1,741 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
Manage Lambda Functions
|
||||
=================
|
||||
|
||||
.. versionadded:: Boron
|
||||
|
||||
Create and destroy Lambda Functions. Be aware that this interacts with Amazon's services,
|
||||
and so may incur charges.
|
||||
|
||||
This module uses ``boto3``, which can be installed via package, or pip.
|
||||
|
||||
This module accepts explicit vpc 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
|
||||
|
||||
vpc.keyid: GKTADJGHEIQSXMKKRBJ08H
|
||||
vpc.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
|
||||
|
||||
Ensure function exists:
|
||||
boto_lambda.function_present:
|
||||
- FunctionName: myfunction
|
||||
- Runtime: python2.7
|
||||
- Role: iam_role_name
|
||||
- Handler: entry_function
|
||||
- ZipFile: code.zip
|
||||
- S3Bucket: bucketname
|
||||
- S3Key: keyname
|
||||
- S3ObjectVersion: version
|
||||
- Description: "My Lambda Function"
|
||||
- Timeout: 3
|
||||
- MemorySize: 128
|
||||
- region: us-east-1
|
||||
- keyid: GKTADJGHEIQSXMKKRBJ08H
|
||||
- key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
|
||||
|
||||
'''
|
||||
|
||||
# Import Python Libs
|
||||
from __future__ import absolute_import
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import hashlib
|
||||
|
||||
# Import Salt Libs
|
||||
import salt.utils.dictupdate as dictupdate
|
||||
import salt.utils
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def __virtual__():
|
||||
'''
|
||||
Only load if boto is available.
|
||||
'''
|
||||
return 'boto_lambda' if 'boto_lambda.function_exists' in __salt__ else False
|
||||
|
||||
|
||||
def function_present(name, FunctionName, Runtime, Role, Handler, ZipFile=None, S3Bucket=None,
|
||||
S3Key=None, S3ObjectVersion=None,
|
||||
Description='', Timeout=3, MemorySize=128,
|
||||
region=None, key=None, keyid=None, profile=None):
|
||||
'''
|
||||
Ensure function exists.
|
||||
|
||||
name
|
||||
The name of the state definition
|
||||
|
||||
FunctionName
|
||||
Name of the Function.
|
||||
|
||||
Runtime
|
||||
The Runtime environment for the function. One of
|
||||
'nodejs', 'java8', or 'python2.7'
|
||||
|
||||
Role
|
||||
The name or ARN of the IAM role that the function assumes when it executes your
|
||||
function to access any other AWS resources.
|
||||
|
||||
Handler
|
||||
The function within your code that Lambda calls to begin execution. For Node.js it is the
|
||||
module-name.*export* value in your function. For Java, it can be package.classname::handler or
|
||||
package.class-name.
|
||||
|
||||
ZipFile
|
||||
A path to a .zip file containing your deployment package. If this is
|
||||
specified, S3Bucket and S3Key must not be specified.
|
||||
|
||||
S3Bucket
|
||||
Amazon S3 bucket name where the .zip file containing your package is
|
||||
stored. If this is specified, S3Key must be specified and ZipFile must
|
||||
NOT be specified.
|
||||
|
||||
S3Key
|
||||
The Amazon S3 object (the deployment package) key name you want to
|
||||
upload. If this is specified, S3Key must be specified and ZipFile must
|
||||
NOT be specified.
|
||||
|
||||
S3ObjectVersion
|
||||
The version of S3 object to use. Optional, should only be specified if
|
||||
S3Bucket and S3Key are specified.
|
||||
|
||||
Description
|
||||
A short, user-defined function description. Lambda does not use this value. Assign a meaningful
|
||||
description as you see fit.
|
||||
|
||||
Timeout
|
||||
The function execution time at which Lambda should terminate this function. Because the execution
|
||||
time has cost implications, we recommend you set this value based on your expected execution time.
|
||||
The default is 3 seconds.
|
||||
|
||||
MemorySize
|
||||
The amount of memory, in MB, your function is given. Lambda uses this memory size to infer
|
||||
the amount of CPU and memory allocated to your function. Your function use-case determines your
|
||||
CPU and memory requirements. For example, a database operation might need less memory compared
|
||||
to an image processing function. The default value is 128 MB. The value must be a multiple of
|
||||
64 MB.
|
||||
|
||||
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': FunctionName,
|
||||
'result': True,
|
||||
'comment': '',
|
||||
'changes': {}
|
||||
}
|
||||
|
||||
r = __salt__['boto_lambda.function_exists'](FunctionName=FunctionName, region=region,
|
||||
key=key, keyid=keyid, profile=profile)
|
||||
|
||||
if 'error' in r:
|
||||
ret['result'] = False
|
||||
ret['comment'] = 'Failed to create function: {0}.'.format(r['error']['message'])
|
||||
return ret
|
||||
|
||||
if not r.get('exists'):
|
||||
if __opts__['test']:
|
||||
ret['comment'] = 'Function {0} is set to be created.'.format(FunctionName)
|
||||
ret['result'] = None
|
||||
return ret
|
||||
r = __salt__['boto_lambda.create_function'](FunctionName=FunctionName, Runtime=Runtime,
|
||||
Role=Role, Handler=Handler,
|
||||
ZipFile=ZipFile, S3Bucket=S3Bucket,
|
||||
S3Key=S3Key,
|
||||
S3ObjectVersion=S3ObjectVersion,
|
||||
Description=Description,
|
||||
Timeout=Timeout, MemorySize=MemorySize,
|
||||
region=region, key=key,
|
||||
keyid=keyid, profile=profile)
|
||||
if not r.get('created'):
|
||||
ret['result'] = False
|
||||
ret['comment'] = 'Failed to create function: {0}.'.format(r['error']['message'])
|
||||
return ret
|
||||
_describe = __salt__['boto_lambda.describe_function'](FunctionName, region=region, key=key,
|
||||
keyid=keyid, profile=profile)
|
||||
ret['changes']['old'] = {'function': None}
|
||||
ret['changes']['new'] = _describe
|
||||
ret['comment'] = 'Function {0} created.'.format(FunctionName)
|
||||
return ret
|
||||
|
||||
ret['comment'] = os.linesep.join([ret['comment'], 'Function {0} is present.'.format(FunctionName)])
|
||||
ret['changes'] = {}
|
||||
# function exists, ensure config matches
|
||||
_ret = _function_config_present(FunctionName, Role, Handler, Description, Timeout,
|
||||
MemorySize, region, key, keyid, profile)
|
||||
if not _ret.get('result'):
|
||||
ret['result'] = False
|
||||
ret['comment'] = _ret['comment']
|
||||
ret['changes'] = {}
|
||||
return ret
|
||||
ret['changes'] = dictupdate.update(ret['changes'], _ret['changes'])
|
||||
ret['comment'] = ' '.join([ret['comment'], _ret['comment']])
|
||||
_ret = _function_code_present(FunctionName, ZipFile, S3Bucket, S3Key, S3ObjectVersion,
|
||||
region, key, keyid, profile)
|
||||
if not _ret.get('result'):
|
||||
ret['result'] = False
|
||||
ret['comment'] = _ret['comment']
|
||||
ret['changes'] = {}
|
||||
return ret
|
||||
ret['changes'] = dictupdate.update(ret['changes'], _ret['changes'])
|
||||
ret['comment'] = ' '.join([ret['comment'], _ret['comment']])
|
||||
return ret
|
||||
|
||||
|
||||
def _get_role_arn(name, region=None, key=None, keyid=None, profile=None):
|
||||
if name.startswith('arn:aws:iam:'):
|
||||
return name
|
||||
|
||||
account_id = __salt__['boto_iam.get_account_id'](
|
||||
region=region, key=key, keyid=keyid, profile=profile
|
||||
)
|
||||
return 'arn:aws:iam::{0}:role/{1}'.format(account_id, name)
|
||||
|
||||
|
||||
def _function_config_present(FunctionName, Role, Handler, Description, Timeout,
|
||||
MemorySize, region, key, keyid, profile):
|
||||
ret = {'result': True, 'comment': '', 'changes': {}}
|
||||
func = __salt__['boto_lambda.describe_function'](FunctionName,
|
||||
region=region, key=key, keyid=keyid, profile=profile)['function']
|
||||
role_arn = _get_role_arn(Role, region, key, keyid, profile)
|
||||
need_update = False
|
||||
for val, var in {
|
||||
'Role': 'role_arn',
|
||||
'Handler': 'Handler',
|
||||
'Description': 'Description',
|
||||
'Timeout': 'Timeout',
|
||||
'MemorySize': 'MemorySize',
|
||||
}.iteritems():
|
||||
if func[val] != locals()[var]:
|
||||
need_update = True
|
||||
ret['changes'].setdefault('new', {})[var] = locals()[var]
|
||||
ret['changes'].setdefault('old', {})[var] = func[val]
|
||||
if need_update:
|
||||
ret['comment'] = os.linesep.join([ret['comment'], 'Function config to be modified'])
|
||||
if __opts__['test']:
|
||||
msg = 'Function {0} set to be modified.'.format(FunctionName)
|
||||
ret['comment'] = msg
|
||||
ret['result'] = None
|
||||
return ret
|
||||
_r = __salt__['boto_lambda.update_function_config'](FunctionName=FunctionName,
|
||||
Role=Role, Handler=Handler, Description=Description,
|
||||
Timeout=Timeout, MemorySize=MemorySize,
|
||||
region=region, key=key,
|
||||
keyid=keyid, profile=profile)
|
||||
if not _r.get('updated'):
|
||||
ret['result'] = False
|
||||
ret['comment'] = 'Failed to update function: {0}.'.format(_r['error']['message'])
|
||||
ret['changes'] = {}
|
||||
return ret
|
||||
|
||||
|
||||
def _function_code_present(FunctionName, ZipFile, S3Bucket, S3Key, S3ObjectVersion,
|
||||
region, key, keyid, profile):
|
||||
ret = {'result': True, 'comment': '', 'changes': {}}
|
||||
func = __salt__['boto_lambda.describe_function'](FunctionName,
|
||||
region=region, key=key, keyid=keyid, profile=profile)['function']
|
||||
update = False
|
||||
if ZipFile:
|
||||
size = os.path.getsize(ZipFile)
|
||||
if size == func['CodeSize']:
|
||||
sha = hashlib.sha256()
|
||||
with salt.utils.fopen(ZipFile, 'rb') as f:
|
||||
sha.update(f.read())
|
||||
hashed = sha.digest().encode('base64').strip()
|
||||
if hashed != func['CodeSha256']:
|
||||
update = True
|
||||
else:
|
||||
update = True
|
||||
else:
|
||||
# No way to judge whether the item in the s3 bucket is current without
|
||||
# downloading it. Cheaper to just request an update every time, and still
|
||||
# idempotent
|
||||
update = True
|
||||
if update:
|
||||
if __opts__['test']:
|
||||
msg = 'Function {0} set to be modified.'.format(FunctionName)
|
||||
ret['comment'] = msg
|
||||
ret['result'] = None
|
||||
return ret
|
||||
ret['changes']['old'] = {
|
||||
'CodeSha256': func['CodeSha256'],
|
||||
'CodeSize': func['CodeSize'],
|
||||
}
|
||||
func = __salt__['boto_lambda.update_function_code'](FunctionName, ZipFile, S3Bucket,
|
||||
S3Key, S3ObjectVersion,
|
||||
region=region, key=key, keyid=keyid, profile=profile)
|
||||
if not func.get('updated'):
|
||||
ret['result'] = False
|
||||
ret['comment'] = 'Failed to update function: {0}.'.format(func['error']['message'])
|
||||
ret['changes'] = {}
|
||||
return ret
|
||||
func = func['function']
|
||||
if func['CodeSha256'] != ret['changes']['old']['CodeSha256'] or \
|
||||
func['CodeSize'] != ret['changes']['old']['CodeSize']:
|
||||
ret['comment'] = os.linesep.join([ret['comment'], 'Function code to be modified'])
|
||||
ret['changes']['new'] = {
|
||||
'CodeSha256': func['CodeSha256'],
|
||||
'CodeSize': func['CodeSize'],
|
||||
}
|
||||
else:
|
||||
del ret['changes']['old']
|
||||
return ret
|
||||
|
||||
|
||||
def function_absent(name, FunctionName, region=None, key=None, keyid=None, profile=None):
|
||||
'''
|
||||
Ensure function with passed properties is absent.
|
||||
|
||||
name
|
||||
The name of the state definition.
|
||||
|
||||
FunctionName
|
||||
Name of the function.
|
||||
|
||||
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': FunctionName,
|
||||
'result': True,
|
||||
'comment': '',
|
||||
'changes': {}
|
||||
}
|
||||
|
||||
r = __salt__['boto_lambda.function_exists'](FunctionName, region=region,
|
||||
key=key, keyid=keyid, profile=profile)
|
||||
if 'error' in r:
|
||||
ret['result'] = False
|
||||
ret['comment'] = 'Failed to delete function: {0}.'.format(r['error']['message'])
|
||||
return ret
|
||||
|
||||
if r and not r['exists']:
|
||||
ret['comment'] = 'Function {0} does not exist.'.format(FunctionName)
|
||||
return ret
|
||||
|
||||
if __opts__['test']:
|
||||
ret['comment'] = 'Function {0} is set to be removed.'.format(FunctionName)
|
||||
ret['result'] = None
|
||||
return ret
|
||||
r = __salt__['boto_lambda.delete_function'](FunctionName,
|
||||
region=region, key=key,
|
||||
keyid=keyid, profile=profile)
|
||||
if not r['deleted']:
|
||||
ret['result'] = False
|
||||
ret['comment'] = 'Failed to delete function: {0}.'.format(r['error']['message'])
|
||||
return ret
|
||||
ret['changes']['old'] = {'function': FunctionName}
|
||||
ret['changes']['new'] = {'function': None}
|
||||
ret['comment'] = 'Function {0} deleted.'.format(FunctionName)
|
||||
return ret
|
||||
|
||||
|
||||
def alias_present(name, FunctionName, Name, FunctionVersion, Description='',
|
||||
region=None, key=None, keyid=None, profile=None):
|
||||
'''
|
||||
Ensure alias exists.
|
||||
|
||||
name
|
||||
The name of the state definition.
|
||||
|
||||
FunctionName
|
||||
Name of the function for which you want to create an alias.
|
||||
|
||||
Name
|
||||
The name of the alias to be created.
|
||||
|
||||
FunctionVersion
|
||||
Function version for which you are creating the alias.
|
||||
|
||||
Description
|
||||
A short, user-defined function description. Lambda does not use this value. Assign a meaningful
|
||||
description as you see fit.
|
||||
|
||||
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': {}
|
||||
}
|
||||
|
||||
r = __salt__['boto_lambda.alias_exists'](FunctionName=FunctionName, Name=Name, region=region,
|
||||
key=key, keyid=keyid, profile=profile)
|
||||
|
||||
if 'error' in r:
|
||||
ret['result'] = False
|
||||
ret['comment'] = 'Failed to create alias: {0}.'.format(r['error']['message'])
|
||||
return ret
|
||||
|
||||
if not r.get('exists'):
|
||||
if __opts__['test']:
|
||||
ret['comment'] = 'Alias {0} is set to be created.'.format(Name)
|
||||
ret['result'] = None
|
||||
return ret
|
||||
r = __salt__['boto_lambda.create_alias'](FunctionName, Name,
|
||||
FunctionVersion, Description,
|
||||
region, key, keyid, profile)
|
||||
if not r.get('created'):
|
||||
ret['result'] = False
|
||||
ret['comment'] = 'Failed to create alias: {0}.'.format(r['error']['message'])
|
||||
return ret
|
||||
_describe = __salt__['boto_lambda.describe_alias'](FunctionName, Name, region=region, key=key,
|
||||
keyid=keyid, profile=profile)
|
||||
ret['changes']['old'] = {'alias': None}
|
||||
ret['changes']['new'] = _describe
|
||||
ret['comment'] = 'Alias {0} created.'.format(Name)
|
||||
return ret
|
||||
|
||||
ret['comment'] = os.linesep.join([ret['comment'], 'Alias {0} is present.'.format(Name)])
|
||||
ret['changes'] = {}
|
||||
_describe = __salt__['boto_lambda.describe_alias'](FunctionName, Name,
|
||||
region=region, key=key, keyid=keyid,
|
||||
profile=profile)['alias']
|
||||
|
||||
need_update = False
|
||||
for val, var in {
|
||||
'FunctionVersion': 'FunctionVersion',
|
||||
'Description': 'Description',
|
||||
}.iteritems():
|
||||
if _describe[val] != locals()[var]:
|
||||
need_update = True
|
||||
ret['changes'].setdefault('new', {})[var] = locals()[var]
|
||||
ret['changes'].setdefault('old', {})[var] = _describe[val]
|
||||
if need_update:
|
||||
ret['comment'] = os.linesep.join([ret['comment'], 'Alias config to be modified'])
|
||||
if __opts__['test']:
|
||||
msg = 'Alias {0} set to be modified.'.format(Name)
|
||||
ret['comment'] = msg
|
||||
ret['result'] = None
|
||||
return ret
|
||||
_r = __salt__['boto_lambda.update_alias'](FunctionName=FunctionName, Name=Name,
|
||||
FunctionVersion=FunctionVersion, Description=Description,
|
||||
region=region, key=key,
|
||||
keyid=keyid, profile=profile)
|
||||
if not _r.get('updated'):
|
||||
ret['result'] = False
|
||||
ret['comment'] = 'Failed to update mapping: {0}.'.format(_r['error']['message'])
|
||||
ret['changes'] = {}
|
||||
return ret
|
||||
|
||||
|
||||
def alias_absent(name, FunctionName, Name, region=None, key=None, keyid=None, profile=None):
|
||||
'''
|
||||
Ensure alias with passed properties is absent.
|
||||
|
||||
name
|
||||
The name of the state definition.
|
||||
|
||||
FunctionName
|
||||
Name of the function.
|
||||
|
||||
Name
|
||||
Name of the alias.
|
||||
|
||||
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': {}
|
||||
}
|
||||
|
||||
r = __salt__['boto_lambda.alias_exists'](FunctionName, Name, region=region,
|
||||
key=key, keyid=keyid, profile=profile)
|
||||
if 'error' in r:
|
||||
ret['result'] = False
|
||||
ret['comment'] = 'Failed to delete alias: {0}.'.format(r['error']['message'])
|
||||
return ret
|
||||
|
||||
if r and not r['exists']:
|
||||
ret['comment'] = 'Alias {0} does not exist.'.format(Name)
|
||||
return ret
|
||||
|
||||
if __opts__['test']:
|
||||
ret['comment'] = 'Alias {0} is set to be removed.'.format(Name)
|
||||
ret['result'] = None
|
||||
return ret
|
||||
r = __salt__['boto_lambda.delete_alias'](FunctionName, Name,
|
||||
region=region, key=key,
|
||||
keyid=keyid, profile=profile)
|
||||
if not r['deleted']:
|
||||
ret['result'] = False
|
||||
ret['comment'] = 'Failed to delete alias: {0}.'.format(r['error']['message'])
|
||||
return ret
|
||||
ret['changes']['old'] = {'alias': Name}
|
||||
ret['changes']['new'] = {'alias': None}
|
||||
ret['comment'] = 'Alias {0} deleted.'.format(Name)
|
||||
return ret
|
||||
|
||||
|
||||
def _get_function_arn(name, region=None, key=None, keyid=None, profile=None):
|
||||
if name.startswith('arn:aws:lambda:'):
|
||||
return name
|
||||
|
||||
account_id = __salt__['boto_iam.get_account_id'](
|
||||
region=region, key=key, keyid=keyid, profile=profile
|
||||
)
|
||||
return 'arn:aws:lambda:{0}:{1}:function:{2}'.format(region, account_id, name)
|
||||
|
||||
|
||||
def event_source_mapping_present(name, EventSourceArn, FunctionName, StartingPosition,
|
||||
Enabled=True, BatchSize=100,
|
||||
region=None, key=None, keyid=None, profile=None):
|
||||
'''
|
||||
Ensure event source mapping exists.
|
||||
|
||||
name
|
||||
The name of the state definition.
|
||||
|
||||
EventSourceArn
|
||||
The Amazon Resource Name (ARN) of the Amazon Kinesis or the Amazon
|
||||
DynamoDB stream that is the event source.
|
||||
|
||||
FunctionName
|
||||
The Lambda function to invoke when AWS Lambda detects an event on the
|
||||
stream.
|
||||
|
||||
You can specify an unqualified function name (for example, "Thumbnail")
|
||||
or you can specify Amazon Resource Name (ARN) of the function (for
|
||||
example, "arn:aws:lambda:us-west-2:account-id:function:ThumbNail"). AWS
|
||||
Lambda also allows you to specify only the account ID qualifier (for
|
||||
example, "account-id:Thumbnail"). Note that the length constraint
|
||||
applies only to the ARN. If you specify only the function name, it is
|
||||
limited to 64 character in length.
|
||||
|
||||
StartingPosition
|
||||
The position in the stream where AWS Lambda should start reading.
|
||||
(TRIM_HORIZON | LATEST)
|
||||
|
||||
Enabled
|
||||
Indicates whether AWS Lambda should begin polling the event source. By
|
||||
default, Enabled is true.
|
||||
|
||||
BatchSize
|
||||
The largest number of records that AWS Lambda will retrieve from your
|
||||
event source at the time of invoking your function. Your function
|
||||
receives an event with all the retrieved records. The default is 100
|
||||
records.
|
||||
|
||||
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': None,
|
||||
'result': True,
|
||||
'comment': '',
|
||||
'changes': {}
|
||||
}
|
||||
|
||||
r = __salt__['boto_lambda.event_source_mapping_exists'](EventSourceArn=EventSourceArn,
|
||||
FunctionName=FunctionName,
|
||||
region=region, key=key, keyid=keyid, profile=profile)
|
||||
|
||||
if 'error' in r:
|
||||
ret['result'] = False
|
||||
ret['comment'] = 'Failed to create event source mapping: {0}.'.format(r['error']['message'])
|
||||
return ret
|
||||
|
||||
if not r.get('exists'):
|
||||
if __opts__['test']:
|
||||
ret['comment'] = 'Event source mapping {0} is set to be created.'.format(FunctionName)
|
||||
ret['result'] = None
|
||||
return ret
|
||||
r = __salt__['boto_lambda.create_event_source_mapping'](EventSourceArn=EventSourceArn,
|
||||
FunctionName=FunctionName, StartingPosition=StartingPosition,
|
||||
Enabled=Enabled, BatchSize=BatchSize,
|
||||
region=region, key=key, keyid=keyid, profile=profile)
|
||||
if not r.get('created'):
|
||||
ret['result'] = False
|
||||
ret['comment'] = 'Failed to create event source mapping: {0}.'.format(r['error']['message'])
|
||||
return ret
|
||||
_describe = __salt__['boto_lambda.describe_event_source_mapping'](
|
||||
EventSourceArn=EventSourceArn,
|
||||
FunctionName=FunctionName,
|
||||
region=region, key=key, keyid=keyid, profile=profile)
|
||||
ret['name'] = _describe['event_source_mapping']['UUID']
|
||||
ret['changes']['old'] = {'event_source_mapping': None}
|
||||
ret['changes']['new'] = _describe
|
||||
ret['comment'] = 'Event source mapping {0} created.'.format(ret['name'])
|
||||
return ret
|
||||
|
||||
ret['comment'] = os.linesep.join([ret['comment'], 'Event source mapping is present.'])
|
||||
ret['changes'] = {}
|
||||
_describe = __salt__['boto_lambda.describe_event_source_mapping'](
|
||||
EventSourceArn=EventSourceArn,
|
||||
FunctionName=FunctionName,
|
||||
region=region, key=key, keyid=keyid, profile=profile)['event_source_mapping']
|
||||
log.warn(_describe)
|
||||
|
||||
need_update = False
|
||||
for val, var in {
|
||||
'BatchSize': 'BatchSize',
|
||||
}.iteritems():
|
||||
if _describe[val] != locals()[var]:
|
||||
need_update = True
|
||||
ret['changes'].setdefault('new', {})[var] = locals()[var]
|
||||
ret['changes'].setdefault('old', {})[var] = _describe[val]
|
||||
# verify FunctionName against FunctionArn
|
||||
function_arn = _get_function_arn(FunctionName,
|
||||
region=region, key=key, keyid=keyid, profile=profile)
|
||||
if _describe['FunctionArn'] != function_arn:
|
||||
need_update = True
|
||||
ret['changes'].setdefault('new', {})['FunctionArn'] = function_arn
|
||||
ret['changes'].setdefault('old', {})['FunctionArn'] = _describe['FunctionArn']
|
||||
# TODO check for 'Enabled', since it doesn't directly map to a specific state
|
||||
if need_update:
|
||||
ret['comment'] = os.linesep.join([ret['comment'], 'Event source mapping to be modified'])
|
||||
if __opts__['test']:
|
||||
msg = 'Event source mapping {0} set to be modified.'.format(_describe['UUID'])
|
||||
ret['comment'] = msg
|
||||
ret['result'] = None
|
||||
return ret
|
||||
_r = __salt__['boto_lambda.update_event_source_mapping'](UUID=_describe['UUID'],
|
||||
FunctionName=FunctionName,
|
||||
Enabled=Enabled,
|
||||
BatchSize=BatchSize,
|
||||
region=region, key=key,
|
||||
keyid=keyid, profile=profile)
|
||||
if not _r.get('updated'):
|
||||
ret['result'] = False
|
||||
ret['comment'] = 'Failed to update mapping: {0}.'.format(_r['error']['message'])
|
||||
ret['changes'] = {}
|
||||
return ret
|
||||
|
||||
|
||||
def event_source_mapping_absent(name, EventSourceArn, FunctionName,
|
||||
region=None, key=None, keyid=None, profile=None):
|
||||
'''
|
||||
Ensure event source mapping with passed properties is absent.
|
||||
|
||||
name
|
||||
The name of the state definition.
|
||||
|
||||
EventSourceArn
|
||||
ARN of the event source.
|
||||
|
||||
FunctionName
|
||||
Name of the lambda function.
|
||||
|
||||
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': None,
|
||||
'result': True,
|
||||
'comment': '',
|
||||
'changes': {}
|
||||
}
|
||||
|
||||
desc = __salt__['boto_lambda.describe_event_source_mapping'](EventSourceArn=EventSourceArn,
|
||||
FunctionName=FunctionName,
|
||||
region=region, key=key, keyid=keyid, profile=profile)
|
||||
if 'error' in desc:
|
||||
ret['result'] = False
|
||||
ret['comment'] = 'Failed to delete event source mapping: {0}.'.format(desc['error']['message'])
|
||||
return ret
|
||||
|
||||
if not desc.get('event_source_mapping'):
|
||||
ret['comment'] = 'Event source mapping does not exist.'
|
||||
return ret
|
||||
|
||||
ret['name'] = desc['event_source_mapping']['UUID']
|
||||
if __opts__['test']:
|
||||
ret['comment'] = 'Event source mapping is set to be removed.'
|
||||
ret['result'] = None
|
||||
return ret
|
||||
r = __salt__['boto_lambda.delete_event_source_mapping'](EventSourceArn=EventSourceArn,
|
||||
FunctionName=FunctionName,
|
||||
region=region, key=key,
|
||||
keyid=keyid, profile=profile)
|
||||
if not r['deleted']:
|
||||
ret['result'] = False
|
||||
ret['comment'] = 'Failed to delete event source mapping: {0}.'.format(r['error']['message'])
|
||||
return ret
|
||||
ret['changes']['old'] = desc
|
||||
ret['changes']['new'] = {'event_source_mapping': None}
|
||||
ret['comment'] = 'Event source mapping deleted.'
|
||||
return ret
|
300
salt/utils/boto3.py
Normal file
300
salt/utils/boto3.py
Normal file
@ -0,0 +1,300 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
Boto3 Common Utils
|
||||
=================
|
||||
|
||||
Note: This module depends on the dicts packed by the loader and,
|
||||
therefore, must be accessed via the loader or from the __utils__ dict.
|
||||
|
||||
The __utils__ dict will not be automatically available to execution modules
|
||||
until 2015.8.0. The `salt.utils.compat.pack_dunder` helper function
|
||||
provides backwards compatibility.
|
||||
|
||||
This module provides common functionality for the boto execution modules.
|
||||
The expected usage is to call `apply_funcs` from the `__virtual__` function
|
||||
of the module. This will bring properly initilized partials of `_get_conn`
|
||||
and `_cache_id` into the module's namespace.
|
||||
|
||||
Example Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import salt.utils.boto3
|
||||
|
||||
def __virtual__():
|
||||
# only required in 2015.2
|
||||
salt.utils.compat.pack_dunder(__name__)
|
||||
|
||||
__utils__['boto.apply_funcs'](__name__, 'vpc')
|
||||
|
||||
def test():
|
||||
conn = _get_conn()
|
||||
vpc_id = _cache_id('test-vpc')
|
||||
|
||||
.. versionadded:: 2015.8.0
|
||||
'''
|
||||
|
||||
# Import Python libs
|
||||
from __future__ import absolute_import
|
||||
import hashlib
|
||||
import logging
|
||||
import sys
|
||||
from distutils.version import LooseVersion as _LooseVersion # pylint: disable=import-error,no-name-in-module
|
||||
from functools import partial
|
||||
|
||||
# Import salt libs
|
||||
from salt.ext.six import string_types # pylint: disable=import-error
|
||||
from salt.ext.six.moves import range # pylint: disable=import-error,redefined-builtin
|
||||
from salt.exceptions import SaltInvocationError
|
||||
|
||||
# Import third party libs
|
||||
# pylint: disable=import-error
|
||||
try:
|
||||
# pylint: disable=import-error
|
||||
import boto
|
||||
import boto3
|
||||
import boto.exception
|
||||
import boto3.session
|
||||
|
||||
# pylint: enable=import-error
|
||||
logging.getLogger('boto3').setLevel(logging.CRITICAL)
|
||||
HAS_BOTO = True
|
||||
except ImportError:
|
||||
HAS_BOTO = False
|
||||
# pylint: enable=import-error
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def __virtual__():
|
||||
'''
|
||||
Only load if boto libraries exist and if boto libraries are greater than
|
||||
a given version.
|
||||
'''
|
||||
# TODO: Determine minimal version we want to support. VPC requires > 2.8.0.
|
||||
required_boto_version = '2.0.0'
|
||||
required_boto3_version = '1.2.1'
|
||||
if not HAS_BOTO:
|
||||
return False
|
||||
elif _LooseVersion(boto.__version__) < _LooseVersion(required_boto_version):
|
||||
return False
|
||||
elif _LooseVersion(boto3.__version__) < _LooseVersion(required_boto3_version):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def _option(value):
|
||||
'''
|
||||
Look up the value for an option.
|
||||
'''
|
||||
if value in __opts__:
|
||||
return __opts__[value]
|
||||
master_opts = __pillar__.get('master', {})
|
||||
if value in master_opts:
|
||||
return master_opts[value]
|
||||
if value in __pillar__:
|
||||
return __pillar__[value]
|
||||
|
||||
|
||||
def _get_profile(service, region, key, keyid, profile):
|
||||
if profile:
|
||||
if isinstance(profile, string_types):
|
||||
_profile = _option(profile)
|
||||
elif isinstance(profile, dict):
|
||||
_profile = profile
|
||||
key = _profile.get('key', None)
|
||||
keyid = _profile.get('keyid', None)
|
||||
region = _profile.get('region', None)
|
||||
|
||||
if not region and _option(service + '.region'):
|
||||
region = _option(service + '.region')
|
||||
|
||||
if not region:
|
||||
region = 'us-east-1'
|
||||
log.info('Assuming default region {0}'.format(region))
|
||||
|
||||
if not key and _option(service + '.key'):
|
||||
key = _option(service + '.key')
|
||||
if not keyid and _option(service + '.keyid'):
|
||||
keyid = _option(service + '.keyid')
|
||||
|
||||
label = 'boto_{0}:'.format(service)
|
||||
if keyid:
|
||||
cxkey = label + hashlib.md5(region + keyid + key).hexdigest()
|
||||
else:
|
||||
cxkey = label + region
|
||||
|
||||
return (cxkey, region, key, keyid)
|
||||
|
||||
|
||||
def cache_id(service, name, sub_resource=None, resource_id=None,
|
||||
invalidate=False, region=None, key=None, keyid=None,
|
||||
profile=None):
|
||||
'''
|
||||
Cache, invalidate, or retrieve an AWS resource id keyed by name.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
__utils__['boto.cache_id']('ec2', 'myinstance',
|
||||
'i-a1b2c3',
|
||||
profile='custom_profile')
|
||||
'''
|
||||
|
||||
cxkey, _, _, _ = _get_profile(service, region, key,
|
||||
keyid, profile)
|
||||
if sub_resource:
|
||||
cxkey = '{0}:{1}:{2}:id'.format(cxkey, sub_resource, name)
|
||||
else:
|
||||
cxkey = '{0}:{1}:id'.format(cxkey, name)
|
||||
|
||||
if invalidate:
|
||||
if cxkey in __context__:
|
||||
del __context__[cxkey]
|
||||
return True
|
||||
elif resource_id in __context__.values():
|
||||
ctx = dict((k, v) for k, v in __context__.items() if v != resource_id)
|
||||
__context__.clear()
|
||||
__context__.update(ctx)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
if resource_id:
|
||||
__context__[cxkey] = resource_id
|
||||
return True
|
||||
|
||||
return __context__.get(cxkey)
|
||||
|
||||
|
||||
def cache_id_func(service):
|
||||
'''
|
||||
Returns a partial `cache_id` function for the provided service.
|
||||
|
||||
... code-block:: python
|
||||
|
||||
cache_id = __utils__['boto.cache_id_func']('ec2')
|
||||
cache_id('myinstance', 'i-a1b2c3')
|
||||
instance_id = cache_id('myinstance')
|
||||
'''
|
||||
return partial(cache_id, service)
|
||||
|
||||
|
||||
def get_connection(service, module=None, region=None, key=None, keyid=None,
|
||||
profile=None):
|
||||
'''
|
||||
Return a boto connection for the service.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
conn = __utils__['boto.get_connection']('ec2', profile='custom_profile')
|
||||
'''
|
||||
|
||||
module = module or service
|
||||
|
||||
cxkey, region, key, keyid = _get_profile(service, region, key,
|
||||
keyid, profile)
|
||||
cxkey = cxkey + ':conn'
|
||||
|
||||
if cxkey in __context__:
|
||||
return __context__[cxkey]
|
||||
|
||||
try:
|
||||
session = boto3.session.Session(aws_access_key_id=keyid,
|
||||
aws_secret_access_key=key,
|
||||
region_name=region)
|
||||
if session is None:
|
||||
raise SaltInvocationError('Region "{0}" is not '
|
||||
'valid.'.format(region))
|
||||
conn = session.client(module)
|
||||
if conn is None:
|
||||
raise SaltInvocationError('Region "{0}" is not '
|
||||
'valid.'.format(region))
|
||||
except boto.exception.NoAuthHandlerFound:
|
||||
raise SaltInvocationError('No authentication credentials found when '
|
||||
'attempting to make boto {0} connection to '
|
||||
'region "{1}".'.format(service, region))
|
||||
__context__[cxkey] = conn
|
||||
return conn
|
||||
|
||||
|
||||
def get_connection_func(service, module=None):
|
||||
'''
|
||||
Returns a partial `get_connection` function for the provided service.
|
||||
|
||||
... code-block:: python
|
||||
|
||||
get_conn = __utils__['boto.get_connection_func']('ec2')
|
||||
conn = get_conn()
|
||||
'''
|
||||
return partial(get_connection, service, module=module)
|
||||
|
||||
|
||||
def get_error(e):
|
||||
# The returns from boto modules vary greatly between modules. We need to
|
||||
# assume that none of the data we're looking for exists.
|
||||
aws = {}
|
||||
if hasattr(e, 'status'):
|
||||
aws['status'] = e.status
|
||||
if hasattr(e, 'reason'):
|
||||
aws['reason'] = e.reason
|
||||
if hasattr(e, 'message') and e.message != '':
|
||||
aws['message'] = e.message
|
||||
if hasattr(e, 'error_code') and e.error_code is not None:
|
||||
aws['code'] = e.error_code
|
||||
|
||||
if 'message' in aws and 'reason' in aws:
|
||||
message = '{0}: {1}'.format(aws['reason'], aws['message'])
|
||||
elif 'message' in aws:
|
||||
message = aws['message']
|
||||
elif 'reason' in aws:
|
||||
message = aws['reason']
|
||||
else:
|
||||
message = ''
|
||||
r = {'message': message}
|
||||
if aws:
|
||||
r['aws'] = aws
|
||||
return r
|
||||
|
||||
|
||||
def exactly_n(l, n=1):
|
||||
'''
|
||||
Tests that exactly N items in an iterable are "truthy" (neither None,
|
||||
False, nor 0).
|
||||
'''
|
||||
i = iter(l)
|
||||
return all(any(i) for j in range(n)) and not any(i)
|
||||
|
||||
|
||||
def exactly_one(l):
|
||||
return exactly_n(l)
|
||||
|
||||
|
||||
def assign_funcs(modname, service, module=None):
|
||||
'''
|
||||
Assign _get_conn and _cache_id functions to the named module.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
_utils__['boto.assign_partials'](__name__, 'ec2')
|
||||
'''
|
||||
mod = sys.modules[modname]
|
||||
setattr(mod, '_get_conn', get_connection_func(service, module=module))
|
||||
setattr(mod, '_cache_id', cache_id_func(service))
|
||||
|
||||
# TODO: Remove this and import salt.utils.exactly_one into boto_* modules instead
|
||||
# Leaving this way for now so boto modules can be back ported
|
||||
setattr(mod, '_exactly_one', exactly_one)
|
||||
|
||||
|
||||
def paged_call(function, marker_flag='NextMarker', marker_arg='Marker', *args, **kwargs):
|
||||
"""Retrieve full set of values from a boto3 API call that may truncate
|
||||
its results, yielding each page as it is obtained.
|
||||
"""
|
||||
while True:
|
||||
ret = function(*args, **kwargs)
|
||||
marker = ret.get(marker_flag)
|
||||
yield ret
|
||||
if not marker:
|
||||
break
|
||||
kwargs[marker_arg] = marker
|
730
tests/unit/modules/boto_lambda_test.py
Normal file
730
tests/unit/modules/boto_lambda_test.py
Normal file
@ -0,0 +1,730 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Import Python libs
|
||||
from __future__ import absolute_import
|
||||
from distutils.version import LooseVersion # pylint: disable=import-error,no-name-in-module
|
||||
|
||||
# Import Salt Testing libs
|
||||
from salttesting.unit import skipIf, TestCase
|
||||
from salttesting.mock import NO_MOCK, NO_MOCK_REASON, patch
|
||||
from salttesting.helpers import ensure_in_syspath
|
||||
|
||||
ensure_in_syspath('../../')
|
||||
|
||||
# Import Salt libs
|
||||
import salt.config
|
||||
import salt.loader
|
||||
from salt.modules import boto_lambda
|
||||
from salt.exceptions import SaltInvocationError
|
||||
|
||||
# Import 3rd-party libs
|
||||
from tempfile import NamedTemporaryFile
|
||||
import logging
|
||||
import os
|
||||
|
||||
# Import Mock libraries
|
||||
from salttesting.mock import NO_MOCK, NO_MOCK_REASON, MagicMock, patch
|
||||
|
||||
# pylint: disable=import-error,no-name-in-module
|
||||
try:
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
HAS_BOTO = True
|
||||
except ImportError:
|
||||
HAS_BOTO = False
|
||||
|
||||
# pylint: enable=import-error,no-name-in-module
|
||||
|
||||
# the boto_lambda module relies on the connect_to_region() method
|
||||
# which was added in boto 2.8.0
|
||||
# https://github.com/boto/boto/commit/33ac26b416fbb48a60602542b4ce15dcc7029f12
|
||||
required_boto3_version = '1.2.1'
|
||||
|
||||
region = 'us-east-1'
|
||||
access_key = 'GKTADJGHEIQSXMKKRBJ08H'
|
||||
secret_key = 'askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs'
|
||||
conn_parameters = {'region': region, 'key': access_key, 'keyid': secret_key, 'profile': {}}
|
||||
error_message = 'An error occurred (101) when calling the {0} operation: Test-defined error'
|
||||
error_content = {
|
||||
'Error': {
|
||||
'Code': 101,
|
||||
'Message': "Test-defined error"
|
||||
}
|
||||
}
|
||||
function_ret = dict(FunctionName='testfunction',
|
||||
Runtime='python2.7',
|
||||
Role=None,
|
||||
Handler='handler',
|
||||
Description='abcdefg',
|
||||
Timeout=5,
|
||||
MemorySize=128,
|
||||
CodeSha256='abcdef',
|
||||
CodeSize=199,
|
||||
FunctionArn='arn:lambda:us-east-1:1234:Something',
|
||||
LastModified='yes')
|
||||
alias_ret = dict(AliasArn='arn:lambda:us-east-1:1234:Something',
|
||||
Name='testalias',
|
||||
FunctionVersion='3',
|
||||
Description='Alias description')
|
||||
event_source_mapping_ret = dict(UUID='1234-1-123',
|
||||
BatchSize=123,
|
||||
EventSourceArn='arn:lambda:us-east-1:1234:Something',
|
||||
FunctionArn='arn:lambda:us-east-1:1234:Something',
|
||||
LastModified='yes',
|
||||
LastProcessingResult='SUCCESS',
|
||||
State='Enabled',
|
||||
StateTransitionReason='Random')
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
opts = salt.config.DEFAULT_MINION_OPTS
|
||||
context = {}
|
||||
utils = salt.loader.utils(opts, whitelist=['boto3'], context=context)
|
||||
|
||||
boto_lambda.__utils__ = utils
|
||||
boto_lambda.__init__(opts)
|
||||
boto_lambda.__salt__ = {}
|
||||
|
||||
|
||||
def _has_required_boto():
|
||||
'''
|
||||
Returns True/False boolean depending on if Boto is installed and correct
|
||||
version.
|
||||
'''
|
||||
if not HAS_BOTO:
|
||||
return False
|
||||
elif LooseVersion(boto3.__version__) < LooseVersion(required_boto3_version):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
class BotoLambdaTestCaseBase(TestCase):
|
||||
conn = None
|
||||
|
||||
# Set up MagicMock to replace the boto3 session
|
||||
def setUp(self):
|
||||
context.clear()
|
||||
|
||||
self.patcher = patch('boto3.session.Session')
|
||||
self.addCleanup(self.patcher.stop)
|
||||
mock_session = self.patcher.start()
|
||||
|
||||
session_instance = mock_session.return_value
|
||||
self.conn = MagicMock()
|
||||
session_instance.client.return_value = self.conn
|
||||
|
||||
|
||||
class TempZipFile(object):
|
||||
def __enter__(self):
|
||||
with NamedTemporaryFile(suffix='.zip', prefix='salt_test_', delete=False) as tmp:
|
||||
tmp.write('###\n')
|
||||
self.zipfile = tmp.name
|
||||
return self.zipfile
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
os.remove(self.zipfile)
|
||||
|
||||
|
||||
class BotoLambdaTestCaseMixin(object):
|
||||
pass
|
||||
|
||||
|
||||
@skipIf(HAS_BOTO is False, 'The boto module must be installed.')
|
||||
@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than'
|
||||
' or equal to version {0}'
|
||||
.format(required_boto3_version))
|
||||
@skipIf(NO_MOCK, NO_MOCK_REASON)
|
||||
class BotoLambdaFunctionTestCase(BotoLambdaTestCaseBase, BotoLambdaTestCaseMixin):
|
||||
'''
|
||||
TestCase for salt.modules.boto_lambda module
|
||||
'''
|
||||
|
||||
def test_that_when_checking_if_a_function_exists_and_a_function_exists_the_function_exists_method_returns_true(self):
|
||||
'''
|
||||
Tests checking lambda function existence when the lambda function already exists
|
||||
'''
|
||||
self.conn.list_functions.return_value = {'Functions': [function_ret]}
|
||||
func_exists_result = boto_lambda.function_exists(FunctionName=function_ret['FunctionName'], **conn_parameters)
|
||||
|
||||
self.assertTrue(func_exists_result['exists'])
|
||||
|
||||
def test_that_when_checking_if_a_function_exists_and_a_function_does_not_exist_the_function_exists_method_returns_false(self):
|
||||
'''
|
||||
Tests checking lambda function existence when the lambda function does not exist
|
||||
'''
|
||||
self.conn.list_functions.return_value = {'Functions': [function_ret]}
|
||||
func_exists_result = boto_lambda.function_exists(FunctionName='myfunc', **conn_parameters)
|
||||
|
||||
self.assertFalse(func_exists_result['exists'])
|
||||
|
||||
def test_that_when_checking_if_a_function_exists_and_boto3_returns_an_error_the_function_exists_method_returns_error(self):
|
||||
'''
|
||||
Tests checking lambda function existence when boto returns an error
|
||||
'''
|
||||
self.conn.list_functions.side_effect = ClientError(error_content, 'list_functions')
|
||||
func_exists_result = boto_lambda.function_exists(FunctionName='myfunc', **conn_parameters)
|
||||
|
||||
self.assertEqual(func_exists_result.get('error', {}).get('message'), error_message.format('list_functions'))
|
||||
|
||||
def test_that_when_creating_a_function_from_zipfile_succeeds_the_create_function_method_returns_true(self):
|
||||
'''
|
||||
tests True function created.
|
||||
'''
|
||||
with patch.dict(boto_lambda.__salt__, {'boto_iam.get_account_id': MagicMock(return_value='1234')}):
|
||||
with TempZipFile() as zipfile:
|
||||
self.conn.create_function.return_value = function_ret
|
||||
lambda_creation_result = boto_lambda.create_function(FunctionName='testfunction',
|
||||
Runtime='python2.7',
|
||||
Role='myrole',
|
||||
Handler='file.method',
|
||||
ZipFile=zipfile,
|
||||
**conn_parameters)
|
||||
|
||||
self.assertTrue(lambda_creation_result['created'])
|
||||
|
||||
def test_that_when_creating_a_function_from_s3_succeeds_the_create_function_method_returns_true(self):
|
||||
'''
|
||||
tests True function created.
|
||||
'''
|
||||
with patch.dict(boto_lambda.__salt__, {'boto_iam.get_account_id': MagicMock(return_value='1234')}):
|
||||
self.conn.create_function.return_value = function_ret
|
||||
lambda_creation_result = boto_lambda.create_function(FunctionName='testfunction',
|
||||
Runtime='python2.7',
|
||||
Role='myrole',
|
||||
Handler='file.method',
|
||||
S3Bucket='bucket',
|
||||
S3Key='key',
|
||||
**conn_parameters)
|
||||
|
||||
self.assertTrue(lambda_creation_result['created'])
|
||||
|
||||
def test_that_when_creating_a_function_without_code_raises_a_salt_invocation_error(self):
|
||||
'''
|
||||
tests Creating a function without code
|
||||
'''
|
||||
with patch.dict(boto_lambda.__salt__, {'boto_iam.get_account_id': MagicMock(return_value='1234')}):
|
||||
with self.assertRaisesRegexp(SaltInvocationError,
|
||||
'Either ZipFile must be specified, or S3Bucket and S3Key must be provided.'):
|
||||
lambda_creation_result = boto_lambda.create_function(FunctionName='testfunction',
|
||||
Runtime='python2.7',
|
||||
Role='myrole',
|
||||
Handler='file.method',
|
||||
**conn_parameters)
|
||||
|
||||
def test_that_when_creating_a_function_with_zipfile_and_s3_raises_a_salt_invocation_error(self):
|
||||
'''
|
||||
tests Creating a function without code
|
||||
'''
|
||||
with patch.dict(boto_lambda.__salt__, {'boto_iam.get_account_id': MagicMock(return_value='1234')}):
|
||||
with self.assertRaisesRegexp(SaltInvocationError,
|
||||
'Either ZipFile must be specified, or S3Bucket and S3Key must be provided.'):
|
||||
with TempZipFile() as zipfile:
|
||||
lambda_creation_result = boto_lambda.create_function(FunctionName='testfunction',
|
||||
Runtime='python2.7',
|
||||
Role='myrole',
|
||||
Handler='file.method',
|
||||
ZipFile=zipfile,
|
||||
S3Bucket='bucket',
|
||||
S3Key='key',
|
||||
**conn_parameters)
|
||||
|
||||
def test_that_when_creating_a_function_fails_the_create_function_method_returns_error(self):
|
||||
'''
|
||||
tests False function not created.
|
||||
'''
|
||||
with patch.dict(boto_lambda.__salt__, {'boto_iam.get_account_id': MagicMock(return_value='1234')}):
|
||||
self.conn.create_function.side_effect = ClientError(error_content, 'create_function')
|
||||
with TempZipFile() as zipfile:
|
||||
lambda_creation_result = boto_lambda.create_function(FunctionName='testfunction',
|
||||
Runtime='python2.7',
|
||||
Role='myrole',
|
||||
Handler='file.method',
|
||||
ZipFile=zipfile,
|
||||
**conn_parameters)
|
||||
self.assertEqual(lambda_creation_result.get('error', {}).get('message'), error_message.format('create_function'))
|
||||
|
||||
def test_that_when_deleting_a_function_succeeds_the_delete_function_method_returns_true(self):
|
||||
'''
|
||||
tests True function deleted.
|
||||
'''
|
||||
with patch.dict(boto_lambda.__salt__, {'boto_iam.get_account_id': MagicMock(return_value='1234')}):
|
||||
result = boto_lambda.delete_function(FunctionName='testfunction',
|
||||
Qualifier=1,
|
||||
**conn_parameters)
|
||||
|
||||
self.assertTrue(result['deleted'])
|
||||
|
||||
def test_that_when_deleting_a_function_fails_the_delete_function_method_returns_false(self):
|
||||
'''
|
||||
tests False function not deleted.
|
||||
'''
|
||||
with patch.dict(boto_lambda.__salt__, {'boto_iam.get_account_id': MagicMock(return_value='1234')}):
|
||||
self.conn.delete_function.side_effect = ClientError(error_content, 'delete_function')
|
||||
result = boto_lambda.delete_function(FunctionName='testfunction',
|
||||
**conn_parameters)
|
||||
self.assertFalse(result['deleted'])
|
||||
|
||||
def test_that_when_describing_function_it_returns_the_dict_of_properties_returns_true(self):
|
||||
'''
|
||||
Tests describing parameters if function exists
|
||||
'''
|
||||
self.conn.list_functions.return_value = {'Functions': [function_ret]}
|
||||
|
||||
with patch.dict(boto_lambda.__salt__, {'boto_iam.get_account_id': MagicMock(return_value='1234')}):
|
||||
result = boto_lambda.describe_function(FunctionName=function_ret['FunctionName'], **conn_parameters)
|
||||
|
||||
self.assertEqual(result, {'function': function_ret})
|
||||
|
||||
def test_that_when_describing_function_it_returns_the_dict_of_properties_returns_false(self):
|
||||
'''
|
||||
Tests describing parameters if function does not exist
|
||||
'''
|
||||
self.conn.list_functions.return_value = {'Functions': []}
|
||||
with patch.dict(boto_lambda.__salt__, {'boto_iam.get_account_id': MagicMock(return_value='1234')}):
|
||||
result = boto_lambda.describe_function(FunctionName='testfunction', **conn_parameters)
|
||||
|
||||
self.assertFalse(result['function'])
|
||||
|
||||
def test_that_when_describing_lambda_on_client_error_it_returns_error(self):
|
||||
'''
|
||||
Tests describing parameters failure
|
||||
'''
|
||||
self.conn.list_functions.side_effect = ClientError(error_content, 'list_functions')
|
||||
result = boto_lambda.describe_function(FunctionName='testfunction', **conn_parameters)
|
||||
self.assertTrue('error' in result)
|
||||
|
||||
def test_that_when_updating_a_function_succeeds_the_update_function_method_returns_true(self):
|
||||
'''
|
||||
tests True function updated.
|
||||
'''
|
||||
with patch.dict(boto_lambda.__salt__, {'boto_iam.get_account_id': MagicMock(return_value='1234')}):
|
||||
self.conn.update_function_config.return_value = function_ret
|
||||
result = boto_lambda.update_function_config(FunctionName=function_ret['FunctionName'], Role='myrole', **conn_parameters)
|
||||
|
||||
self.assertTrue(result['updated'])
|
||||
|
||||
def test_that_when_updating_a_function_fails_the_update_function_method_returns_error(self):
|
||||
'''
|
||||
tests False function not updated.
|
||||
'''
|
||||
with patch.dict(boto_lambda.__salt__, {'boto_iam.get_account_id': MagicMock(return_value='1234')}):
|
||||
self.conn.update_function_configuration.side_effect = ClientError(error_content, 'update_function')
|
||||
result = boto_lambda.update_function_config(FunctionName='testfunction',
|
||||
Role='myrole',
|
||||
**conn_parameters)
|
||||
self.assertEqual(result.get('error', {}).get('message'), error_message.format('update_function'))
|
||||
|
||||
def test_that_when_updating_function_code_from_zipfile_succeeds_the_update_function_method_returns_true(self):
|
||||
'''
|
||||
tests True function updated.
|
||||
'''
|
||||
with patch.dict(boto_lambda.__salt__, {'boto_iam.get_account_id': MagicMock(return_value='1234')}):
|
||||
with TempZipFile() as zipfile:
|
||||
self.conn.update_function_code.return_value = function_ret
|
||||
result = boto_lambda.update_function_code(FunctionName=function_ret['FunctionName'], ZipFile=zipfile, **conn_parameters)
|
||||
|
||||
self.assertTrue(result['updated'])
|
||||
|
||||
def test_that_when_updating_function_code_from_s3_succeeds_the_update_function_method_returns_true(self):
|
||||
'''
|
||||
tests True function updated.
|
||||
'''
|
||||
with patch.dict(boto_lambda.__salt__, {'boto_iam.get_account_id': MagicMock(return_value='1234')}):
|
||||
self.conn.update_function_code.return_value = function_ret
|
||||
result = boto_lambda.update_function_code(FunctionName='testfunction',
|
||||
S3Bucket='bucket',
|
||||
S3Key='key',
|
||||
**conn_parameters)
|
||||
|
||||
self.assertTrue(result['updated'])
|
||||
|
||||
def test_that_when_updating_function_code_without_code_raises_a_salt_invocation_error(self):
|
||||
'''
|
||||
tests Creating a function without code
|
||||
'''
|
||||
with patch.dict(boto_lambda.__salt__, {'boto_iam.get_account_id': MagicMock(return_value='1234')}):
|
||||
with self.assertRaisesRegexp(SaltInvocationError,
|
||||
'Either ZipFile must be specified, or S3Bucket and S3Key must be provided.'):
|
||||
result = boto_lambda.update_function_code(FunctionName='testfunction',
|
||||
**conn_parameters)
|
||||
|
||||
def test_that_when_updating_function_code_fails_the_update_function_method_returns_error(self):
|
||||
'''
|
||||
tests False function not updated.
|
||||
'''
|
||||
with patch.dict(boto_lambda.__salt__, {'boto_iam.get_account_id': MagicMock(return_value='1234')}):
|
||||
self.conn.update_function_code.side_effect = ClientError(error_content, 'update_function_code')
|
||||
result = boto_lambda.update_function_code(FunctionName='testfunction',
|
||||
S3Bucket='bucket',
|
||||
S3Key='key',
|
||||
**conn_parameters)
|
||||
self.assertEqual(result.get('error', {}).get('message'), error_message.format('update_function_code'))
|
||||
|
||||
def test_that_when_listing_function_versions_succeeds_the_list_function_versions_method_returns_true(self):
|
||||
'''
|
||||
tests True function versions listed.
|
||||
'''
|
||||
with patch.dict(boto_lambda.__salt__, {'boto_iam.get_account_id': MagicMock(return_value='1234')}):
|
||||
self.conn.list_versions_by_function.return_value = {'Versions': [function_ret]}
|
||||
result = boto_lambda.list_function_versions(FunctionName='testfunction',
|
||||
**conn_parameters)
|
||||
|
||||
self.assertTrue(result['Versions'])
|
||||
|
||||
def test_that_when_listing_function_versions_fails_the_list_function_versions_method_returns_false(self):
|
||||
'''
|
||||
tests False no function versions listed.
|
||||
'''
|
||||
with patch.dict(boto_lambda.__salt__, {'boto_iam.get_account_id': MagicMock(return_value='1234')}):
|
||||
self.conn.list_versions_by_function.return_value = {'Versions': []}
|
||||
result = boto_lambda.list_function_versions(FunctionName='testfunction',
|
||||
**conn_parameters)
|
||||
self.assertFalse(result['Versions'])
|
||||
|
||||
def test_that_when_listing_function_versions_fails_the_list_function_versions_method_returns_error(self):
|
||||
'''
|
||||
tests False function versions error.
|
||||
'''
|
||||
with patch.dict(boto_lambda.__salt__, {'boto_iam.get_account_id': MagicMock(return_value='1234')}):
|
||||
self.conn.list_versions_by_function.side_effect = ClientError(error_content, 'list_versions_by_function')
|
||||
result = boto_lambda.list_function_versions(FunctionName='testfunction',
|
||||
**conn_parameters)
|
||||
self.assertEqual(result.get('error', {}).get('message'), error_message.format('list_versions_by_function'))
|
||||
|
||||
|
||||
@skipIf(HAS_BOTO is False, 'The boto module must be installed.')
|
||||
@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than'
|
||||
' or equal to version {0}'
|
||||
.format(required_boto3_version))
|
||||
@skipIf(NO_MOCK, NO_MOCK_REASON)
|
||||
class BotoLambdaAliasTestCase(BotoLambdaTestCaseBase, BotoLambdaTestCaseMixin):
|
||||
'''
|
||||
TestCase for salt.modules.boto_lambda module aliases
|
||||
'''
|
||||
def test_that_when_creating_an_alias_succeeds_the_create_alias_method_returns_true(self):
|
||||
'''
|
||||
tests True alias created.
|
||||
'''
|
||||
self.conn.create_alias.return_value = alias_ret
|
||||
result = boto_lambda.create_alias(FunctionName='testfunction',
|
||||
Name=alias_ret['Name'],
|
||||
FunctionVersion=alias_ret['FunctionVersion'],
|
||||
**conn_parameters)
|
||||
|
||||
self.assertTrue(result['created'])
|
||||
|
||||
def test_that_when_creating_an_alias_fails_the_create_alias_method_returns_error(self):
|
||||
'''
|
||||
tests False alias not created.
|
||||
'''
|
||||
self.conn.create_alias.side_effect = ClientError(error_content, 'create_alias')
|
||||
result = boto_lambda.create_alias(FunctionName='testfunction',
|
||||
Name=alias_ret['Name'],
|
||||
FunctionVersion=alias_ret['FunctionVersion'],
|
||||
**conn_parameters)
|
||||
self.assertEqual(result.get('error', {}).get('message'), error_message.format('create_alias'))
|
||||
|
||||
def test_that_when_deleting_an_alias_succeeds_the_delete_alias_method_returns_true(self):
|
||||
'''
|
||||
tests True alias deleted.
|
||||
'''
|
||||
result = boto_lambda.delete_alias(FunctionName='testfunction',
|
||||
Name=alias_ret['Name'],
|
||||
**conn_parameters)
|
||||
|
||||
self.assertTrue(result['deleted'])
|
||||
|
||||
def test_that_when_deleting_an_alias_fails_the_delete_alias_method_returns_false(self):
|
||||
'''
|
||||
tests False alias not deleted.
|
||||
'''
|
||||
self.conn.delete_alias.side_effect = ClientError(error_content, 'delete_alias')
|
||||
result = boto_lambda.delete_alias(FunctionName='testfunction',
|
||||
Name=alias_ret['Name'],
|
||||
**conn_parameters)
|
||||
self.assertFalse(result['deleted'])
|
||||
|
||||
def test_that_when_checking_if_an_alias_exists_and_the_alias_exists_the_alias_exists_method_returns_true(self):
|
||||
'''
|
||||
Tests checking lambda alias existence when the lambda alias already exists
|
||||
'''
|
||||
self.conn.list_aliases.return_value = {'Aliases': [alias_ret]}
|
||||
result = boto_lambda.alias_exists(FunctionName='testfunction',
|
||||
Name=alias_ret['Name'],
|
||||
**conn_parameters)
|
||||
self.assertTrue(result['exists'])
|
||||
|
||||
def test_that_when_checking_if_an_alias_exists_and_the_alias_does_not_exist_the_alias_exists_method_returns_false(self):
|
||||
'''
|
||||
Tests checking lambda alias existence when the lambda alias does not exist
|
||||
'''
|
||||
self.conn.list_aliases.return_value = {'Aliases': [alias_ret]}
|
||||
result = boto_lambda.alias_exists(FunctionName='testfunction',
|
||||
Name='otheralias',
|
||||
**conn_parameters)
|
||||
|
||||
self.assertFalse(result['exists'])
|
||||
|
||||
def test_that_when_checking_if_an_alias_exists_and_boto3_returns_an_error_the_alias_exists_method_returns_error(self):
|
||||
'''
|
||||
Tests checking lambda alias existence when boto returns an error
|
||||
'''
|
||||
self.conn.list_aliases.side_effect = ClientError(error_content, 'list_aliases')
|
||||
result = boto_lambda.alias_exists(FunctionName='testfunction',
|
||||
Name=alias_ret['Name'],
|
||||
**conn_parameters)
|
||||
|
||||
self.assertEqual(result.get('error', {}).get('message'), error_message.format('list_aliases'))
|
||||
|
||||
def test_that_when_describing_alias_it_returns_the_dict_of_properties_returns_true(self):
|
||||
'''
|
||||
Tests describing parameters if alias exists
|
||||
'''
|
||||
self.conn.list_aliases.return_value = {'Aliases': [alias_ret]}
|
||||
|
||||
result = boto_lambda.describe_alias(FunctionName='testfunction',
|
||||
Name=alias_ret['Name'],
|
||||
**conn_parameters)
|
||||
|
||||
self.assertEqual(result, {'alias': alias_ret})
|
||||
|
||||
def test_that_when_describing_alias_it_returns_the_dict_of_properties_returns_false(self):
|
||||
'''
|
||||
Tests describing parameters if alias does not exist
|
||||
'''
|
||||
self.conn.list_aliases.return_value = {'Aliases': [alias_ret]}
|
||||
result = boto_lambda.describe_alias(FunctionName='testfunction',
|
||||
Name='othername',
|
||||
**conn_parameters)
|
||||
|
||||
self.assertFalse(result['alias'])
|
||||
|
||||
def test_that_when_describing_lambda_on_client_error_it_returns_error(self):
|
||||
'''
|
||||
Tests describing parameters failure
|
||||
'''
|
||||
self.conn.list_aliases.side_effect = ClientError(error_content, 'list_aliases')
|
||||
result = boto_lambda.describe_alias(FunctionName='testfunction',
|
||||
Name=alias_ret['Name'],
|
||||
**conn_parameters)
|
||||
self.assertTrue('error' in result)
|
||||
|
||||
def test_that_when_updating_an_alias_succeeds_the_update_alias_method_returns_true(self):
|
||||
'''
|
||||
tests True alias updated.
|
||||
'''
|
||||
self.conn.update_alias.return_value = alias_ret
|
||||
result = boto_lambda.update_alias(FunctionName='testfunctoin',
|
||||
Name=alias_ret['Name'],
|
||||
Description=alias_ret['Description'],
|
||||
**conn_parameters)
|
||||
|
||||
self.assertTrue(result['updated'])
|
||||
|
||||
def test_that_when_updating_an_alias_fails_the_update_alias_method_returns_error(self):
|
||||
'''
|
||||
tests False alias not updated.
|
||||
'''
|
||||
self.conn.update_alias.side_effect = ClientError(error_content, 'update_alias')
|
||||
result = boto_lambda.update_alias(FunctionName='testfunction',
|
||||
Name=alias_ret['Name'],
|
||||
**conn_parameters)
|
||||
self.assertEqual(result.get('error', {}).get('message'), error_message.format('update_alias'))
|
||||
|
||||
|
||||
@skipIf(HAS_BOTO is False, 'The boto module must be installed.')
|
||||
@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than'
|
||||
' or equal to version {0}'
|
||||
.format(required_boto3_version))
|
||||
@skipIf(NO_MOCK, NO_MOCK_REASON)
|
||||
class BotoLambdaEventSourceMappingTestCase(BotoLambdaTestCaseBase, BotoLambdaTestCaseMixin):
|
||||
'''
|
||||
TestCase for salt.modules.boto_lambda module mappings
|
||||
'''
|
||||
def test_that_when_creating_a_mapping_succeeds_the_create_event_source_mapping_method_returns_true(self):
|
||||
'''
|
||||
tests True mapping created.
|
||||
'''
|
||||
self.conn.create_event_source_mapping.return_value = event_source_mapping_ret
|
||||
result = boto_lambda.create_event_source_mapping(
|
||||
EventSourceArn=event_source_mapping_ret['EventSourceArn'],
|
||||
FunctionName=event_source_mapping_ret['FunctionArn'],
|
||||
StartingPosition='LATEST',
|
||||
**conn_parameters)
|
||||
|
||||
self.assertTrue(result['created'])
|
||||
|
||||
def test_that_when_creating_an_event_source_mapping_fails_the_create_event_source_mapping_method_returns_error(self):
|
||||
'''
|
||||
tests False mapping not created.
|
||||
'''
|
||||
self.conn.create_event_source_mapping.side_effect = ClientError(error_content, 'create_event_source_mapping')
|
||||
result = boto_lambda.create_event_source_mapping(
|
||||
EventSourceArn=event_source_mapping_ret['EventSourceArn'],
|
||||
FunctionName=event_source_mapping_ret['FunctionArn'],
|
||||
StartingPosition='LATEST',
|
||||
**conn_parameters)
|
||||
self.assertEqual(result.get('error', {}).get('message'),
|
||||
error_message.format('create_event_source_mapping'))
|
||||
|
||||
def test_that_when_listing_mapping_ids_succeeds_the_get_event_source_mapping_ids_method_returns_true(self):
|
||||
'''
|
||||
tests True mapping ids listed.
|
||||
'''
|
||||
self.conn.list_event_source_mappings.return_value = {'EventSourceMappings': [event_source_mapping_ret]}
|
||||
result = boto_lambda.get_event_source_mapping_ids(
|
||||
EventSourceArn=event_source_mapping_ret['EventSourceArn'],
|
||||
FunctionName=event_source_mapping_ret['FunctionArn'],
|
||||
**conn_parameters)
|
||||
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_that_when_listing_event_source_mapping_ids_fails_the_get_event_source_mapping_ids_versions_method_returns_false(self):
|
||||
'''
|
||||
tests False no mapping ids listed.
|
||||
'''
|
||||
self.conn.list_event_source_mappings.return_value = {'EventSourceMappings': []}
|
||||
result = boto_lambda.get_event_source_mapping_ids(
|
||||
EventSourceArn=event_source_mapping_ret['EventSourceArn'],
|
||||
FunctionName=event_source_mapping_ret['FunctionArn'],
|
||||
**conn_parameters)
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_that_when_listing_event_source_mapping_ids_fails_the_get_event_source_mapping_ids_method_returns_error(self):
|
||||
'''
|
||||
tests False mapping ids error.
|
||||
'''
|
||||
self.conn.list_event_source_mappings.side_effect = ClientError(error_content, 'list_event_source_mappings')
|
||||
result = boto_lambda.get_event_source_mapping_ids(
|
||||
EventSourceArn=event_source_mapping_ret['EventSourceArn'],
|
||||
FunctionName=event_source_mapping_ret['FunctionArn'],
|
||||
**conn_parameters)
|
||||
self.assertEqual(result.get('error', {}).get('message'), error_message.format('list_event_source_mappings'))
|
||||
|
||||
def test_that_when_deleting_an_event_source_mapping_by_UUID_succeeds_the_delete_event_source_mapping_method_returns_true(self):
|
||||
'''
|
||||
tests True mapping deleted.
|
||||
'''
|
||||
result = boto_lambda.delete_event_source_mapping(
|
||||
UUID=event_source_mapping_ret['UUID'],
|
||||
**conn_parameters)
|
||||
self.assertTrue(result['deleted'])
|
||||
|
||||
def test_that_when_deleting_an_event_source_mapping_by_name_succeeds_the_delete_event_source_mapping_method_returns_true(self):
|
||||
'''
|
||||
tests True mapping deleted.
|
||||
'''
|
||||
self.conn.list_event_source_mappings.return_value = {'EventSourceMappings': [event_source_mapping_ret]}
|
||||
result = boto_lambda.delete_event_source_mapping(
|
||||
EventSourceArn=event_source_mapping_ret['EventSourceArn'],
|
||||
FunctionName=event_source_mapping_ret['FunctionArn'],
|
||||
**conn_parameters)
|
||||
self.assertTrue(result['deleted'])
|
||||
|
||||
def test_that_when_deleting_an_event_source_mapping_without_identifier_the_delete_event_source_mapping_method_raises_saltinvocationexception(self):
|
||||
'''
|
||||
tests Deleting a mapping without identifier
|
||||
'''
|
||||
with self.assertRaisesRegexp(SaltInvocationError,
|
||||
'Either UUID must be specified, or EventSourceArn and FunctionName must be provided.'):
|
||||
result = boto_lambda.delete_event_source_mapping(**conn_parameters)
|
||||
|
||||
def test_that_when_deleting_an_event_source_mapping_fails_the_delete_event_source_mapping_method_returns_false(self):
|
||||
'''
|
||||
tests False mapping not deleted.
|
||||
'''
|
||||
self.conn.delete_event_source_mapping.side_effect = ClientError(error_content, 'delete_event_source_mapping')
|
||||
result = boto_lambda.delete_event_source_mapping(UUID=event_source_mapping_ret['UUID'],
|
||||
**conn_parameters)
|
||||
self.assertFalse(result['deleted'])
|
||||
|
||||
def test_that_when_checking_if_an_event_source_mapping_exists_and_the_event_source_mapping_exists_the_event_source_mapping_exists_method_returns_true(self):
|
||||
'''
|
||||
Tests checking lambda event_source_mapping existence when the lambda
|
||||
event_source_mapping already exists
|
||||
'''
|
||||
self.conn.get_event_source_mapping.return_value = event_source_mapping_ret
|
||||
result = boto_lambda.event_source_mapping_exists(
|
||||
UUID=event_source_mapping_ret['UUID'],
|
||||
**conn_parameters)
|
||||
self.assertTrue(result['exists'])
|
||||
|
||||
def test_that_when_checking_if_an_event_source_mapping_exists_and_the_event_source_mapping_does_not_exist_the_event_source_mapping_exists_method_returns_false(self):
|
||||
'''
|
||||
Tests checking lambda event_source_mapping existence when the lambda
|
||||
event_source_mapping does not exist
|
||||
'''
|
||||
self.conn.get_event_source_mapping.return_value = None
|
||||
result = boto_lambda.event_source_mapping_exists(
|
||||
UUID='other_UUID',
|
||||
**conn_parameters)
|
||||
self.assertFalse(result['exists'])
|
||||
|
||||
def test_that_when_checking_if_an_event_source_mapping_exists_and_boto3_returns_an_error_the_event_source_mapping_exists_method_returns_error(self):
|
||||
'''
|
||||
Tests checking lambda event_source_mapping existence when boto returns an error
|
||||
'''
|
||||
self.conn.get_event_source_mapping.side_effect = ClientError(error_content, 'list_event_source_mappings')
|
||||
result = boto_lambda.event_source_mapping_exists(
|
||||
UUID=event_source_mapping_ret['UUID'],
|
||||
**conn_parameters)
|
||||
self.assertEqual(result.get('error', {}).get('message'), error_message.format('list_event_source_mappings'))
|
||||
|
||||
def test_that_when_describing_event_source_mapping_it_returns_the_dict_of_properties_returns_true(self):
|
||||
'''
|
||||
Tests describing parameters if event_source_mapping exists
|
||||
'''
|
||||
self.conn.get_event_source_mapping.return_value = event_source_mapping_ret
|
||||
result = boto_lambda.describe_event_source_mapping(
|
||||
UUID=event_source_mapping_ret['UUID'],
|
||||
**conn_parameters)
|
||||
self.assertEqual(result, {'event_source_mapping': event_source_mapping_ret})
|
||||
|
||||
def test_that_when_describing_event_source_mapping_it_returns_the_dict_of_properties_returns_false(self):
|
||||
'''
|
||||
Tests describing parameters if event_source_mapping does not exist
|
||||
'''
|
||||
self.conn.get_event_source_mapping.return_value = None
|
||||
result = boto_lambda.describe_event_source_mapping(
|
||||
UUID=event_source_mapping_ret['UUID'],
|
||||
**conn_parameters)
|
||||
self.assertFalse(result['event_source_mapping'])
|
||||
|
||||
def test_that_when_describing_event_source_mapping_on_client_error_it_returns_error(self):
|
||||
'''
|
||||
Tests describing parameters failure
|
||||
'''
|
||||
self.conn.get_event_source_mapping.side_effect = ClientError(error_content, 'get_event_source_mapping')
|
||||
result = boto_lambda.describe_event_source_mapping(
|
||||
UUID=event_source_mapping_ret['UUID'],
|
||||
**conn_parameters)
|
||||
self.assertTrue('error' in result)
|
||||
|
||||
def test_that_when_updating_an_event_source_mapping_succeeds_the_update_event_source_mapping_method_returns_true(self):
|
||||
'''
|
||||
tests True event_source_mapping updated.
|
||||
'''
|
||||
self.conn.update_event_source_mapping.return_value = event_source_mapping_ret
|
||||
result = boto_lambda.update_event_source_mapping(
|
||||
UUID=event_source_mapping_ret['UUID'],
|
||||
FunctionName=event_source_mapping_ret['FunctionArn'],
|
||||
**conn_parameters)
|
||||
|
||||
self.assertTrue(result['updated'])
|
||||
|
||||
def test_that_when_updating_an_event_source_mapping_fails_the_update_event_source_mapping_method_returns_error(self):
|
||||
'''
|
||||
tests False event_source_mapping not updated.
|
||||
'''
|
||||
self.conn.update_event_source_mapping.side_effect = ClientError(error_content, 'update_event_source_mapping')
|
||||
result = boto_lambda.update_event_source_mapping(
|
||||
UUID=event_source_mapping_ret['UUID'],
|
||||
FunctionName=event_source_mapping_ret['FunctionArn'],
|
||||
**conn_parameters)
|
||||
self.assertEqual(result.get('error', {}).get('message'), error_message.format('update_event_source_mapping'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from integration import run_tests # pylint: disable=import-error
|
||||
run_tests(BotoLambdaFunctionTestCase, needs_daemon=False)
|
380
tests/unit/states/boto_lambda_test.py
Normal file
380
tests/unit/states/boto_lambda_test.py
Normal file
@ -0,0 +1,380 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Import Python libs
|
||||
from __future__ import absolute_import
|
||||
from distutils.version import LooseVersion # pylint: disable=import-error,no-name-in-module
|
||||
|
||||
# Import Salt Testing libs
|
||||
from salttesting.unit import skipIf, TestCase
|
||||
from salttesting.mock import NO_MOCK, NO_MOCK_REASON, patch
|
||||
from salttesting.helpers import ensure_in_syspath
|
||||
|
||||
ensure_in_syspath('../../')
|
||||
|
||||
# Import Salt libs
|
||||
import salt.config
|
||||
import salt.loader
|
||||
|
||||
# Import 3rd-party libs
|
||||
import logging
|
||||
|
||||
# Import Mock libraries
|
||||
from salttesting.mock import NO_MOCK, NO_MOCK_REASON, MagicMock, patch
|
||||
|
||||
# pylint: disable=import-error,no-name-in-module
|
||||
from unit.modules.boto_lambda_test import BotoLambdaTestCaseMixin, TempZipFile
|
||||
|
||||
# Import 3rd-party libs
|
||||
try:
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
HAS_BOTO = True
|
||||
except ImportError:
|
||||
HAS_BOTO = False
|
||||
|
||||
# pylint: enable=import-error,no-name-in-module
|
||||
|
||||
# the boto_lambda module relies on the connect_to_region() method
|
||||
# which was added in boto 2.8.0
|
||||
# https://github.com/boto/boto/commit/33ac26b416fbb48a60602542b4ce15dcc7029f12
|
||||
required_boto3_version = '1.2.1'
|
||||
|
||||
region = 'us-east-1'
|
||||
access_key = 'GKTADJGHEIQSXMKKRBJ08H'
|
||||
secret_key = 'askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs'
|
||||
conn_parameters = {'region': region, 'key': access_key, 'keyid': secret_key, 'profile': {}}
|
||||
error_message = 'An error occurred (101) when calling the {0} operation: Test-defined error'
|
||||
error_content = {
|
||||
'Error': {
|
||||
'Code': 101,
|
||||
'Message': "Test-defined error"
|
||||
}
|
||||
}
|
||||
function_ret = dict(FunctionName='testfunction',
|
||||
Runtime='python2.7',
|
||||
Role='arn:aws:iam::1234:role/functionrole',
|
||||
Handler='handler',
|
||||
Description='abcdefg',
|
||||
Timeout=5,
|
||||
MemorySize=128,
|
||||
CodeSha256='abcdef',
|
||||
CodeSize=199,
|
||||
FunctionArn='arn:lambda:us-east-1:1234:Something',
|
||||
LastModified='yes')
|
||||
alias_ret = dict(AliasArn='arn:lambda:us-east-1:1234:Something',
|
||||
Name='testalias',
|
||||
FunctionVersion='3',
|
||||
Description='Alias description')
|
||||
event_source_mapping_ret = dict(UUID='1234-1-123',
|
||||
BatchSize=123,
|
||||
EventSourceArn='arn:aws:dynamodb:us-east-1:1234::Something',
|
||||
FunctionArn='arn:aws:lambda:us-east-1:1234:function:myfunc',
|
||||
LastModified='yes',
|
||||
LastProcessingResult='SUCCESS',
|
||||
State='Enabled',
|
||||
StateTransitionReason='Random')
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
opts = salt.config.DEFAULT_MINION_OPTS
|
||||
context = {}
|
||||
utils = salt.loader.utils(opts, whitelist=['boto3'], context=context)
|
||||
serializers = salt.loader.serializers(opts)
|
||||
funcs = salt.loader.minion_mods(opts, context=context, utils=utils, whitelist=['boto_lambda'])
|
||||
salt_states = salt.loader.states(opts=opts, functions=funcs, utils=utils, whitelist=['boto_lambda'], serializers=serializers)
|
||||
|
||||
|
||||
def _has_required_boto():
|
||||
'''
|
||||
Returns True/False boolean depending on if Boto is installed and correct
|
||||
version.
|
||||
'''
|
||||
if not HAS_BOTO:
|
||||
return False
|
||||
elif LooseVersion(boto3.__version__) < LooseVersion(required_boto3_version):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
class BotoLambdaStateTestCaseBase(TestCase):
|
||||
conn = None
|
||||
|
||||
# Set up MagicMock to replace the boto3 session
|
||||
def setUp(self):
|
||||
context.clear()
|
||||
|
||||
self.patcher = patch('boto3.session.Session')
|
||||
self.addCleanup(self.patcher.stop)
|
||||
mock_session = self.patcher.start()
|
||||
|
||||
session_instance = mock_session.return_value
|
||||
self.conn = MagicMock()
|
||||
session_instance.client.return_value = self.conn
|
||||
|
||||
|
||||
@skipIf(HAS_BOTO is False, 'The boto module must be installed.')
|
||||
@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than'
|
||||
' or equal to version {0}'
|
||||
.format(required_boto3_version))
|
||||
@skipIf(NO_MOCK, NO_MOCK_REASON)
|
||||
class BotoLambdaFunctionTestCase(BotoLambdaStateTestCaseBase, BotoLambdaTestCaseMixin):
|
||||
'''
|
||||
TestCase for salt.modules.boto_lambda state.module
|
||||
'''
|
||||
|
||||
def test_present_when_function_does_not_exist(self):
|
||||
'''
|
||||
Tests present on a function that does not exist.
|
||||
'''
|
||||
self.conn.list_functions.side_effect = [{'Functions': []}, {'Functions': [function_ret]}]
|
||||
self.conn.create_function.return_value = function_ret
|
||||
with patch.dict(funcs, {'boto_iam.get_account_id': MagicMock(return_value='1234')}):
|
||||
with TempZipFile() as zipfile:
|
||||
result = salt_states['boto_lambda.function_present'](
|
||||
'function present',
|
||||
FunctionName=function_ret['FunctionName'],
|
||||
Runtime=function_ret['Runtime'],
|
||||
Role=function_ret['Role'],
|
||||
Handler=function_ret['Handler'],
|
||||
ZipFile=zipfile)
|
||||
|
||||
self.assertTrue(result['result'])
|
||||
self.assertEqual(result['changes']['new']['function']['FunctionName'],
|
||||
function_ret['FunctionName'])
|
||||
|
||||
def test_present_when_function_exists(self):
|
||||
self.conn.list_functions.return_value = {'Functions': [function_ret]}
|
||||
self.conn.update_function_code.return_value = function_ret
|
||||
with patch.dict(funcs, {'boto_iam.get_account_id': MagicMock(return_value='1234')}):
|
||||
with TempZipFile() as zipfile:
|
||||
with patch('hashlib.sha256') as sha256:
|
||||
with patch('os.path.getsize', return_value=199):
|
||||
sha = sha256()
|
||||
digest = sha.digest()
|
||||
encoded = sha.encode()
|
||||
encoded.strip.return_value = function_ret['CodeSha256']
|
||||
result = salt_states['boto_lambda.function_present'](
|
||||
'function present',
|
||||
FunctionName=function_ret['FunctionName'],
|
||||
Runtime=function_ret['Runtime'],
|
||||
Role=function_ret['Role'],
|
||||
Handler=function_ret['Handler'],
|
||||
ZipFile=zipfile,
|
||||
Description=function_ret['Description'],
|
||||
Timeout=function_ret['Timeout'])
|
||||
self.assertTrue(result['result'])
|
||||
self.assertEqual(result['changes'], {})
|
||||
|
||||
def test_present_with_failure(self):
|
||||
self.conn.list_functions.side_effect = [{'Functions': []}, {'Functions': [function_ret]}]
|
||||
self.conn.create_function.side_effect = ClientError(error_content, 'create_function')
|
||||
with patch.dict(funcs, {'boto_iam.get_account_id': MagicMock(return_value='1234')}):
|
||||
with TempZipFile() as zipfile:
|
||||
with patch('hashlib.sha256') as sha256:
|
||||
with patch('os.path.getsize', return_value=199):
|
||||
sha = sha256()
|
||||
digest = sha.digest()
|
||||
encoded = sha.encode()
|
||||
encoded.strip.return_value = function_ret['CodeSha256']
|
||||
result = salt_states['boto_lambda.function_present'](
|
||||
'function present',
|
||||
FunctionName=function_ret['FunctionName'],
|
||||
Runtime=function_ret['Runtime'],
|
||||
Role=function_ret['Role'],
|
||||
Handler=function_ret['Handler'],
|
||||
ZipFile=zipfile,
|
||||
Description=function_ret['Description'],
|
||||
Timeout=function_ret['Timeout'])
|
||||
self.assertFalse(result['result'])
|
||||
self.assertTrue('An error occurred' in result['comment'])
|
||||
|
||||
def test_absent_when_function_does_not_exist(self):
|
||||
'''
|
||||
Tests absent on a function that does not exist.
|
||||
'''
|
||||
self.conn.list_functions.return_value = {'Functions': [function_ret]}
|
||||
result = salt_states['boto_lambda.function_absent']('test', 'myfunc')
|
||||
self.assertTrue(result['result'])
|
||||
self.assertEqual(result['changes'], {})
|
||||
|
||||
def test_absent_when_function_exists(self):
|
||||
self.conn.list_functions.return_value = {'Functions': [function_ret]}
|
||||
result = salt_states['boto_lambda.function_absent']('test', function_ret['FunctionName'])
|
||||
self.assertTrue(result['result'])
|
||||
self.assertEqual(result['changes']['new']['function'], None)
|
||||
|
||||
def test_absent_with_failure(self):
|
||||
self.conn.list_functions.return_value = {'Functions': [function_ret]}
|
||||
self.conn.delete_function.side_effect = ClientError(error_content, 'delete_function')
|
||||
result = salt_states['boto_lambda.function_absent']('test', function_ret['FunctionName'])
|
||||
self.assertFalse(result['result'])
|
||||
self.assertTrue('An error occurred' in result['comment'])
|
||||
|
||||
|
||||
@skipIf(HAS_BOTO is False, 'The boto module must be installed.')
|
||||
@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than'
|
||||
' or equal to version {0}'
|
||||
.format(required_boto3_version))
|
||||
@skipIf(NO_MOCK, NO_MOCK_REASON)
|
||||
class BotoLambdaAliasTestCase(BotoLambdaStateTestCaseBase, BotoLambdaTestCaseMixin):
|
||||
'''
|
||||
TestCase for salt.modules.boto_lambda state.module aliases
|
||||
'''
|
||||
|
||||
def test_present_when_alias_does_not_exist(self):
|
||||
'''
|
||||
Tests present on a alias that does not exist.
|
||||
'''
|
||||
self.conn.list_aliases.side_effect = [{'Aliases': []}, {'Aliases': [alias_ret]}]
|
||||
self.conn.create_alias.return_value = alias_ret
|
||||
result = salt_states['boto_lambda.alias_present'](
|
||||
'alias present',
|
||||
FunctionName='testfunc',
|
||||
Name=alias_ret['Name'],
|
||||
FunctionVersion=alias_ret['FunctionVersion'])
|
||||
|
||||
self.assertTrue(result['result'])
|
||||
self.assertEqual(result['changes']['new']['alias']['Name'],
|
||||
alias_ret['Name'])
|
||||
|
||||
def test_present_when_alias_exists(self):
|
||||
self.conn.list_aliases.return_value = {'Aliases': [alias_ret]}
|
||||
self.conn.create_alias.return_value = alias_ret
|
||||
result = salt_states['boto_lambda.alias_present'](
|
||||
'alias present',
|
||||
FunctionName='testfunc',
|
||||
Name=alias_ret['Name'],
|
||||
FunctionVersion=alias_ret['FunctionVersion'],
|
||||
Description=alias_ret['Description'])
|
||||
self.assertTrue(result['result'])
|
||||
self.assertEqual(result['changes'], {})
|
||||
|
||||
def test_present_with_failure(self):
|
||||
self.conn.list_aliases.side_effect = [{'Aliases': []}, {'Aliases': [alias_ret]}]
|
||||
self.conn.create_alias.side_effect = ClientError(error_content, 'create_alias')
|
||||
result = salt_states['boto_lambda.alias_present'](
|
||||
'alias present',
|
||||
FunctionName='testfunc',
|
||||
Name=alias_ret['Name'],
|
||||
FunctionVersion=alias_ret['FunctionVersion'])
|
||||
self.assertFalse(result['result'])
|
||||
self.assertTrue('An error occurred' in result['comment'])
|
||||
|
||||
def test_absent_when_alias_does_not_exist(self):
|
||||
'''
|
||||
Tests absent on a alias that does not exist.
|
||||
'''
|
||||
self.conn.list_aliases.return_value = {'Aliases': [alias_ret]}
|
||||
result = salt_states['boto_lambda.alias_absent'](
|
||||
'alias absent',
|
||||
FunctionName='testfunc',
|
||||
Name='myalias')
|
||||
self.assertTrue(result['result'])
|
||||
self.assertEqual(result['changes'], {})
|
||||
|
||||
def test_absent_when_alias_exists(self):
|
||||
self.conn.list_aliases.return_value = {'Aliases': [alias_ret]}
|
||||
result = salt_states['boto_lambda.alias_absent'](
|
||||
'alias absent',
|
||||
FunctionName='testfunc',
|
||||
Name=alias_ret['Name'])
|
||||
self.assertTrue(result['result'])
|
||||
self.assertEqual(result['changes']['new']['alias'], None)
|
||||
|
||||
def test_absent_with_failure(self):
|
||||
self.conn.list_aliases.return_value = {'Aliases': [alias_ret]}
|
||||
self.conn.delete_alias.side_effect = ClientError(error_content, 'delete_alias')
|
||||
result = salt_states['boto_lambda.alias_absent'](
|
||||
'alias absent',
|
||||
FunctionName='testfunc',
|
||||
Name=alias_ret['Name'])
|
||||
self.assertFalse(result['result'])
|
||||
self.assertTrue('An error occurred' in result['comment'])
|
||||
|
||||
|
||||
@skipIf(HAS_BOTO is False, 'The boto module must be installed.')
|
||||
@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than'
|
||||
' or equal to version {0}'
|
||||
.format(required_boto3_version))
|
||||
@skipIf(NO_MOCK, NO_MOCK_REASON)
|
||||
class BotoLambdaEventSourceMappingTestCase(BotoLambdaStateTestCaseBase, BotoLambdaTestCaseMixin):
|
||||
'''
|
||||
TestCase for salt.modules.boto_lambda state.module event_source_mappings
|
||||
'''
|
||||
|
||||
def test_present_when_event_source_mapping_does_not_exist(self):
|
||||
'''
|
||||
Tests present on a event_source_mapping that does not exist.
|
||||
'''
|
||||
self.conn.list_event_source_mappings.side_effect = [{'EventSourceMappings': []}, {'EventSourceMappings': [event_source_mapping_ret]}]
|
||||
self.conn.get_event_source_mapping.return_value = event_source_mapping_ret
|
||||
self.conn.create_event_source_mapping.return_value = event_source_mapping_ret
|
||||
result = salt_states['boto_lambda.event_source_mapping_present'](
|
||||
'event source mapping present',
|
||||
EventSourceArn=event_source_mapping_ret['EventSourceArn'],
|
||||
FunctionName='myfunc',
|
||||
StartingPosition='LATEST')
|
||||
|
||||
self.assertTrue(result['result'])
|
||||
self.assertEqual(result['changes']['new']['event_source_mapping']['UUID'],
|
||||
event_source_mapping_ret['UUID'])
|
||||
|
||||
def test_present_when_event_source_mapping_exists(self):
|
||||
self.conn.list_event_source_mappings.return_value = {'EventSourceMappings': [event_source_mapping_ret]}
|
||||
self.conn.get_event_source_mapping.return_value = event_source_mapping_ret
|
||||
self.conn.create_event_source_mapping.return_value = event_source_mapping_ret
|
||||
result = salt_states['boto_lambda.event_source_mapping_present'](
|
||||
'event source mapping present',
|
||||
EventSourceArn=event_source_mapping_ret['EventSourceArn'],
|
||||
FunctionName=event_source_mapping_ret['FunctionArn'],
|
||||
StartingPosition='LATEST',
|
||||
BatchSize=event_source_mapping_ret['BatchSize'])
|
||||
self.assertTrue(result['result'])
|
||||
self.assertEqual(result['changes'], {})
|
||||
|
||||
def test_present_with_failure(self):
|
||||
self.conn.list_event_source_mappings.side_effect = [{'EventSourceMappings': []}, {'EventSourceMappings': [event_source_mapping_ret]}]
|
||||
self.conn.get_event_source_mapping.return_value = event_source_mapping_ret
|
||||
self.conn.create_event_source_mapping.side_effect = ClientError(error_content, 'create_event_source_mapping')
|
||||
result = salt_states['boto_lambda.event_source_mapping_present'](
|
||||
'event source mapping present',
|
||||
EventSourceArn=event_source_mapping_ret['EventSourceArn'],
|
||||
FunctionName=event_source_mapping_ret['FunctionArn'],
|
||||
StartingPosition='LATEST',
|
||||
BatchSize=event_source_mapping_ret['BatchSize'])
|
||||
self.assertFalse(result['result'])
|
||||
self.assertTrue('An error occurred' in result['comment'])
|
||||
|
||||
def test_absent_when_event_source_mapping_does_not_exist(self):
|
||||
'''
|
||||
Tests absent on a event_source_mapping that does not exist.
|
||||
'''
|
||||
self.conn.list_event_source_mappings.return_value = {'EventSourceMappings': []}
|
||||
result = salt_states['boto_lambda.event_source_mapping_absent'](
|
||||
'event source mapping absent',
|
||||
EventSourceArn=event_source_mapping_ret['EventSourceArn'],
|
||||
FunctionName='myfunc')
|
||||
self.assertTrue(result['result'])
|
||||
self.assertEqual(result['changes'], {})
|
||||
|
||||
def test_absent_when_event_source_mapping_exists(self):
|
||||
self.conn.list_event_source_mappings.return_value = {'EventSourceMappings': [event_source_mapping_ret]}
|
||||
self.conn.get_event_source_mapping.return_value = event_source_mapping_ret
|
||||
result = salt_states['boto_lambda.event_source_mapping_absent'](
|
||||
'event source mapping absent',
|
||||
EventSourceArn=event_source_mapping_ret['EventSourceArn'],
|
||||
FunctionName='myfunc')
|
||||
self.assertTrue(result['result'])
|
||||
self.assertEqual(result['changes']['new']['event_source_mapping'], None)
|
||||
|
||||
def test_absent_with_failure(self):
|
||||
self.conn.list_event_source_mappings.return_value = {'EventSourceMappings': [event_source_mapping_ret]}
|
||||
self.conn.get_event_source_mapping.return_value = event_source_mapping_ret
|
||||
self.conn.delete_event_source_mapping.side_effect = ClientError(error_content, 'delete_event_source_mapping')
|
||||
result = salt_states['boto_lambda.event_source_mapping_absent'](
|
||||
'event source mapping absent',
|
||||
EventSourceArn=event_source_mapping_ret['EventSourceArn'],
|
||||
FunctionName='myfunc')
|
||||
self.assertFalse(result['result'])
|
||||
self.assertTrue('An error occurred' in result['comment'])
|
Loading…
Reference in New Issue
Block a user