From d31ded47fb0f5b198c2d86d17c7ab165c945fe6a Mon Sep 17 00:00:00 2001 From: mikedlr Date: Tue, 6 Feb 2018 22:41:46 +0000 Subject: [PATCH] Aws ssm multiple fixes (#35569) * aws ssm parameter lookup test case - fails demonstrating no exception when parameter missing * aws ssm parameter lookup - fail in case parameter doesn't exist * aws ssm parameter lookup test case - failing case for nice return from path lookup * aws ssm parameter lookup - convert incoming taglist to a key-value dictionary * aws ssm parameter lookup - pep8 / style clean up * aws_ssm lookup plugin rewrite for more standard interface * aws_ssm module and lookup - introduce integration test and fix: * aws_ssm module and lookup - error case integraton test and many PEP8 and other cleanups * aws ssm parameter lookup - Various fixes in response to review + recursive fix & test * aws ssm parameter lookup - more in response to review - shertel/abadger * aws ssm parameter lookup unit test - move to mocker according to abadger * aws ssm parameter lookup - integrate with new documentation fragment * aws ssm parameter lookup - accept either aws_profile or boto_profile * aws ssm parameter lookup - eliminate lookup document fragment until env vars are fixed later --- .../cloud/amazon/aws_ssm_parameter_store.py | 87 +++---- lib/ansible/plugins/lookup/aws_ssm.py | 240 ++++++++++-------- .../targets/aws_ssm_parameters/aliases | 2 + .../aws_ssm_parameters/defaults/main.yml | 3 + .../targets/aws_ssm_parameters/tasks/main.yml | 136 ++++++++++ test/units/plugins/lookup/test_aws_ssm.py | 137 ++++++++++ 6 files changed, 451 insertions(+), 154 deletions(-) create mode 100644 test/integration/targets/aws_ssm_parameters/aliases create mode 100644 test/integration/targets/aws_ssm_parameters/defaults/main.yml create mode 100644 test/integration/targets/aws_ssm_parameters/tasks/main.yml create mode 100644 test/units/plugins/lookup/test_aws_ssm.py diff --git a/lib/ansible/modules/cloud/amazon/aws_ssm_parameter_store.py b/lib/ansible/modules/cloud/amazon/aws_ssm_parameter_store.py index 50adaea56b..d51339908a 100644 --- a/lib/ansible/modules/cloud/amazon/aws_ssm_parameter_store.py +++ b/lib/ansible/modules/cloud/amazon/aws_ssm_parameter_store.py @@ -60,7 +60,9 @@ options: description: - region. required: false -author: Bill Wang (ozbillwang@gmail.com) +author: + - Bill Wang (ozbillwang@gmail.com) + - Michael De La Rue (@mikedlr) extends_documentation_fragment: aws requirements: [ botocore, boto3 ] ''' @@ -107,13 +109,11 @@ delete_parameter: type: dictionary ''' -import traceback -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import HAS_BOTO3, camel_dict_to_snake_dict -from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info +from ansible.module_utils.aws.core import AnsibleAWSModule +from ansible.module_utils.ec2 import boto3_conn, get_aws_connection_info try: - from botocore.exceptions import ClientError, NoCredentialsError + from botocore.exceptions import ClientError except ImportError: pass # will be captured by imported HAS_BOTO3 @@ -139,64 +139,54 @@ def create_update_parameter(client, module): response = client.put_parameter(**args) changed = True except ClientError as e: - module.fail_json(msg=e.message, exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) + module.fail_json_aws(e, msg="setting parameter") return changed, response def delete_parameter(client, module): - changed = False response = {} try: - get_response = client.get_parameters( - Names=[module.params.get('name')] + response = client.delete_parameter( + Name=module.params.get('name') ) except ClientError as e: - module.fail_json(msg=e.message, exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) + if e.response['Error']['Code'] == 'ParameterNotFound': + return False, {} + module.fail_json_aws(e, msg="deleting parameter") - if get_response['Parameters']: - try: - response = client.delete_parameter( - Name=module.params.get('name') - ) - changed = True - except ClientError as e: - module.fail_json(msg=e.message, exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) + return True, response - return changed, response + +def setup_client(module): + region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) + connection = boto3_conn(module, conn_type='client', resource='ssm', region=region, endpoint=ec2_url, **aws_connect_params) + return connection + + +def setup_module_object(): + argument_spec = dict( + name=dict(required=True), + description=dict(), + value=dict(required=False), + state=dict(default='present', choices=['present', 'absent']), + string_type=dict(default='String', choices=['String', 'StringList', 'SecureString']), + decryption=dict(default=True, type='bool'), + key_id=dict(default="alias/aws/ssm"), + overwrite=dict(default=True, type='bool'), + region=dict(required=False), + ) + + return AnsibleAWSModule( + argument_spec=argument_spec, + ) def main(): - - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - name=dict(required=True), - description=dict(), - value=dict(required=False), - state=dict(default='present', choices=['present', 'absent']), - string_type=dict(default='String', choices=['String', 'StringList', 'SecureString']), - decryption=dict(default=True, type='bool'), - key_id=dict(default="alias/aws/ssm"), - overwrite=dict(default=True, type='bool'), - region=dict(required=False), - ) - ) - - module = AnsibleModule(argument_spec=argument_spec) - - if not HAS_BOTO3: - module.fail_json(msg='boto3 are required.') + module = setup_module_object() state = module.params.get('state') - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - client = boto3_conn(module, conn_type='client', resource='ssm', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except NoCredentialsError as e: - module.fail_json(msg="Can't authorize connection - %s" % str(e)) + client = setup_client(module) invocations = { "present": create_update_parameter, @@ -205,5 +195,6 @@ def main(): (changed, response) = invocations[state](client, module) module.exit_json(changed=changed, response=response) + if __name__ == '__main__': main() diff --git a/lib/ansible/plugins/lookup/aws_ssm.py b/lib/ansible/plugins/lookup/aws_ssm.py index 2ad3be7e62..537c6aa185 100644 --- a/lib/ansible/plugins/lookup/aws_ssm.py +++ b/lib/ansible/plugins/lookup/aws_ssm.py @@ -1,5 +1,6 @@ # (c) 2016, Bill Wang # (c) 2017, Marat Bakeev +# (c) 2018, Michael De La Rue # (c) 2017 Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) @@ -7,33 +8,42 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = ''' - lookup: aws_ssm - author: - - Bill Wang - - Marat Bakeev - version_added: 2.5 - short_description: Get the value for a SSM parameter. - description: - - Get the value for an Amazon Simple Systems Manager parameter or a heirarchy of parameters. The first - argument you pass the lookup can either be a parameter name or a hierarchy of parameters. Hierarchies start - with a forward slash and end with the parameter name. Up to 5 layers may be specified. - options: - aws_profile: - description: The boto profile to use. You may use environment variables or the default profile as an alternative. - region: - description: The region to use. You may use environment variables ar the default profile's region as an alternative. - decrypt: - description: A boolean to indicate whether to decrypt the parameter. - default: false - bypath: - description: A boolean to indicate whether the parameter is provided as a hierarchy. - default: false - recursive: - description: A boolean to indicate whether to retrieve all parameters within a hierarchy. - default: false - shortnames: - description: Indicates whether to return the shortened name if using a parameter hierarchy. - default: false +lookup: aws_ssm +author: + - Bill Wang + - Marat Bakeev + - Michael De La Rue +version_added: 2.5 +requirements: + - boto3 + - botocore +short_description: Get the value for a SSM parameter or all parameters under a path. +description: + - Get the value for an Amazon Simple Systems Manager parameter or a heirarchy of parameters. + The first argument you pass the lookup can either be a parameter name or a hierarchy of + parameters. Hierarchies start with a forward slash and end with the parameter name. Up to + 5 layers may be specified. + - When explicitly looking up a parameter by name the parameter being missing will be an error. + - When looking up a path for parameters under it a dictionary will be returned for each path. + If there is no parameter under that path then the return will be successful but the + dictionary will be empty. +options: + decrypt: + description: A boolean to indicate whether to decrypt the parameter. + default: false + type: boolean + bypath: + description: A boolean to indicate whether the parameter is provided as a hierarchy. + default: false + type: boolean + recursive: + description: A boolean to indicate whether to retrieve all parameters within a hierarchy. + default: false + type: boolean + shortnames: + description: Indicates whether to return the name only without path if using a parameter hierarchy. + default: false + type: boolean ''' EXAMPLES = ''' @@ -45,130 +55,148 @@ EXAMPLES = ''' debug: msg="{{ lookup('aws_ssm', 'NoKey') }}" - name: lookup ssm parameter store in nominated region - debug: msg="{{ lookup('aws_ssm', 'Hello', 'region=us-east-2' ) }}" + debug: msg="{{ lookup('aws_ssm', 'Hello', region=us-east-2 ) }}" - name: lookup ssm parameter store without decrypted - debug: msg="{{ lookup('aws_ssm', 'Hello', 'decrypt=False' ) }}" + debug: msg="{{ lookup('aws_ssm', 'Hello', decrypt=False ) }}" - name: lookup ssm parameter store in nominated aws profile - debug: msg="{{ lookup('aws_ssm', 'Hello', 'aws_profile=myprofile' ) }}" + debug: msg="{{ lookup('aws_ssm', 'Hello', aws_profile=myprofile ) }}" - name: lookup ssm parameter store with all options. - debug: msg="{{ lookup('aws_ssm', 'Hello', 'decrypt=false', 'region=us-east-2', 'aws_profile=myprofile') }}" + debug: msg="{{ lookup('aws_ssm', 'Hello', decrypt=false, region=us-east-2, aws_profile=myprofile') }}" - name: return a dictionary of ssm parameters from a hierarchy path - debug: msg="{{ lookup('aws_ssm', '/PATH/to/params', 'region=ap-southeast-2', 'bypath', 'recursive=true' ) }}" + debug: msg="{{ lookup('aws_ssm', '/PATH/to/params', region=ap-southeast-2, bypath=true, recursive=true' ) }}" - name: return a dictionary of ssm parameters from a hierarchy path with shortened names (param instead of /PATH/to/param) - debug: msg="{{ lookup('aws_ssm', '/PATH/to/params', 'region=ap-southeast-2', 'shortnames', 'bypath', 'recursive=true' ) }}" + debug: msg="{{ lookup('aws_ssm', '/PATH/to/params', region=ap-southeast-2, shortnames=true, bypath=true, recursive=true ) }}" - name: Iterate over a parameter hierarchy - debug: msg='key contains {{item.Name }} with value {{item.Value}} ' - with_aws_ssm: - - '/TEST/test-list' - - 'region=ap-southeast-2' - - 'bypath' + debug: msg='key contains {{item.Name}} with value {{item.Value}} ' + loop: '{{ query("aws_ssm", "/TEST/test-list", region="ap-southeast-2", bypath=true) }}' + ''' -from ansible.module_utils.ec2 import HAS_BOTO3 +from ansible.module_utils._text import to_native +from ansible.module_utils.ec2 import HAS_BOTO3, boto3_tag_list_to_ansible_dict from ansible.errors import AnsibleError from ansible.plugins.lookup import LookupBase -from ansible.module_utils.parsing.convert_bool import boolean + +try: + from __main__ import display +except ImportError: + from ansible.utils.display import Display + display = Display() try: from botocore.exceptions import ClientError + import botocore import boto3 except ImportError: pass # will be captured by imported HAS_BOTO3 -class LookupModule(LookupBase): - def run(self, terms, variables, **kwargs): - ''' - :param terms: a list of plugin options - e.g. ['parameter_name', 'region=us-east-1', 'aws_profile=profile', 'decrypt=false'] - :param variables: config variables - :return The value of the SSM parameter or None - ''' +def _boto3_conn(region, credentials): + if 'boto_profile' in credentials: + boto_profile = credentials.pop('boto_profile') + else: + boto_profile = None - ret = {} - response = {} - session = {} - ssm_dict = {} - lparams = {} + try: + connection = boto3.session.Session(profile_name=boto_profile).client('ssm', region, **credentials) + except (botocore.exceptions.ProfileNotFound, botocore.exceptions.PartialCredentialsError): + if boto_profile: + try: + connection = boto3.session.Session(profile_name=boto_profile).client('ssm', region) + # FIXME: we should probably do better passing on of the error information + except (botocore.exceptions.ProfileNotFound, botocore.exceptions.PartialCredentialsError): + raise AnsibleError("Insufficient credentials found.") + else: + raise AnsibleError("Insufficient credentials found.") + return connection + + +class LookupModule(LookupBase): + def run(self, terms, variables=None, boto_profile=None, aws_profile=None, + aws_secret_key=None, aws_access_key=None, aws_security_token=None, region=None, + bypath=False, shortnames=False, recursive=False, decrypt=True): + ''' + :arg terms: a list of lookups to run. + e.g. ['parameter_name', 'parameter_name_too' ] + :kwarg variables: ansible variables active at the time of the lookup + :kwarg aws_secret_key: identity of the AWS key to use + :kwarg aws_access_key: AWS seret key (matching identity) + :kwarg aws_security_token: AWS session key if using STS + :kwarg decrypt: Set to True to get decrypted parameters + :kwarg region: AWS region in which to do the lookup + :kwarg bypath: Set to True to do a lookup of variables under a path + :kwarg recursive: Set to True to recurse below the path (requires bypath=True) + :returns: A list of parameter values or a list of dictionaries if bypath=True. + ''' if not HAS_BOTO3: - raise AnsibleError('botocore and boto3 are required.') + raise AnsibleError('botocore and boto3 are required for aws_ssm lookup.') - ssm_dict['WithDecryption'] = True + ret = [] + response = {} + ssm_dict = {} - # check if option 'bypath' is specified, while still allowing to have a parameter with the same name - if 'bypath' in terms[1:]: - ssm_dict['Path'] = terms[0] - del terms[terms[1:].index('bypath') + 1] + credentials = {} + if aws_profile: + credentials['boto_profile'] = aws_profile else: - ssm_dict['Names'] = [terms[0]] + credentials['boto_profile'] = boto_profile + credentials['aws_secret_access_key'] = aws_secret_key + credentials['aws_access_key_id'] = aws_access_key + credentials['aws_session_token'] = aws_security_token - # Option to return short parameter names in by path lookups - if 'shortnames' in terms[1:]: - lparams['shortnames'] = True - del terms[terms[1:].index('shortnames') + 1] + client = _boto3_conn(region, credentials) - if len(terms) > 1: - for param in terms[1:]: - if "=" in param: - key, value = param.split('=') - else: - raise AnsibleError("ssm parameter store plugin needs key=value pairs, but received %s" % param) + ssm_dict['WithDecryption'] = decrypt - if key == "region" or key == "aws_profile": - session[key] = value - - # recurse or not - if key == "recursive" and boolean(value): - ssm_dict['Recursive'] = True - - # decrypt the value or not - if key == "decrypt" and boolean(value): - ssm_dict['WithDecryption'] = False - - if "aws_profile" in session: - boto3.setup_default_session(profile_name=session['aws_profile']) - - if "region" in session: - client = boto3.client('ssm', region_name=session['region']) - else: - client = boto3.client('ssm') - - try: - # Lookup by path - if 'Path' in ssm_dict: - response = client.get_parameters_by_path(**ssm_dict) + # Lookup by path + if bypath: + ssm_dict['Recursive'] = recursive + for term in terms: + ssm_dict["Path"] = term + display.vvv("AWS_ssm path lookup term: %s in region: %s" % (term, region)) + try: + response = client.get_parameters_by_path(**ssm_dict) + except ClientError as e: + raise AnsibleError("SSM lookup exception: {0}".format(to_native(e))) paramlist = list() paramlist.extend(response['Parameters']) - # Manual pagination, since boto doesnt support it yet for get_parameters_by_path + # Manual pagination, since boto doesn't support it yet for get_parameters_by_path while 'NextToken' in response: response = client.get_parameters_by_path(NextToken=response['NextToken'], **ssm_dict) paramlist.extend(response['Parameters']) # shorten parameter names. yes, this will return duplicate names with different values. - if 'shortnames' in lparams: + if shortnames: for x in paramlist: x['Name'] = x['Name'][x['Name'].rfind('/') + 1:] + display.vvvv("AWS_ssm path lookup returned: %s" % str(paramlist)) if len(paramlist): - return paramlist + ret.append(boto3_tag_list_to_ansible_dict(paramlist, + tag_name_key_name="Name", + tag_value_key_name="Value")) else: - return None - # Lookup by parameter name - else: + ret.append({}) + # Lookup by parameter name - always returns a list with one or no entry. + else: + display.vvv("AWS_ssm name lookup term: %s" % terms) + ssm_dict["Names"] = terms + try: response = client.get_parameters(**ssm_dict) - ret.update(response) - if ret['Parameters']: - return [ret['Parameters'][0]['Value']] - else: - return None + except ClientError as e: + raise AnsibleError("SSM lookup exception: {0}".format(to_native(e))) + if len(response['Parameters']) == len(terms): + ret = [p['Value'] for p in response['Parameters']] + else: + raise AnsibleError('Undefined AWS SSM parameter: %s ' % str(response['InvalidParameters'])) - except ClientError as e: - raise AnsibleError("SSM lookup exception: {0}".format(e)) + display.vvvv("AWS_ssm path lookup returning: %s " % str(ret)) + return ret diff --git a/test/integration/targets/aws_ssm_parameters/aliases b/test/integration/targets/aws_ssm_parameters/aliases new file mode 100644 index 0000000000..d6ae2f116b --- /dev/null +++ b/test/integration/targets/aws_ssm_parameters/aliases @@ -0,0 +1,2 @@ +cloud/aws +posix/ci/cloud/group4/aws diff --git a/test/integration/targets/aws_ssm_parameters/defaults/main.yml b/test/integration/targets/aws_ssm_parameters/defaults/main.yml new file mode 100644 index 0000000000..13f8ba31ae --- /dev/null +++ b/test/integration/targets/aws_ssm_parameters/defaults/main.yml @@ -0,0 +1,3 @@ +--- +# defaults file for aws_lambda test +ssm_key_prefix: '{{resource_prefix}}' diff --git a/test/integration/targets/aws_ssm_parameters/tasks/main.yml b/test/integration/targets/aws_ssm_parameters/tasks/main.yml new file mode 100644 index 0000000000..82fd3c2d7c --- /dev/null +++ b/test/integration/targets/aws_ssm_parameters/tasks/main.yml @@ -0,0 +1,136 @@ +--- +# +# Author: Michael De La Rue +# based on aws_lambda test cases +- block: + + # ============================================================ + - name: set up aws connection info + set_fact: + aws_connection_info: &aws_connection_info + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + security_token: "{{ security_token }}" + region: "{{ aws_region }}" + no_log: yes + # ============================================================ + - name: Create or update key/value pair in aws parameter store + aws_ssm_parameter_store: + name: "/{{ssm_key_prefix}}/Hello" + description: "This is your first key" + value: "World" + <<: *aws_connection_info + + - name: Check that parameter was stored correctly + assert: + that: + - "'{{lookup('aws_ssm', '/' ~ ssm_key_prefix ~ '/Hello', region=ec2_region, aws_access_key=ec2_access_key, aws_secret_key=ec2_secret_key, aws_security_token=security_token )}}' == 'World'" + + # ============================================================ + - name: Create or update key/value pair in aws parameter store + aws_ssm_parameter_store: + name: "/{{ssm_key_prefix}}/path/wonvar" + description: "This is your first key" + value: "won value" + <<: *aws_connection_info + + - name: Create or update key/value pair in aws parameter store + aws_ssm_parameter_store: + name: "/{{ssm_key_prefix}}/path/toovar" + description: "This is your first key" + value: "too value" + <<: *aws_connection_info + + - name: Create or update key/value pair in aws parameter store + aws_ssm_parameter_store: + name: "/{{ssm_key_prefix}}/path/tree/treevar" + description: "This is your first key" + value: "tree value" + <<: *aws_connection_info + + # ============================================================ + - name: Create or update key/value pair in aws parameter store + aws_ssm_parameter_store: + name: "/{{ssm_key_prefix}}/deeppath/wondir/samevar" + description: "This is your first key" + value: "won value" + <<: *aws_connection_info + + - name: Create or update key/value pair in aws parameter store + aws_ssm_parameter_store: + name: "/{{ssm_key_prefix}}/deeppath/toodir/samevar" + description: "This is your first key" + value: "too value" + <<: *aws_connection_info + + # ============================================================ + - name: debug the lookup + debug: + msg: "{{lookup('aws_ssm', '/' ~ ssm_key_prefix ~ '/path', region=ec2_region, aws_access_key=ec2_access_key, aws_secret_key=ec2_secret_key, aws_security_token=security_token, bypath=True )}}'" + + - name: Check that parameter path is stored and retrieved + assert: + that: + - "'{{lookup('aws_ssm', '/' ~ ssm_key_prefix ~ '/path', region=ec2_region, aws_access_key=ec2_access_key, aws_secret_key=ec2_secret_key, aws_security_token=security_token, bypath=True, shortnames=true ) | to_json }}' == '{\"toovar\": \"too value\", \"wonvar\": \"won value\"}'" + + # ============================================================ + - name: Error in case we don't find a named parameter + debug: + msg: "'{{lookup('aws_ssm', '/' ~ ssm_key_prefix ~ '/Goodbye', region=ec2_region, aws_access_key=ec2_access_key, aws_secret_key=ec2_secret_key, aws_security_token=security_token )}}' == 'World'" + register: result + ignore_errors: true + + - name: assert failure from failure to find parameter + assert: + that: + - 'result.failed' + - "'Undefined AWS SSM parameter' in result.msg" + + # ============================================================ + - name: Handle multiple paths with one that doesn't exist - default to full names. + assert: + that: + - "'{{lookup('aws_ssm', '/' ~ ssm_key_prefix ~ '/path', '/' ~ ssm_key_prefix ~ '/dont_create_this_path_you_will_break_the_ansible_tests', region=ec2_region, aws_access_key=ec2_access_key, aws_secret_key=ec2_secret_key, aws_security_token=security_token, bypath=True ) | to_json }}' in ( '[{\"/' ~ ssm_key_prefix ~ '/path/toovar\": \"too value\", \"/' ~ ssm_key_prefix ~ '/path/wonvar\": \"won value\"}, {}]', '[{\"/' ~ ssm_key_prefix ~ '/path/wonvar\": \"won value\", \"/' ~ ssm_key_prefix ~ '/path/toovar\": \"too value\"}, {}]' )" + + + # ============================================================ + # this may be a bit of a nasty test case; we should perhaps accept _either_ value that was stored + # in the two variables named 'samevar' + + - name: Handle multiple paths with one that doesn't exist - shortnames - including overlap. + assert: + that: + - "'{{lookup('aws_ssm', '/' ~ ssm_key_prefix ~ '/path', '/' ~ ssm_key_prefix ~ '/dont_create_this_path_you_will_break_the_ansible_tests', '/' ~ ssm_key_prefix ~ '/deeppath', region=ec2_region, aws_access_key=ec2_access_key, aws_secret_key=ec2_secret_key, aws_security_token=security_token, bypath=True, shortnames=true, recursive=true ) | to_json }}' == '[{\"toovar\": \"too value\", \"treevar\": \"tree value\", \"wonvar\": \"won value\"}, {}, {\"samevar\": \"won value\"}]'" + + + # ============================================================ + - name: Delete key/value pair in aws parameter store + aws_ssm_parameter_store: + name: "/{{ssm_key_prefix}}/Hello" + state: absent + <<: *aws_connection_info + + # ============================================================ + - name: Attempt delete key/value pair in aws parameter store again + aws_ssm_parameter_store: + name: "/{{ssm_key_prefix}}/Hello" + state: absent + <<: *aws_connection_info + register: result + + - name: assert that changed is False since parameter should be deleted + assert: + that: + - result.changed == False + always: + # ============================================================ + - name: Delete remaining key/value pairs in aws parameter store + aws_ssm_parameter_store: + name: "{{item}}" + state: absent + <<: *aws_connection_info + with_items: + - "/{{ssm_key_prefix}}/Hello" + - "/{{ssm_key_prefix}}/path/wonvar" + - "/{{ssm_key_prefix}}/path/toovar" + - "/{{ssm_key_prefix}}/path/tree/treevar" diff --git a/test/units/plugins/lookup/test_aws_ssm.py b/test/units/plugins/lookup/test_aws_ssm.py new file mode 100644 index 0000000000..90dbd1ac11 --- /dev/null +++ b/test/units/plugins/lookup/test_aws_ssm.py @@ -0,0 +1,137 @@ +# +# (c) 2017 Michael De La Rue +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) + +import pytest +from copy import copy + +from ansible.errors import AnsibleError + +import ansible.plugins.lookup.aws_ssm as aws_ssm + +try: + import boto3 + from botocore.exceptions import ClientError +except ImportError: + pytestmark = pytest.mark.skip("This test requires the boto3 and botocore Python libraries") + +simple_variable_success_response = { + 'Parameters': [ + { + 'Name': 'simple_variable', + 'Type': 'String', + 'Value': 'simplevalue', + 'Version': 1 + } + ], + 'InvalidParameters': [], + 'ResponseMetadata': { + 'RequestId': '12121212-3434-5656-7878-9a9a9a9a9a9a', + 'HTTPStatusCode': 200, + 'HTTPHeaders': { + 'x-amzn-requestid': '12121212-3434-5656-7878-9a9a9a9a9a9a', + 'content-type': 'application/x-amz-json-1.1', + 'content-length': '116', + 'date': 'Tue, 23 Jan 2018 11:04:27 GMT' + }, + 'RetryAttempts': 0 + } +} + +path_success_response = copy(simple_variable_success_response) +path_success_response['Parameters'] = [ + {'Name': '/testpath/too', 'Type': 'String', 'Value': 'simple_value_too', 'Version': 1}, + {'Name': '/testpath/won', 'Type': 'String', 'Value': 'simple_value_won', 'Version': 1} +] + +missing_variable_fail_response = copy(simple_variable_success_response) +missing_variable_fail_response['Parameters'] = [] +missing_variable_fail_response['InvalidParameters'] = ['missing_variable'] + + +dummy_credentials = {} +dummy_credentials['boto_profile'] = None +dummy_credentials['aws_secret_key'] = "notasecret" +dummy_credentials['aws_access_key'] = "notakey" +dummy_credentials['aws_security_token'] = None +dummy_credentials['region'] = 'eu-west-1' + + +def test_lookup_variable(mocker): + lookup = aws_ssm.LookupModule() + lookup._load_name = "aws_ssm" + + boto3_double = mocker.MagicMock() + boto3_double.Session.return_value.client.return_value.get_parameters.return_value = simple_variable_success_response + boto3_client_double = boto3_double.Session.return_value.client + + with mocker.patch.object(boto3, 'session', boto3_double): + retval = lookup.run(["simple_variable"], {}, **dummy_credentials) + assert(retval[0] == "simplevalue") + boto3_client_double.assert_called_with('ssm', 'eu-west-1', aws_access_key_id='notakey', + aws_secret_access_key="notasecret", aws_session_token=None) + + +def test_path_lookup_variable(mocker): + lookup = aws_ssm.LookupModule() + lookup._load_name = "aws_ssm" + + boto3_double = mocker.MagicMock() + get_path_fn = boto3_double.Session.return_value.client.return_value.get_parameters_by_path + get_path_fn.return_value = path_success_response + boto3_client_double = boto3_double.Session.return_value.client + + with mocker.patch.object(boto3, 'session', boto3_double): + args = copy(dummy_credentials) + args["bypath"] = 'true' + retval = lookup.run(["/testpath"], {}, **args) + assert(retval[0]["/testpath/won"] == "simple_value_won") + assert(retval[0]["/testpath/too"] == "simple_value_too") + boto3_client_double.assert_called_with('ssm', 'eu-west-1', aws_access_key_id='notakey', + aws_secret_access_key="notasecret", aws_session_token=None) + get_path_fn.assert_called_with(Path="/testpath", Recursive=False, WithDecryption=True) + + +def test_warn_missing_variable(mocker): + lookup = aws_ssm.LookupModule() + lookup._load_name = "aws_ssm" + + boto3_double = mocker.MagicMock() + boto3_double.Session.return_value.client.return_value.get_parameters.return_value = missing_variable_fail_response + + with pytest.raises(AnsibleError): + with mocker.patch.object(boto3, 'session', boto3_double): + lookup.run(["missing_variable"], {}, **dummy_credentials) + + +error_response = {'Error': {'Code': 'ResourceNotFoundException', 'Message': 'Fake Testing Error'}} +operation_name = 'FakeOperation' + + +def test_warn_denied_variable(mocker): + lookup = aws_ssm.LookupModule() + lookup._load_name = "aws_ssm" + + boto3_double = mocker.MagicMock() + boto3_double.Session.return_value.client.return_value.get_parameters.side_effect = ClientError(error_response, operation_name) + + with pytest.raises(AnsibleError): + with mocker.patch.object(boto3, 'session', boto3_double): + lookup.run(["denied_variable"], {}, **dummy_credentials)