diff --git a/lib/ansible/modules/clustering/etcd3.py b/lib/ansible/modules/clustering/etcd3.py new file mode 100644 index 00000000000..fdb15549947 --- /dev/null +++ b/lib/ansible/modules/clustering/etcd3.py @@ -0,0 +1,184 @@ +#!/usr/bin/python +# +# (c) 2018, Jean-Philippe Evrard +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = """ +--- +module: etcd3 +short_description: "Set or delete key value pairs from an etcd3 cluster" +version_added: "2.5" +requirements: + - etcd3 +description: + - Sets or deletes values in etcd3 cluster using its v3 api. + - Needs python etcd3 lib to work +options: + key: + description: + - the key where the information is stored in the cluster + required: true + value: + description: + - the information stored + required: true + host: + description: + - the IP address of the cluster + default: 'localhost' + port: + description: + - the port number used to connect to the cluster + default: 2379 + state: + description: + - the state of the value for the key. + - can be present or absent + required: true +author: + - Jean-Philippe Evrard (@evrardjp) +""" + +EXAMPLES = """ +# Store a value "bar" under the key "foo" for a cluster located "http://localhost:2379" +- etcd3: + key: "foo" + value: "baz3" + host: "localhost" + port: 2379 + state: "present" +""" + +RETURN = ''' +key: + description: The key that was queried + returned: always + type: str +old_value: + description: The previous value in the cluster + returned: always + type: str +''' + +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native +try: + import etcd3 + etcd_found = True +except ImportError: + etcd_found = False + + +def run_module(): + # define the available arguments/parameters that a user can pass to + # the module + module_args = dict( + key=dict(type='str', required=True), + value=dict(type='str', required=True), + host=dict(type='str', default='localhost'), + port=dict(type='int', default=2379), + state=dict(type='str', required=True, choices=['present', 'absent']), + ) + + # seed the result dict in the object + # we primarily care about changed and state + # change is if this module effectively modified the target + # state will include any data that you want your module to pass back + # for consumption, for example, in a subsequent task + result = dict( + changed=False, + ) + + # the AnsibleModule object will be our abstraction working with Ansible + # this includes instantiation, a couple of common attr would be the + # args/params passed to the execution, as well as if the module + # supports check mode + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True + ) + + result['key'] = module.params.get('key') + + if not etcd_found: + module.fail_json(msg="the python etcd3 module is required") + + allowed_keys = ['host', 'port', 'ca_cert', 'cert_key', 'cert_cert', + 'timeout', 'user', 'password'] + # TODO(evrardjp): Move this back to a dict comprehension when python 2.7 is + # the minimum supported version + # client_params = {key: value for key, value in module.params.items() if key in allowed_keys} + client_params = dict() + for key, value in module.params.items(): + if key in allowed_keys: + client_params[key] = value + try: + etcd = etcd3.client(**client_params) + except Exception as exp: + module.fail_json(msg='Cannot connect to etcd cluster: %s' % (to_native(exp)), + exception=traceback.format_exc()) + try: + cluster_value = etcd.get(module.params['key']) + except Exception as exp: + module.fail_json(msg='Cannot reach data: %s' % (to_native(exp)), + exception=traceback.format_exc()) + + # Make the cluster_value[0] a string for string comparisons + result['old_value'] = to_native(cluster_value[0]) + + if module.params['state'] == 'absent': + if cluster_value[0] is not None: + if module.check_mode: + result['changed'] = True + else: + try: + etcd.delete(module.params['key']) + except Exception as exp: + module.fail_json(msg='Cannot delete %s: %s' % (module.params['key'], to_native(exp)), + exception=traceback.format_exc()) + else: + result['changed'] = True + elif module.params['state'] == 'present': + if result['old_value'] != module.params['value']: + if module.check_mode: + result['changed'] = True + else: + try: + etcd.put(module.params['key'], module.params['value']) + except Exception as exp: + module.fail_json(msg='Cannot add or edit key %s: %s' % (module.params['key'], to_native(exp)), + exception=traceback.format_exc()) + else: + result['changed'] = True + else: + module.fail_json(msg="State not recognized") + + # manipulate or modify the state as needed (this is going to be the + # part where your module will do what it needs to do) + + # during the execution of the module, if there is an exception or a + # conditional state that effectively causes a failure, run + # AnsibleModule.fail_json() to pass in the message and the result + + # in the event of a successful module execution, you will want to + # simple AnsibleModule.exit_json(), passing the key/value results + module.exit_json(**result) + + +def main(): + run_module() + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/etcd3/aliases b/test/integration/targets/etcd3/aliases new file mode 100644 index 00000000000..b31f76595c4 --- /dev/null +++ b/test/integration/targets/etcd3/aliases @@ -0,0 +1,4 @@ +destructive +posix/ci/group1 +skip/osx +skip/freebsd diff --git a/test/integration/targets/etcd3/defaults/main.yml b/test/integration/targets/etcd3/defaults/main.yml new file mode 100644 index 00000000000..bf8934ed75b --- /dev/null +++ b/test/integration/targets/etcd3/defaults/main.yml @@ -0,0 +1,13 @@ +--- +# test code for the etcd3 module +# (c) 2017, Jean-Philippe Evrard +# +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# # Copyright: (c) 2018, Ansible Project +# +etcd3_ver: "v3.2.14" +etcd3_download_server: "https://storage.googleapis.com/etcd" +#etcd3_download_server: "https://github.com/coreos/etcd/releases/download" +etcd3_download_url: "{{ etcd3_download_server }}/{{ etcd3_ver }}/etcd-{{ etcd3_ver }}-linux-amd64.tar.gz" +etcd3_download_location: /tmp/etcd-download-test +etcd3_path: "{{ etcd3_download_location }}/etcd-{{ etcd3_ver }}-linux-amd64" diff --git a/test/integration/targets/etcd3/tasks/main.yml b/test/integration/targets/etcd3/tasks/main.yml new file mode 100644 index 00000000000..99c5013f764 --- /dev/null +++ b/test/integration/targets/etcd3/tasks/main.yml @@ -0,0 +1,25 @@ +--- +# test code for the etcd3 module +# (c) 2017, Jean-Philippe Evrard + +# 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 . + +# ============================================================ + +- name: run_tests for supported distros + include_tasks: run_tests.yml + when: + - ansible_distribution | lower ~ "-" ~ ansible_distribution_major_version | lower != 'centos-6' diff --git a/test/integration/targets/etcd3/tasks/run_tests.yml b/test/integration/targets/etcd3/tasks/run_tests.yml new file mode 100644 index 00000000000..2095d2d4b89 --- /dev/null +++ b/test/integration/targets/etcd3/tasks/run_tests.yml @@ -0,0 +1,178 @@ +--- +# test code for the etcd3 module +# (c) 2017, Jean-Philippe Evrard + +# 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 . + +# ============================================================ + +- name: python 2 + set_fact: + python_suffix: "" + when: ansible_python_version is version('3', '<') + +- name: python 3 + set_fact: + python_suffix: "-py3" + when: ansible_python_version is version('3', '>=') + +- include_vars: '{{ item }}' + with_first_found: + - files: + - '{{ ansible_distribution }}-{{ ansible_distribution_major_version }}{{ python_suffix }}.yml' + - '{{ ansible_distribution }}-{{ ansible_distribution_version }}{{ python_suffix }}.yml' + - '{{ ansible_os_family }}{{ python_suffix }}.yml' + - 'default{{ python_suffix }}.yml' + paths: '../vars' + +# Install requirements for etcd3 module +- name: Install etcd3 packages + package: + name: "{{ etcd3_deps_packages }}" + state: present + +- name: Install etcd3 module + pip: + name: etcd3 + state: present + +# Check if re-installing etcd3 is required +- name: Check if etcd3ctl exists for re-use. + shell: "ETCDCTL_API=3 {{ etcd3_path }}/etcdctl --endpoints=localhost:2379 get foo" + args: + executable: /bin/bash + changed_when: false + failed_when: false + register: _testetcd3ctl + +# Installing etcd3 +- name: If can't reuse, prepare download folder + file: + path: "{{ etcd3_download_location }}" + state: directory + register: _etcddownloadexists + when: + - _testetcd3ctl.rc != 0 + +- name: Delete download folder if already exists (to start clean) + file: + path: "{{ etcd3_download_location }}" + state: absent + when: + - _testetcd3ctl.rc != 0 + - _etcddownloadexists is not changed + +- name: Recreate download folder if purged + file: + path: "{{ etcd3_download_location }}" + state: directory + when: + - _testetcd3ctl.rc != 0 + - _etcddownloadexists is not changed + +- name: Download etcd3 + unarchive: + src: "{{ etcd3_download_url }}" + dest: "{{ etcd3_download_location }}" + remote_src: yes + when: + - _testetcd3ctl.rc != 0 + +# Running etcd3 and kill afterwards if it wasn't running before. +- name: Run etcd3 + shell: "{{ etcd3_path }}/etcd &" + register: _etcd3run + changed_when: true + when: + - _testetcd3ctl.rc != 0 + +# Integration tests +- name: Check mode, show need change + etcd3: + key: "foo" + value: "bar" + state: "present" + register: _etcd3_prst_chktst + check_mode: true + +- name: Change to new value + etcd3: + key: "foo" + value: "bar" + state: "present" + register: _etcd3_prst_chgtst + +- name: Idempotency test, show unchanged. + etcd3: + key: "foo" + value: "bar" + state: "present" + register: _etcd3_prst_idmptnttst + +- name: Idempotency test in check mode, show unchanged + etcd3: + key: "foo" + value: "bar" + state: "present" + register: _etcd3_prst_idmptntchktst + check_mode: true + +- name: Check mode, show need removal of key + etcd3: + key: "foo" + value: "baz" + state: "absent" + register: _etcd3_absnt_chktst + check_mode: true + +- name: Remove foo key + etcd3: + key: "foo" + value: "baz" + state: "absent" + register: _etcd3_absnt_chgtst + +- name: Idempotency test in check mode, show unchanged + etcd3: + key: "foo" + value: "baz" + state: "absent" + register: _etcd3_absnt_idmptnttst + check_mode: true + +- name: Idempotency test, show unchanged + etcd3: + key: "foo" + value: "baz" + state: "absent" + register: _etcd3_absnt_idmptntchktst + +- name: Checking the status are expected + assert: + that: + - _etcd3_prst_chktst is changed + - _etcd3_prst_chgtst is changed + - _etcd3_prst_idmptnttst is not changed + - _etcd3_prst_idmptntchktst is not changed + - _etcd3_absnt_chktst is changed + - _etcd3_absnt_chgtst is changed + - _etcd3_absnt_idmptnttst is not changed + - _etcd3_absnt_idmptntchktst is not changed + +- name: kill etcd3 + command: "pkill etcd" + when: + - _testetcd3ctl.rc != 0 diff --git a/test/integration/targets/etcd3/vars/RedHat-py3.yml b/test/integration/targets/etcd3/vars/RedHat-py3.yml new file mode 100644 index 00000000000..6337a873074 --- /dev/null +++ b/test/integration/targets/etcd3/vars/RedHat-py3.yml @@ -0,0 +1 @@ +etcd3_deps_packages: [] diff --git a/test/integration/targets/etcd3/vars/RedHat.yml b/test/integration/targets/etcd3/vars/RedHat.yml new file mode 100644 index 00000000000..8eda2060150 --- /dev/null +++ b/test/integration/targets/etcd3/vars/RedHat.yml @@ -0,0 +1,3 @@ +etcd3_deps_packages: + - gcc-c++ + - python-devel diff --git a/test/integration/targets/etcd3/vars/Suse.yml b/test/integration/targets/etcd3/vars/Suse.yml new file mode 100644 index 00000000000..8eda2060150 --- /dev/null +++ b/test/integration/targets/etcd3/vars/Suse.yml @@ -0,0 +1,3 @@ +etcd3_deps_packages: + - gcc-c++ + - python-devel diff --git a/test/integration/targets/etcd3/vars/Ubuntu-16-py3.yml b/test/integration/targets/etcd3/vars/Ubuntu-16-py3.yml new file mode 100644 index 00000000000..6337a873074 --- /dev/null +++ b/test/integration/targets/etcd3/vars/Ubuntu-16-py3.yml @@ -0,0 +1 @@ +etcd3_deps_packages: [] diff --git a/test/integration/targets/etcd3/vars/Ubuntu-16.yml b/test/integration/targets/etcd3/vars/Ubuntu-16.yml new file mode 100644 index 00000000000..6337a873074 --- /dev/null +++ b/test/integration/targets/etcd3/vars/Ubuntu-16.yml @@ -0,0 +1 @@ +etcd3_deps_packages: [] diff --git a/test/integration/targets/etcd3/vars/default.yml b/test/integration/targets/etcd3/vars/default.yml new file mode 100644 index 00000000000..6337a873074 --- /dev/null +++ b/test/integration/targets/etcd3/vars/default.yml @@ -0,0 +1 @@ +etcd3_deps_packages: []