Merge pull request #29573 from kraney/boto_lambda

An implementation of support for AWS Lambda
This commit is contained in:
Mike Place 2015-12-10 13:01:26 -07:00
commit 3514765d33
5 changed files with 2933 additions and 0 deletions

782
salt/modules/boto_lambda.py Normal file
View 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
View 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
View 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

View 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)

View 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'])