From 91a36faddba3e537b21639e9e8694c2e983095b2 Mon Sep 17 00:00:00 2001 From: Bill Wang Date: Tue, 10 Oct 2017 10:04:40 +1100 Subject: [PATCH] New module: Add module for Amazon Systems Manager Parameter Store (cloud/amazon/ssm_parameter_store) (#23460) - new module: ssm_parameter_store - new lookup: ssm * lookup module ssm - adjust error message * Pacify pylint erroring on botocore not found * adjust to version 2.5 --- .../cloud/amazon/ssm_parameter_store.py | 220 ++++++++++++++++++ lib/ansible/plugins/lookup/ssm.py | 98 ++++++++ 2 files changed, 318 insertions(+) create mode 100644 lib/ansible/modules/cloud/amazon/ssm_parameter_store.py create mode 100644 lib/ansible/plugins/lookup/ssm.py diff --git a/lib/ansible/modules/cloud/amazon/ssm_parameter_store.py b/lib/ansible/modules/cloud/amazon/ssm_parameter_store.py new file mode 100644 index 0000000000..2419731866 --- /dev/null +++ b/lib/ansible/modules/cloud/amazon/ssm_parameter_store.py @@ -0,0 +1,220 @@ +#!/usr/bin/python +# 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 . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'metadata_version': '1.1'} + +DOCUMENTATION = ''' +--- +module: ssm_parameter_store +short_description: Manage key-value pairs in aws parameter store. +description: + - Manage key-value pairs in aws parameter store. +version_added: "2.5" +options: + name: + description: + - parameter key name. + required: true + description: + description: + - parameter key desciption. + required: false + value: + description: + - Parameter value. + required: false + state: + description: + - Creates or modifies an existing parameter + - Deletes a parameter + required: false + choices: ['present', 'absent'] + default: present + string_type: + description: + - Parameter String type + required: false + choices: ['String', 'StringList', 'SecureString'] + default: String + decryption: + description: + - Work with SecureString type to get plain text secrets + - Boolean + required: false + default: True + key_id: + description: + - aws KMS key to decrypt the secrets. + required: false + default: aws/ssm (this key is automatically generated at the first parameter created). + overwrite: + description: + - Overwrite the value when create or update parameter + - Boolean + required: false + default: True + region: + description: + - region. + required: false +author: Bill Wang (ozbillwang@gmail.com) +extends_documentation_fragment: aws +requirements: [ botocore, boto3 ] +''' + +EXAMPLES = ''' +- name: Create or update key/value pair in aws parameter store + ssm_parameter_store: + name: "Hello" + description: "This is your first key" + value: "World" + +- name: Delete the key + ssm_parameter_store: + name: "Hello" + state: absent + +- name: Create or update secure key/value pair with default kms key (aws/ssm) + ssm_parameter_store: + name: "Hello" + description: "This is your first key" + string_type: "SecureString" + value: "World" + +- name: Create or update secure key/value pair with nominated kms key + ssm_parameter_store: + name: "Hello" + description: "This is your first key" + string_type: "SecureString" + key_id: "alias/demo" + value: "World" + +- name: recommend to use with ssm lookup plugin + debug: msg="{{ lookup('ssm', 'hello') }}" +''' + +RETURN = ''' +put_parameter: + description: Add one or more paramaters to the system. + returned: success + type: dictionary +delete_parameter: + description: Delete a parameter from the system. + returned: success + 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 + +try: + from botocore.exceptions import ClientError, NoCredentialsError +except ImportError: + pass # will be captured by imported HAS_BOTO3 + + +def create_update_parameter(client, module): + changed = False + response = {} + + args = dict( + Name=module.params.get('name'), + Value=module.params.get('value'), + Type=module.params.get('string_type'), + Overwrite=module.params.get('overwrite') + ) + + if module.params.get('description'): + args.update(Description=module.params.get('description')) + + if module.params.get('string_type') == 'SecureString': + args.update(KeyId=module.params.get('key_id')) + + try: + 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)) + + return changed, response + + +def delete_parameter(client, module): + changed = False + response = {} + + try: + get_response = client.get_parameters( + Names=[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 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 changed, response + + +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.') + 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)) + + invocations = { + "present": create_update_parameter, + "absent": delete_parameter, + } + (changed, response) = invocations[state](client, module) + module.exit_json(changed=changed, response=response) + +if __name__ == '__main__': + main() diff --git a/lib/ansible/plugins/lookup/ssm.py b/lib/ansible/plugins/lookup/ssm.py new file mode 100644 index 0000000000..5fa4ebd3c9 --- /dev/null +++ b/lib/ansible/plugins/lookup/ssm.py @@ -0,0 +1,98 @@ +# (c) 2016, Bill Wang +# +# 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 . + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.module_utils.ec2 import HAS_BOTO3 +from ansible.errors import AnsibleError +from ansible.plugins.lookup import LookupBase + +try: + from botocore.exceptions import ClientError + import boto3 +except ImportError: + pass # will be captured by imported HAS_BOTO3 + + +class LookupModule(LookupBase): + def run(self, terms, variables, **kwargs): + ''' + # lookup sample: + - name: lookup ssm parameter store in the current region + debug: msg="{{ lookup('ssm', 'Hello' ) }}" + + - name: lookup a key which doesn't exist, return "" + debug: msg="{{ lookup('ssm', 'NoKey') }}" + + - name: lookup ssm parameter store in nominated region + debug: msg="{{ lookup('ssm', 'Hello', 'region=us-east-2' ) }}" + + - name: lookup ssm parameter store without decrypted + debug: msg="{{ lookup('ssm', 'Hello', 'decrypt=False' ) }}" + + - name: lookup ssm parameter store in nominated aws profile + debug: msg="{{ lookup('ssm', 'Hello', 'aws_profile=myprofile' ) }}" + + - name: lookup ssm parameter store with all options. + debug: msg="{{ lookup('ssm', 'Hello', 'decrypt=false', 'region=us-east-2', 'aws_profile=myprofile') }}" + ''' + + ret = {} + response = {} + session = {} + ssm_dict = {} + + if not HAS_BOTO3: + raise AnsibleError('botocore and boto3 are required.') + + ssm_dict['WithDecryption'] = True + ssm_dict['Names'] = [terms[0]] + + 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) + + if key == "region" or key == "aws_profile": + session[key] = value + + # decrypt the value or not + if key == "decrypt" and value.lower() == "false": + 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: + response = client.get_parameters(**ssm_dict) + except ClientError as e: + raise AnsibleError("SSM lookup exception: {0}".format(e)) + + ret.update(response) + + if ret['Parameters']: + return [ret['Parameters'][0]['Value']] + else: + return None