feat: manage users

This commit is contained in:
Adrien Waksberg 2023-09-29 15:54:34 +02:00
parent aaeea21e54
commit f1ba54d2ad
10 changed files with 228 additions and 26 deletions

View file

@ -7,6 +7,7 @@ Which is based on [Keep A Changelog](http://keepachangelog.com/)
### Added ### Added
- feat: manage user
- feat: add variable to set major version - feat: add variable to set major version
- feat: add ilm policy - feat: add ilm policy
- test: add support debian 12 - test: add support debian 12
@ -17,6 +18,7 @@ Which is based on [Keep A Changelog](http://keepachangelog.com/)
### Changed ### Changed
- major default version is 8
- replace kitchen to molecule - replace kitchen to molecule
### Fixed ### Fixed

View file

@ -16,6 +16,8 @@ Install and configure Elasticsearch
* `elasticsearch_major_version` - set the major version (default: `7`) * `elasticsearch_major_version` - set the major version (default: `7`)
* `elasticsearch_heap_size` - set the heap size (default: `1g`) * `elasticsearch_heap_size` - set the heap size (default: `1g`)
* `elasticsearch_api_user` - set the admin user (default: `elastic`)
* `elasticsearch_api_password` - set the password for api
* `elasticsearch_config` - hash with the configuration (see [elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/settings.html)) * `elasticsearch_config` - hash with the configuration (see [elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/settings.html))
``` ```

View file

@ -1,10 +1,13 @@
--- ---
elasticsearch_major_version: 7 elasticsearch_major_version: 8
elasticsearch_api_user: elastic
elasticsearch_heap_size: 1g elasticsearch_heap_size: 1g
elasticsearch_config: {} elasticsearch_config: {}
elasticsearch_default_config: elasticsearch_default_config:
path.data: /var/lib/elasticsearch path.data: /var/lib/elasticsearch
path.logs: /var/log/elasticsearch path.logs: /var/log/elasticsearch
xpack.security.transport.ssl.enabled: false
xpack.security.http.ssl.enabled: false
elasticsearch_full_config: "{{ elasticsearch_default_config | combine(elasticsearch_config) }}" elasticsearch_full_config: "{{ elasticsearch_default_config | combine(elasticsearch_config) }}"
elasticsearch_index_templates: {} elasticsearch_index_templates: {}
elasticsearch_ilm_policies: {} elasticsearch_ilm_policies: {}

View file

@ -0,0 +1,52 @@
#!/usr/bin/python
from ansible.module_utils.basic import *
from ansible.module_utils.elasticsearch_api import *
import subprocess
class ElasticsearchInitPassword:
def __init__(self, api_url, api_user, api_password):
self.api = ElasticsearchApi(
api_url,
api_user,
api_password
)
self.user = api_user
self.password = api_password
def is_set(self):
status_code, _ = self.api.get('_cluster/health')
if status_code == 401:
return False
return True
def change(self):
subprocess.run(
['/usr/share/elasticsearch/bin/elasticsearch-reset-password', '-u', self.user, '-b', '-i'],
input='{}\n{}'.format(self.password, self.password).encode()
)
def main():
fields = {
'api_url': { 'type': 'str', 'default': 'http://127.0.0.1:9200' },
'api_user': { 'type': 'str', 'default': None },
'api_password': { 'type': 'str', 'default': None, 'no_log': True },
}
module = AnsibleModule(argument_spec=fields)
changed = False
init = ElasticsearchInitPassword(
module.params['api_url'],
module.params['api_user'],
module.params['api_password'],
)
if not init.is_set():
init.change()
changed = True
module.exit_json(changed=changed)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,106 @@
#!/usr/bin/python
from ansible.module_utils.basic import *
from ansible.module_utils.elasticsearch_api import *
class ElasticsearchUser:
def __init__(self, api_url, api_user, api_password, name, password, roles):
self.api = ElasticsearchApi(
api_url,
api_user,
api_password
)
self.api_url = api_url
self.name = name
self.password = password
self.roles = roles
self.exist = False
self.data = {}
def get_data(self):
status_code, data = self.api.get('_security/user/{}'.format(self.name))
if status_code == 200:
self.exist = True
self.data = data[self.name]
def roles_have_changed(self):
for role in self.roles:
if role not in self.data['roles']:
return True
for role in self.data['roles']:
if role not in self.roles:
return True
return False
def password_has_changed(self):
api = ElasticsearchApi(
self.api_url,
self.name,
self.password
)
status_code, _ = api.get('_cluster/health')
if status_code == 401:
return True
return False
def has_changed(self):
if self.roles_have_changed():
return True
if self.password_has_changed():
return True
return False
def create(self):
self.api.put(
'_security/user/{}'.format(self.name),
{
'password': self.password,
'roles': self.roles
}
)
def delete(self):
self.api.delete('_security/user/{}'.format(self.name))
def main():
fields = {
'name': { 'type': 'str', 'required': True },
'password': { 'type': 'str', 'required': True, 'no_log': True },
'roles': { 'type': 'list', 'default': [] },
'api_url': { 'type': 'str', 'default': 'http://127.0.0.1:9200' },
'api_user': { 'type': 'str', 'default': None },
'api_password': { 'type': 'str', 'default': None, 'no_log': True },
'state': { 'type': 'str', 'default': 'present', 'choice': ['present', 'absent'] },
}
module = AnsibleModule(argument_spec=fields)
changed = False
user = ElasticsearchUser(
module.params['api_url'],
module.params['api_user'],
module.params['api_password'],
module.params['name'],
module.params['password'],
module.params['roles'],
)
user.get_data()
if module.params['state'] == 'present':
if not user.exist or user.has_changed():
user.create()
changed = True
else:
if user.exist:
user.delete()
changed = True
module.exit_json(changed=changed)
if __name__ == '__main__':
main()

View file

@ -5,28 +5,25 @@ import base64
class ElasticsearchApi: class ElasticsearchApi:
def __init__(self, url, user, password): def __init__(self, url, user, password):
self.url = url self.url = url
self.headers = {} self.basic = None
if user and password: if user and password:
token = base64.b64encode('{}:{}',) self.basic = requests.auth.HTTPBasicAuth(user, password)
self.headers = { 'Authorization': 'Basic ' + base64.b64encode({},) }
def get(self, path): def get(self, path):
r = requests.get( r = requests.get(
'{}/{}'.format(self.url, path), '{}/{}'.format(self.url, path),
headers=self.headers auth=self.basic
) )
if r.status_code == 500: if r.status_code == 500:
raise Exception('Server return 500 error: {}'.format(r.text)) raise Exception('Server return 500 error: {}'.format(r.text))
elif r.status_code == 401:
raise Exception('Authentification has failed')
return r.status_code, r.json() return r.status_code, r.json()
def put(self, path, data): def put(self, path, data):
r = requests.put( r = requests.put(
'{}/{}'.format(self.url, path), '{}/{}'.format(self.url, path),
headers=self.headers, auth=self.basic,
json=data json=data
) )

View file

@ -4,7 +4,13 @@
roles: roles:
- ansible-role-elasticsearch - ansible-role-elasticsearch
vars: vars:
elasticsearch_password: mysecret
elasticsearch_heap_size: 512m elasticsearch_heap_size: 512m
elasticsearch_users:
toto:
password: supers3cret
roles:
- viewer
elasticsearch_index_templates: elasticsearch_index_templates:
test: test:
index_patterns: index_patterns:

View file

@ -11,6 +11,8 @@ platforms:
command: /bin/systemd command: /bin/systemd
capabilities: capabilities:
- SYS_ADMIN - SYS_ADMIN
published_ports:
- 127.0.0.1:5601:5601
lint: | lint: |
set -e set -e
yamllint . yamllint .

View file

@ -2,31 +2,36 @@ import os
import testinfra.utils.ansible_runner import testinfra.utils.ansible_runner
def test_packages(host): def test_packages(host):
package = host.package('elasticsearch') package = host.package('elasticsearch')
assert package.is_installed assert package.is_installed
def test_config_file(host): def test_config_file(host):
config = host.file('/etc/elasticsearch/elasticsearch.yml') config = host.file('/etc/elasticsearch/elasticsearch.yml')
assert config.user == 'root' assert config.user == 'root'
assert config.group == 'elasticsearch' assert config.group == 'elasticsearch'
assert config.mode == 0o640 assert config.mode == 0o640
assert config.contains('path.data: /var/lib/elasticsearch') assert config.contains('path.data: /var/lib/elasticsearch')
def test_service(host): def test_service(host):
service = host.service('elasticsearch') service = host.service('elasticsearch')
assert service.is_running assert service.is_running
assert service.is_enabled assert service.is_enabled
def test_socket(host): def test_socket(host):
for port in [9200, 9300]: for port in [9200, 9300]:
socket = host.socket('tcp://127.0.0.1:%d' % (port)) socket = host.socket('tcp://127.0.0.1:%d' % (port))
assert socket.is_listening assert socket.is_listening
def test_java_memory(host): def test_java_memory(host):
process = host.process.get(user='elasticsearch', comm='java') process = host.process.filter(user='elasticsearch', comm='java')
assert '-Xms512m' in process.args assert '-Xms512m' in process[1].args
assert '-Xmx512m' in process.args assert '-Xmx512m' in process[1].args
def test_elasticsearch_template(host): def test_elasticsearch_template(host):
result = host.check_output('curl -v http://127.0.0.1:9200/_template/test') result = host.check_output('curl -v -u elastic:mysecret http://127.0.0.1:9200/_template/test')
assert '"number_of_replicas":"1"' in result assert '"number_of_replicas":"1"' in result
def test_elasticsearch_user(host):
result = host.check_output('curl -v -u elastic:mysecret http://127.0.0.1:9200/_security/user/toto')
assert '"username":"toto"' in result
assert '"roles":["viewer"]' in result

View file

@ -1,8 +1,31 @@
--- ---
- name: Init elastic password
elasticsearch_init_password:
api_user: "{{ elasticsearch_api_user }}"
api_password: "{{ elasticsearch_password }}"
run_once: true
tags: elasticsearch
- name: Manage users
elasticsearch_user:
name: "{{ item.key }}"
password: "{{ item.value.password }}"
roles: "{{ item.value.roles | default(omit) }}"
api_user: "{{ elasticsearch_api_user }}"
api_password: "{{ elasticsearch_password }}"
state: "{{ item.value.state | default('present') }}"
loop: "{{ elasticsearch_users | dict2items }}"
loop_control:
label: "{{ item.key }}"
run_once: true
tags: elasticsearch
- name: Copy ilm policies - name: Copy ilm policies
elasticsearch_ilm_policy: elasticsearch_ilm_policy:
name: "{{ item.key }}" name: "{{ item.key }}"
phases: "{{ item.value | default({}) }}" phases: "{{ item.value | default({}) }}"
api_user: "{{ elasticsearch_api_user }}"
api_password: "{{ elasticsearch_password }}"
loop: "{{ elasticsearch_ilm_policies | dict2items }}" loop: "{{ elasticsearch_ilm_policies | dict2items }}"
loop_control: loop_control:
label: "{{ item.key }}" label: "{{ item.key }}"
@ -15,6 +38,10 @@
index_patterns: "{{ item.value.index_patterns }}" index_patterns: "{{ item.value.index_patterns }}"
settings: "{{ item.value.settings | default({}) }}" settings: "{{ item.value.settings | default({}) }}"
mappings: "{{ item.value.mappings | default({}) }}" mappings: "{{ item.value.mappings | default({}) }}"
api_user: "{{ elasticsearch_api_user }}"
api_password: "{{ elasticsearch_password }}"
loop: "{{ elasticsearch_index_templates | dict2items }}" loop: "{{ elasticsearch_index_templates | dict2items }}"
loop_control:
label: "{{ item.key }}"
run_once: true run_once: true
tags: elasticsearch tags: elasticsearch