diff --git a/.github/workflows/test_elasticsearch_modules.yml b/.github/workflows/test_elasticsearch_modules.yml new file mode 100644 index 00000000..2fd273ee --- /dev/null +++ b/.github/workflows/test_elasticsearch_modules.yml @@ -0,0 +1,63 @@ +--- +name: Test Elasticsearch modules +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + type: choice + options: + - info + - warning + - debug + pull_request: + paths: + - '.github/workflows/test_elasticsearch_modules.yml' + - 'molecule/elasticsearch_test_modules/*' + +jobs: + molecule_elasticsearch_modules: + runs-on: ubuntu-latest + + env: + COLLECTION_NAMESPACE: netways + COLLECTION_NAME: elasticstack + + strategy: + fail-fast: false + matrix: + distro: [ubuntu2204] + scenario: + - elasticsearch_test_modules + release: + - 8 + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Python 3.8 + uses: actions/setup-python@v5 + with: + python-version: 3.8 + + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + python3 -m pip install -r requirements-test.txt + + - name: Install collection + run: | + mkdir -p ~/.ansible/collections/ansible_collections/$COLLECTION_NAMESPACE + cp -a ../ansible-collection-$COLLECTION_NAME ~/.ansible/collections/ansible_collections/$COLLECTION_NAMESPACE/$COLLECTION_NAME + + - name: Test with molecule + run: | + molecule test -s ${{ matrix.scenario }} + env: + MOLECULE_DISTRO: ${{ matrix.distro }} + PY_COLORS: '1' + ANSIBLE_FORCE_COLOR: '1' + ELASTIC_RELEASE: ${{ matrix.release }} diff --git a/.gitignore b/.gitignore index 18c9f355..5fa051f4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .cache *.swp -__pycache__* \ No newline at end of file +__pycache__* +.vscode diff --git a/README.md b/README.md index b401cb14..14a36df5 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Every role is documented with all variables, please refer to the documentation f **Please note**: If you are already using this collection before version `1.0.0`, please note that we had to rename a significant amount of variables due to naming schema changes made by Ansible. Please review the variables you have set in your playbooks and variable files. -## Roles Documentation +## Roles documentation * [Beats](docs/role-beats.md) * [Elasticsearch](docs/role-elasticsearch.md) @@ -16,6 +16,11 @@ Every role is documented with all variables, please refer to the documentation f * [Logstash](docs/role-logstash.md) * [Repos](docs/role-repos.md) +## Modules documentation + +* [elasticsearch_role](docs/module-elasticsearch_role.md) +* [elasticsearch_user](docs/module-elasticsearch_user.md) + ## Installation You can easily install the collection with the `ansible-galaxy` command. diff --git a/docs/module-elasticsearch_role.md b/docs/module-elasticsearch_role.md new file mode 100644 index 00000000..1127a344 --- /dev/null +++ b/docs/module-elasticsearch_role.md @@ -0,0 +1,68 @@ +Ansible module: elasticsearch_role +=== + +This module creates, updates and deletes roles from your Elasticsearch. + +Requirements +--- + +As this module uses the Elasticsearch API you will need to install the `elasticsearch` Python3 library. +``` +pip3 install elasticsearch +``` + +Module arguments +--- + +* *name*: Name of your role (**Required**) +* *cluster*: List of clusters +* *indicies*: List of indicies + * *names*: List of names (**Required**) + * *privileges*: List of privileges (**Required**) +* *state*: State of the role (Default: `present`) +* *host*: API endpoint (**Required**) +* *auth_user*: User to authenticate on the Elasticsearch API (**Required**) +* *auth_pass*: Password for the given user (**Required**) +* *verify_certs*: Verify certificates (Default: `true`) +* *ca_certs*: Verify HTTPS connection by using ca certificate. Path to ca needs to be given + +Example usage +--- +``` + - name: Create elasticsearch role 'new-role1' + netways.elasticstack.elasticsearch_role: + name: new-role1 + cluster: + - manage_own_api_key + - delegate_pki + indicies: + - names: + - default01 + privileges: + - read + - write + state: present + host: https://localhost:9200 + auth_user: elastic + auth_pass: changeMe123! + verify_certs: true + ca_certs: /etc/elasticsearch/certs/http_ca.crt + + - name: Create elasticsearch role 'new-role2' + netways.elasticstack.elasticsearch_role: + name: new-role2 + cluster: + - manage_own_api_key + - delegate_pki + indicies: + - names: + - default01 + privileges: + - read + - write + state: present + host: https://localhost:9200 + auth_user: elastic + auth_pass: changeMe123! + verify_certs: false +``` \ No newline at end of file diff --git a/docs/module-elasticsearch_user.md b/docs/module-elasticsearch_user.md new file mode 100644 index 00000000..9ab1ea07 --- /dev/null +++ b/docs/module-elasticsearch_user.md @@ -0,0 +1,65 @@ +Ansible module: elasticsearch_user +=== + +This module creates, updates and deletes users from your Elasticsearch. + +Requirements +--- + +As this module uses the Elasticsearch API you will need to install the `elasticsearch` Python3 library. +``` +pip3 install elasticsearch +``` + +Module arguments +--- + +* *name*: Name of your user (**Required**) +* *fullname*: Fullname of your user +* *password*: Password for your user (**Required**) +* *email*: Email for your user +* *roles*: List of roles (**Required**) +* *enabled*: Define wheter this user should be enabled (Default: `true`) +* *state*: State of the role. `absent` to delete the user (Default: `present`) +* *host*: API endpoint (**Required**) +* *auth_user*: User to authenticate on the Elasticsearch API (**Required**) +* *auth_pass*: Password for the given user (**Required**) +* *verify_certs*: Verify certificates (Default: `true`) +* *ca_certs*: Verify HTTPS connection by using ca certificate. Path to ca needs to be given + +Example usage +--- +``` + - name: Create elasticsearch user 'new-user1' + netways.elasticstack.elasticsearch_user: + name: new-user1 + fullname: New User 1 + password: changeMe321! + email: new1@user.de + roles: + - new-role + - logstash-writer + enabled: true + state: present + host: https://localhost:9200 + auth_user: elastic + auth_pass: changeMe123! + verify_certs: true + ca_certs: /etc/elasticsearch/certs/http_ca.crt + + - name: Create elasticsearch user 'new-user2' + netways.elasticstack.elasticsearch_user: + name: new-user2 + fullname: New User 2 + password: changeMe321! + email: new2@user.de + roles: + - new-role + - logstash-writer + enabled: true + state: present + host: https://localhost:9200 + auth_user: elastic + auth_pass: changeMe123! + verify_certs: false +``` \ No newline at end of file diff --git a/molecule/elasticsearch_test_modules/converge.yml b/molecule/elasticsearch_test_modules/converge.yml new file mode 100644 index 00000000..aeef2985 --- /dev/null +++ b/molecule/elasticsearch_test_modules/converge.yml @@ -0,0 +1,65 @@ +--- +# The workaround for arbitrarily named role directory is important because the git repo has one name and the role within it another +# Found at: https://github.com/ansible-community/molecule/issues/1567#issuecomment-436876722 +- name: Converge + collections: + - netways.elasticstack + hosts: all + vars: + elasticstack_full_stack: false + elasticsearch_jna_workaround: true + elasticsearch_disable_systemcallfilterchecks: true + #elasticstack_release: "{{ lookup('env', 'ELASTIC_RELEASE') | int}}" + elasticstack_release: 8 + elasticsearch_heap: "1" + elasticstack_no_log: false + tasks: + - name: Include Elastics repos role + ansible.builtin.include_role: + name: repos + - name: Include Elasticsearch + ansible.builtin.include_role: + name: elasticsearch + + - name: Fetch Elastic password # noqa: risky-shell-pipe + ansible.builtin.shell: > + if test -n "$(ps -p $$ | grep bash)"; then set -o pipefail; fi; + grep "PASSWORD elastic" /usr/share/elasticsearch/initial_passwords | + awk {' print $4 '} + register: elasticstack_password + changed_when: false + + - name: Create elasticsearch role 'new-role' + netways.elasticstack.elasticsearch_role: + name: new-role1 + cluster: + - manage_own_api_key + - delegate_pki + indicies: + - names: + - foobar321 + privileges: + - read + - write + state: present + host: https://localhost:9200 + auth_user: elastic + auth_pass: "{{ elasticstack_password.stdout }}" + verify_certs: false + + - name: Create elasticsearch user 'new-user' + netways.elasticstack.elasticsearch_user: + name: new-user1 + fullname: New User + password: changeMe123! + email: new@user.de + roles: + - new-role1 + - logstash-writer + enabled: true + state: present + host: https://localhost:9200 + auth_user: elastic + auth_pass: "{{ elasticstack_password.stdout }}" + verify_certs: false + ca_certs: /etc/elasticsearch/certs/http_ca.crt diff --git a/molecule/elasticsearch_test_modules/molecule.yml b/molecule/elasticsearch_test_modules/molecule.yml new file mode 100644 index 00000000..ee0c0a47 --- /dev/null +++ b/molecule/elasticsearch_test_modules/molecule.yml @@ -0,0 +1,24 @@ +--- +dependency: + name: galaxy + options: + requirements-file: requirements.yml +driver: + name: docker +platforms: + - name: elasticsearch_default + groups: + - elasticsearch + image: "geerlingguy/docker-${MOLECULE_DISTRO:-debian11}-ansible:latest" + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + pre_build_image: true +provisioner: + name: ansible + env: + ANSIBLE_VERBOSITY: 3 +verifier: + name: ansible diff --git a/molecule/elasticsearch_test_modules/prepare.yml b/molecule/elasticsearch_test_modules/prepare.yml new file mode 100644 index 00000000..e55e3df0 --- /dev/null +++ b/molecule/elasticsearch_test_modules/prepare.yml @@ -0,0 +1,22 @@ +--- +- name: Prepare + hosts: all + tasks: + - name: Install packages for Debian + ansible.builtin.apt: + name: + - gpg + - gpg-agent + - procps + - curl + - iproute2 + - git + - openssl + - python3 + update_cache: yes + + - name: Install python module dependencies + ansible.builtin.pip: + name: "{{ item }}" + loop: + - elasticsearch diff --git a/molecule/elasticsearch_test_modules/requirements.yml b/molecule/elasticsearch_test_modules/requirements.yml new file mode 100644 index 00000000..8dd51618 --- /dev/null +++ b/molecule/elasticsearch_test_modules/requirements.yml @@ -0,0 +1,3 @@ +--- +collections: + - community.general diff --git a/plugins/module_utils/api.py b/plugins/module_utils/api.py new file mode 100644 index 00000000..6dada178 --- /dev/null +++ b/plugins/module_utils/api.py @@ -0,0 +1,15 @@ +# !/usr/bin/python3 + +# Copyright (c) 2024, Tobias Bauriedel +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or +# https://www.gnu.org/licenses/gpl-3.0.txt) + +from elasticsearch import Elasticsearch +import ssl + +class Api(): + def new_client_basic_auth(host, auth_user, auth_pass, ca_certs, verify_certs) -> Elasticsearch: + ctx = ssl.create_default_context(cafile=ca_certs) + ctx.check_hostname = False + ctx.verify_mode = False + return Elasticsearch(hosts=[host], basic_auth=(auth_user, auth_pass), ssl_context=ctx, verify_certs=verify_certs) diff --git a/plugins/module_utils/elasticsearch_role.py b/plugins/module_utils/elasticsearch_role.py new file mode 100644 index 00000000..77504af1 --- /dev/null +++ b/plugins/module_utils/elasticsearch_role.py @@ -0,0 +1,86 @@ +#!/usr/bin/python + +# Copyright (c) 2024, Tobias Bauriedel +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or +# https://www.gnu.org/licenses/gpl-3.0.txt) + +from ansible_collections.netways.elasticstack.plugins.module_utils.api import ( + Api +) + +class Role(): + def __init__(self, result, role_name, cluster, indicies, state, host, auth_user, auth_pass, verify_certs, ca_certs): + self.role_name = role_name + self.cluster = cluster + self.indicies = indicies + self.state = state + self.result = result + + self.client = Api.new_client_basic_auth(host=host, auth_user=auth_user, auth_pass=auth_pass, verify_certs=verify_certs, ca_certs=ca_certs) + + self.handle() + + + def return_result(self) -> dict: + return self.result + + + def handle(self): + + if self.state == 'absent': + self.handle_absent() + elif self.state == 'present': + self.handle_present() + + return + + + def handle_absent(self): + if self.role_name not in self.get_all().raw: + return + + res = self.delete() + if res['found'] == True: + self.result['changed'] = True + self.result['msg'] = self.role_name + " has been deleted" + + return + + + def handle_present(self): + if self.role_name in self.get_all().raw: + pre_role = self.get() + else: + pre_role = None + + res = self.put() + + if res.raw['role']['created'] == True: + self.result['changed'] = True + self.result['msg'] = self.role_name + " has been created" + return + + if pre_role == None: + return + + if pre_role.raw != self.get().raw: + self.result['changed'] = True + self.result['msg'] = self.role_name + " has been updated" + + return + + + def get_all(self): + return self.client.security.get_role() + + + def get(self): + return self.client.security.get_role(name=self.role_name) + + + def put(self): + return self.client.security.put_role(name=self.role_name, cluster=self.cluster, indices=self.indicies) + + + def delete(self): + return self.client.security.delete_role(name=self.role_name) diff --git a/plugins/module_utils/elasticsearch_user.py b/plugins/module_utils/elasticsearch_user.py new file mode 100644 index 00000000..c84da609 --- /dev/null +++ b/plugins/module_utils/elasticsearch_user.py @@ -0,0 +1,88 @@ +#!/usr/bin/python + +# Copyright (c) 2024, Tobias Bauriedel +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or +# https://www.gnu.org/licenses/gpl-3.0.txt) + +from ansible_collections.netways.elasticstack.plugins.module_utils.api import ( + Api +) + +class User(): + def __init__(self, result, user_name, full_name, password, email, roles, enabled, state, host, auth_user, auth_pass, verify_certs, ca_certs): + self.user_name = user_name + self.full_name = full_name + self.password = password + self.email = email + self.roles = roles + self.enabled = enabled + self.state = state + self.result = result + + self.client = Api.new_client_basic_auth(host=host, auth_user=auth_user, auth_pass=auth_pass, ca_certs=ca_certs, verify_certs=verify_certs) + + self.handle() + + + def return_result(self) -> dict: + return self.result + + + def handle(self): + if self.state == 'absent': + self.handle_absent() + elif self.state == 'present': + self.handle_present() + + return + + + def handle_absent(self): + if self.user_name not in self.get_all().raw: + return + + res = self.delete() + if res['found'] == True: + self.result['changed'] = True + self.result['msg'] = self.user_name + " has been deleted" + + return + + + def handle_present(self): + if self.user_name in self.get_all().raw: + pre_user = self.get() + else: + pre_user = None + + res = self.put() + + if res.raw['created'] == True: + self.result['changed'] = True + self.result['msg'] = self.user_name + " has been created" + return + + if pre_user == None: + return + + if pre_user.raw != self.get().raw: + self.result['changed'] = True + self.result['msg'] = self.user_name + " has beed updated" + + return + + + def get_all(self): + return self.client.security.get_user() + + + def get(self): + return self.client.security.get_user(username=self.user_name) + + + def put(self): + return self.client.security.put_user(username=self.user_name, password=self.password, email=self.email, full_name=self.full_name, enabled=self.enabled, roles=self.roles) + + + def delete(self): + return self.client.security.delete_user(username=self.user_name) \ No newline at end of file diff --git a/plugins/modules/elasticsearch_role.py b/plugins/modules/elasticsearch_role.py new file mode 100644 index 00000000..050a907a --- /dev/null +++ b/plugins/modules/elasticsearch_role.py @@ -0,0 +1,94 @@ +#!/usr/bin/python + +# Copyright (c) 2024, Tobias Bauriedel +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or +# https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.netways.elasticstack.plugins.module_utils.elasticsearch_role import ( + Role +) + +def run_module(): + ''' + Elasticsearch user management. + + ``` + netways.elasticstack.elasticsearch_role: + name: new-role + cluster: + - manage_own_api_key + - delegate_pki + indicies: + - names: + - foobar + privileges: + - read + - write + state: present + host: https://localhost:9200 + auth_user: elastic + auth_pass: changeMe123! + verify_certs: false + ca_certs: /etc/elasticsearch/certs/http_ca.crt + ``` + ''' + + # get role + # https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-role.html + + # create or update role + # https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-role.html + + module = AnsibleModule( + argument_spec=dict( + # User args + name=dict(type=str, required=True), + cluster=dict(type=list, required=False), + indicies=dict(type=list, required=False), + state=dict(type=str, required=False, default='present'), + + # Auth args + host=dict(type=str, required=True), + auth_user=dict(type=str, required=True), + auth_pass=dict(type=str, required=True, no_log=True), + ca_certs=dict(type=str, required=False), + verify_certs=dict(type=bool, required=False, default=True) + ) + ) + + result = dict( + failed=False, + changed=False + ) + + if module.params['state'] != 'absent' and module.params['state'] != 'present': + result['stderr'] = "Invalid state given. Please use 'absent' or 'present'" + result['failed'] = True + + module.exit_json(**result) + + + role = Role( + result=result, + role_name=module.params['name'], + cluster=module.params['cluster'], + indicies=module.params['indicies'], + state=module.params['state'], + host=module.params['host'], + auth_user=module.params['auth_user'], + auth_pass=module.params['auth_pass'], + ca_certs=module.params['ca_certs'], + verify_certs=module.params['verify_certs'], + ) + + result = role.return_result() + + module.exit_json(**result) + + +if __name__ == "__main__": + run_module() \ No newline at end of file diff --git a/plugins/modules/elasticsearch_user.py b/plugins/modules/elasticsearch_user.py new file mode 100644 index 00000000..3e05e91b --- /dev/null +++ b/plugins/modules/elasticsearch_user.py @@ -0,0 +1,91 @@ +#!/usr/bin/python + +# Copyright (c) 2024, Tobias Bauriedel +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or +# https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.netways.elasticstack.plugins.module_utils.elasticsearch_user import ( + User +) + + +def run_module(): + ''' + Elasticsearch user management. + + ``` + netways.elasticstack.elasticsearch_user: + name: new-user1 + fullname: New User + password: changeMe123! + email: "new@user.de" + roles: + - new-role1 + enabled: true + state: absent + host: https://localhost:9200 + auth_user: elastic + auth_pass: "{{ elasticstack_password.stdout }}" + verify_certs: false + ca_certs: /etc/elasticsearch/certs/http_ca.crt + ``` + ''' + + module = AnsibleModule( + argument_spec=dict( + # User args + name=dict(type=str, required=True), + fullname=dict(type=str, required=False), + password=dict(type=str, required=True, no_log=True), + email=dict(type=str, required=False), + roles=dict(type=list, required=True), + enabled=dict(type=bool, required=False, default=True), + state=dict(type=str, required=False, default="present"), + + # Auth args + host=dict(type=str, required=True), + auth_user=dict(type=str, required=True), + auth_pass=dict(type=str, required=True, no_log=True), + ca_certs=dict(type=str, required=False), + verify_certs=dict(type=bool, required=False, default=True) + ) + ) + + result = dict( + failed=False, + changed=False + ) + + if module.params['state'] != 'absent' and module.params['state'] != 'present': + result['stderr'] = "Invalid state given. Please use 'absent' or 'present'" + result['failed'] = True + + module.exit_json(**result) + + + user = User( + result=result, + user_name=module.params['name'], + full_name=module.params['fullname'], + password=module.params['password'], + email=module.params['email'], + roles=module.params['roles'], + enabled=module.params['enabled'], + state=module.params['state'], + host=module.params['host'], + auth_user=module.params['auth_user'], + auth_pass=module.params['auth_pass'], + ca_certs=module.params['ca_certs'], + verify_certs=module.params['verify_certs'], + ) + + result = user.return_result() + + module.exit_json(**result) + +if __name__ == "__main__": + run_module() \ No newline at end of file diff --git a/requirements-test.txt b/requirements-test.txt index ac24d2cf..c508b1c5 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -4,3 +4,4 @@ molecule molecule-plugins[docker] pytest passlib +elasticsearch \ No newline at end of file