1#!/usr/bin/python
2# This file is part of Ansible
3#
4# Ansible is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, either version 3 of the License, or
7# (at your option) any later version.
8#
9# Ansible is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
16
17ANSIBLE_METADATA = {'metadata_version': '1.1',
18                    'status': ['preview'],
19                    'supported_by': 'community'}
20
21
22DOCUMENTATION = '''
23---
24module: lambda_info
25short_description: Gathers AWS Lambda function details
26description:
27  - Gathers various details related to Lambda functions, including aliases, versions and event source mappings.
28    Use module M(lambda) to manage the lambda function itself, M(lambda_alias) to manage function aliases and
29    M(lambda_event) to manage lambda event source mappings.
30
31version_added: "2.9"
32
33options:
34  query:
35    description:
36      - Specifies the resource type for which to gather information.  Leave blank to retrieve all information.
37    required: true
38    choices: [ "aliases", "all", "config", "mappings", "policy", "versions" ]
39    default: "all"
40  function_name:
41    description:
42      - The name of the lambda function for which information is requested.
43    aliases: [ "function", "name"]
44  event_source_arn:
45    description:
46      - For query type 'mappings', this is the Amazon Resource Name (ARN) of the Amazon Kinesis or DynamoDB stream.
47author: Pierre Jodouin (@pjodouin)
48requirements:
49    - boto3
50extends_documentation_fragment:
51  - aws
52  - ec2
53'''
54
55EXAMPLES = '''
56---
57# Simple example of listing all info for a function
58- name: List all for a specific function
59  lambda_info:
60    query: all
61    function_name: myFunction
62  register: my_function_details
63# List all versions of a function
64- name: List function versions
65  lambda_info:
66    query: versions
67    function_name: myFunction
68  register: my_function_versions
69# List all lambda function versions
70- name: List all function
71  lambda_info:
72    query: all
73    max_items: 20
74  register: output
75- name: show Lambda information
76  debug:
77    msg: "{{ output['function'] }}"
78'''
79
80RETURN = '''
81---
82function:
83    description: lambda function list
84    returned: success
85    type: dict
86function.TheName:
87    description: lambda function information, including event, mapping, and version information
88    returned: success
89    type: dict
90'''
91
92from ansible.module_utils.aws.core import AnsibleAWSModule
93from ansible.module_utils.ec2 import camel_dict_to_snake_dict, get_aws_connection_info, boto3_conn
94import json
95import datetime
96import sys
97import re
98
99
100try:
101    from botocore.exceptions import ClientError
102except ImportError:
103    pass  # protected by AnsibleAWSModule
104
105
106def fix_return(node):
107    """
108    fixup returned dictionary
109
110    :param node:
111    :return:
112    """
113
114    if isinstance(node, datetime.datetime):
115        node_value = str(node)
116
117    elif isinstance(node, list):
118        node_value = [fix_return(item) for item in node]
119
120    elif isinstance(node, dict):
121        node_value = dict([(item, fix_return(node[item])) for item in node.keys()])
122
123    else:
124        node_value = node
125
126    return node_value
127
128
129def alias_details(client, module):
130    """
131    Returns list of aliases for a specified function.
132
133    :param client: AWS API client reference (boto3)
134    :param module: Ansible module reference
135    :return dict:
136    """
137
138    lambda_info = dict()
139
140    function_name = module.params.get('function_name')
141    if function_name:
142        params = dict()
143        if module.params.get('max_items'):
144            params['MaxItems'] = module.params.get('max_items')
145
146        if module.params.get('next_marker'):
147            params['Marker'] = module.params.get('next_marker')
148        try:
149            lambda_info.update(aliases=client.list_aliases(FunctionName=function_name, **params)['Aliases'])
150        except ClientError as e:
151            if e.response['Error']['Code'] == 'ResourceNotFoundException':
152                lambda_info.update(aliases=[])
153            else:
154                module.fail_json_aws(e, msg="Trying to get aliases")
155    else:
156        module.fail_json(msg='Parameter function_name required for query=aliases.')
157
158    return {function_name: camel_dict_to_snake_dict(lambda_info)}
159
160
161def all_details(client, module):
162    """
163    Returns all lambda related facts.
164
165    :param client: AWS API client reference (boto3)
166    :param module: Ansible module reference
167    :return dict:
168    """
169
170    if module.params.get('max_items') or module.params.get('next_marker'):
171        module.fail_json(msg='Cannot specify max_items nor next_marker for query=all.')
172
173    lambda_info = dict()
174
175    function_name = module.params.get('function_name')
176    if function_name:
177        lambda_info[function_name] = {}
178        lambda_info[function_name].update(config_details(client, module)[function_name])
179        lambda_info[function_name].update(alias_details(client, module)[function_name])
180        lambda_info[function_name].update(policy_details(client, module)[function_name])
181        lambda_info[function_name].update(version_details(client, module)[function_name])
182        lambda_info[function_name].update(mapping_details(client, module)[function_name])
183    else:
184        lambda_info.update(config_details(client, module))
185
186    return lambda_info
187
188
189def config_details(client, module):
190    """
191    Returns configuration details for one or all lambda functions.
192
193    :param client: AWS API client reference (boto3)
194    :param module: Ansible module reference
195    :return dict:
196    """
197
198    lambda_info = dict()
199
200    function_name = module.params.get('function_name')
201    if function_name:
202        try:
203            lambda_info.update(client.get_function_configuration(FunctionName=function_name))
204        except ClientError as e:
205            if e.response['Error']['Code'] == 'ResourceNotFoundException':
206                lambda_info.update(function={})
207            else:
208                module.fail_json_aws(e, msg="Trying to get {0} configuration".format(function_name))
209    else:
210        params = dict()
211        if module.params.get('max_items'):
212            params['MaxItems'] = module.params.get('max_items')
213
214        if module.params.get('next_marker'):
215            params['Marker'] = module.params.get('next_marker')
216
217        try:
218            lambda_info.update(function_list=client.list_functions(**params)['Functions'])
219        except ClientError as e:
220            if e.response['Error']['Code'] == 'ResourceNotFoundException':
221                lambda_info.update(function_list=[])
222            else:
223                module.fail_json_aws(e, msg="Trying to get function list")
224
225        functions = dict()
226        for func in lambda_info.pop('function_list', []):
227            functions[func['FunctionName']] = camel_dict_to_snake_dict(func)
228        return functions
229
230    return {function_name: camel_dict_to_snake_dict(lambda_info)}
231
232
233def mapping_details(client, module):
234    """
235    Returns all lambda event source mappings.
236
237    :param client: AWS API client reference (boto3)
238    :param module: Ansible module reference
239    :return dict:
240    """
241
242    lambda_info = dict()
243    params = dict()
244    function_name = module.params.get('function_name')
245
246    if function_name:
247        params['FunctionName'] = module.params.get('function_name')
248
249    if module.params.get('event_source_arn'):
250        params['EventSourceArn'] = module.params.get('event_source_arn')
251
252    if module.params.get('max_items'):
253        params['MaxItems'] = module.params.get('max_items')
254
255    if module.params.get('next_marker'):
256        params['Marker'] = module.params.get('next_marker')
257
258    try:
259        lambda_info.update(mappings=client.list_event_source_mappings(**params)['EventSourceMappings'])
260    except ClientError as e:
261        if e.response['Error']['Code'] == 'ResourceNotFoundException':
262            lambda_info.update(mappings=[])
263        else:
264            module.fail_json_aws(e, msg="Trying to get source event mappings")
265
266    if function_name:
267        return {function_name: camel_dict_to_snake_dict(lambda_info)}
268
269    return camel_dict_to_snake_dict(lambda_info)
270
271
272def policy_details(client, module):
273    """
274    Returns policy attached to a lambda function.
275
276    :param client: AWS API client reference (boto3)
277    :param module: Ansible module reference
278    :return dict:
279    """
280
281    if module.params.get('max_items') or module.params.get('next_marker'):
282        module.fail_json(msg='Cannot specify max_items nor next_marker for query=policy.')
283
284    lambda_info = dict()
285
286    function_name = module.params.get('function_name')
287    if function_name:
288        try:
289            # get_policy returns a JSON string so must convert to dict before reassigning to its key
290            lambda_info.update(policy=json.loads(client.get_policy(FunctionName=function_name)['Policy']))
291        except ClientError as e:
292            if e.response['Error']['Code'] == 'ResourceNotFoundException':
293                lambda_info.update(policy={})
294            else:
295                module.fail_json_aws(e, msg="Trying to get {0} policy".format(function_name))
296    else:
297        module.fail_json(msg='Parameter function_name required for query=policy.')
298
299    return {function_name: camel_dict_to_snake_dict(lambda_info)}
300
301
302def version_details(client, module):
303    """
304    Returns all lambda function versions.
305
306    :param client: AWS API client reference (boto3)
307    :param module: Ansible module reference
308    :return dict:
309    """
310
311    lambda_info = dict()
312
313    function_name = module.params.get('function_name')
314    if function_name:
315        params = dict()
316        if module.params.get('max_items'):
317            params['MaxItems'] = module.params.get('max_items')
318
319        if module.params.get('next_marker'):
320            params['Marker'] = module.params.get('next_marker')
321
322        try:
323            lambda_info.update(versions=client.list_versions_by_function(FunctionName=function_name, **params)['Versions'])
324        except ClientError as e:
325            if e.response['Error']['Code'] == 'ResourceNotFoundException':
326                lambda_info.update(versions=[])
327            else:
328                module.fail_json_aws(e, msg="Trying to get {0} versions".format(function_name))
329    else:
330        module.fail_json(msg='Parameter function_name required for query=versions.')
331
332    return {function_name: camel_dict_to_snake_dict(lambda_info)}
333
334
335def main():
336    """
337    Main entry point.
338
339    :return dict: ansible facts
340    """
341    argument_spec = dict(
342        function_name=dict(required=False, default=None, aliases=['function', 'name']),
343        query=dict(required=False, choices=['aliases', 'all', 'config', 'mappings', 'policy', 'versions'], default='all'),
344        event_source_arn=dict(required=False, default=None)
345    )
346
347    module = AnsibleAWSModule(
348        argument_spec=argument_spec,
349        supports_check_mode=True,
350        mutually_exclusive=[],
351        required_together=[]
352    )
353
354    # validate function_name if present
355    function_name = module.params['function_name']
356    if function_name:
357        if not re.search(r"^[\w\-:]+$", function_name):
358            module.fail_json(
359                msg='Function name {0} is invalid. Names must contain only alphanumeric characters and hyphens.'.format(function_name)
360            )
361        if len(function_name) > 64:
362            module.fail_json(msg='Function name "{0}" exceeds 64 character limit'.format(function_name))
363
364    try:
365        region, endpoint, aws_connect_kwargs = get_aws_connection_info(module, boto3=True)
366        aws_connect_kwargs.update(dict(region=region,
367                                       endpoint=endpoint,
368                                       conn_type='client',
369                                       resource='lambda'
370                                       ))
371        client = boto3_conn(module, **aws_connect_kwargs)
372    except ClientError as e:
373        module.fail_json_aws(e, "trying to set up boto connection")
374
375    this_module = sys.modules[__name__]
376
377    invocations = dict(
378        aliases='alias_details',
379        all='all_details',
380        config='config_details',
381        mappings='mapping_details',
382        policy='policy_details',
383        versions='version_details',
384    )
385
386    this_module_function = getattr(this_module, invocations[module.params['query']])
387    all_facts = fix_return(this_module_function(client, module))
388
389    results = dict(function=all_facts, changed=False)
390
391    if module.check_mode:
392        results['msg'] = 'Check mode set but ignored for fact gathering only.'
393
394    module.exit_json(**results)
395
396
397if __name__ == '__main__':
398    main()
399