New module etcd3 (#35053)
This module uses etcd3 python library to do k/v manipulation on an etcd3 cluster using version3 api.
This commit is contained in:
parent
3b76db5239
commit
5f1254697d
11 changed files with 414 additions and 0 deletions
184
lib/ansible/modules/clustering/etcd3.py
Normal file
184
lib/ansible/modules/clustering/etcd3.py
Normal file
|
@ -0,0 +1,184 @@
|
|||
#!/usr/bin/python
|
||||
#
|
||||
# (c) 2018, Jean-Philippe Evrard <jean-philippe@evrard.me>
|
||||
# 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()
|
4
test/integration/targets/etcd3/aliases
Normal file
4
test/integration/targets/etcd3/aliases
Normal file
|
@ -0,0 +1,4 @@
|
|||
destructive
|
||||
posix/ci/group1
|
||||
skip/osx
|
||||
skip/freebsd
|
13
test/integration/targets/etcd3/defaults/main.yml
Normal file
13
test/integration/targets/etcd3/defaults/main.yml
Normal file
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
# test code for the etcd3 module
|
||||
# (c) 2017, Jean-Philippe Evrard <jean-philippe@evrard.me>
|
||||
#
|
||||
# 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"
|
25
test/integration/targets/etcd3/tasks/main.yml
Normal file
25
test/integration/targets/etcd3/tasks/main.yml
Normal file
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
# test code for the etcd3 module
|
||||
# (c) 2017, Jean-Philippe Evrard <jean-philippe@evrard.me>
|
||||
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ============================================================
|
||||
|
||||
- name: run_tests for supported distros
|
||||
include_tasks: run_tests.yml
|
||||
when:
|
||||
- ansible_distribution | lower ~ "-" ~ ansible_distribution_major_version | lower != 'centos-6'
|
178
test/integration/targets/etcd3/tasks/run_tests.yml
Normal file
178
test/integration/targets/etcd3/tasks/run_tests.yml
Normal file
|
@ -0,0 +1,178 @@
|
|||
---
|
||||
# test code for the etcd3 module
|
||||
# (c) 2017, Jean-Philippe Evrard <jean-philippe@evrard.me>
|
||||
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ============================================================
|
||||
|
||||
- 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
|
1
test/integration/targets/etcd3/vars/RedHat-py3.yml
Normal file
1
test/integration/targets/etcd3/vars/RedHat-py3.yml
Normal file
|
@ -0,0 +1 @@
|
|||
etcd3_deps_packages: []
|
3
test/integration/targets/etcd3/vars/RedHat.yml
Normal file
3
test/integration/targets/etcd3/vars/RedHat.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
etcd3_deps_packages:
|
||||
- gcc-c++
|
||||
- python-devel
|
3
test/integration/targets/etcd3/vars/Suse.yml
Normal file
3
test/integration/targets/etcd3/vars/Suse.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
etcd3_deps_packages:
|
||||
- gcc-c++
|
||||
- python-devel
|
1
test/integration/targets/etcd3/vars/Ubuntu-16-py3.yml
Normal file
1
test/integration/targets/etcd3/vars/Ubuntu-16-py3.yml
Normal file
|
@ -0,0 +1 @@
|
|||
etcd3_deps_packages: []
|
1
test/integration/targets/etcd3/vars/Ubuntu-16.yml
Normal file
1
test/integration/targets/etcd3/vars/Ubuntu-16.yml
Normal file
|
@ -0,0 +1 @@
|
|||
etcd3_deps_packages: []
|
1
test/integration/targets/etcd3/vars/default.yml
Normal file
1
test/integration/targets/etcd3/vars/default.yml
Normal file
|
@ -0,0 +1 @@
|
|||
etcd3_deps_packages: []
|
Loading…
Reference in a new issue