library: improve ceph_mgr_module module

This moves the logic from the playbook to the ceph_mgr_module.

Signed-off-by: Guillaume Abrioux <gabrioux@ibm.com>
improve-ceph-mgr-module
Guillaume Abrioux 2023-03-07 17:06:48 +01:00
parent 5622a033a9
commit d8685aaef6
2 changed files with 396 additions and 57 deletions

View File

@ -25,6 +25,8 @@ except ImportError:
generate_cmd, \
is_containerized
import datetime
import json
import os
ANSIBLE_METADATA = {
@ -54,14 +56,25 @@ options:
description:
- If 'enable' is used, the module enables the MGR module.
If 'absent' is used, the module disables the MGR module.
If 'auto', is used, the module does:
enable all modules present in 'name',
disable everything not listed in 'name',
don't touch to anything listed in 'mgr_initial_modules' parameter.
required: false
choices: ['enable', 'disable']
default: enable
choices: ['auto, 'enable', 'disable']
default: auto
author:
- Dimitri Savineau <dsavinea@redhat.com>
'''
EXAMPLES = '''
- name: enable some modules
ceph_mgr_module:
name:
- dashboard
- stats
- alerts
- name: enable dashboard mgr module
ceph_mgr_module:
name: dashboard
@ -79,12 +92,67 @@ EXAMPLES = '''
RETURN = '''# '''
def get_run_dir(module,
cluster,
container_image):
cmd = generate_cmd(sub_cmd=['config'],
args=['get', 'mon', 'run_dir'],
cluster=cluster,
container_image=container_image)
rc, out, err = module.run_command(cmd)
if not rc and out:
out = out.strip()
return out
else:
raise RuntimeError("Can't retrieve run_dir config parameter")
def get_mgr_initial_modules(module,
cluster,
container_image):
node_name = os.uname()[1]
run_dir = get_run_dir(module, cluster, container_image)
# /var/run/ceph/ceph-mon.mon0.asok
socket_path = f"{run_dir}/{cluster}-mon.{node_name}.asok"
cmd = ['ceph', '--admin-daemon', socket_path, 'config', 'get', 'mgr_initial_modules', '--format', 'json']
rc, out, err = module.run_command(cmd)
if not rc and out:
out = json.loads(out)
out = [m for m in out['mgr_initial_modules'].split()]
return out
else:
raise RuntimeError(f"Can't retrieve 'mgr_initial_modules' config parameter.\ncmd={cmd}\nrc={rc}:\nstderr:\n{err}\n")
def mgr_module_ls(module, cluster, container_image):
cmd = generate_cmd(sub_cmd=['mgr', 'module'],
args=['ls'],
cluster=cluster,
container_image=container_image)
cmd.extend(['--format', 'json'])
rc, out, err = module.run_command(cmd)
if not rc and out:
out = out.strip()
out = json.loads(out)
return out
raise RuntimeError("Can't retrieve mgr module list")
def get_modules_from_reports(report):
return ','.join([report[0] for report in report])
def get_cmd_from_reports(report):
return [_report[4] for _report in report]
def main():
module = AnsibleModule(
argument_spec=dict(
name=dict(type='str', required=True),
name=dict(type='list', required=True),
cluster=dict(type='str', required=False, default='ceph'),
state=dict(type='str', required=False, default='enable', choices=['enable', 'disable']), # noqa: E501
state=dict(type='str', required=False, default='auto', choices=['enable', 'auto', 'disable']), # noqa: E501
),
supports_check_mode=True,
)
@ -94,15 +162,15 @@ def main():
state = module.params.get('state')
startd = datetime.datetime.now()
changed = False
container_image = is_containerized()
cmd = generate_cmd(sub_cmd=['mgr', 'module'],
args=[state, name],
cluster=cluster,
container_image=container_image)
if module.check_mode:
cmd = generate_cmd(sub_cmd=['mgr', 'module'],
args=['enable', 'noup'],
cluster=cluster,
container_image=container_image)
exit_module(
module=module,
out='',
@ -112,21 +180,103 @@ def main():
startd=startd,
changed=False
)
else:
rc, out, err = module.run_command(cmd)
if 'is already enabled' in err:
changed = False
else:
module_list = mgr_module_ls(module,
cluster=cluster,
container_image=container_image)
enabled_modules = module_list['enabled_modules']
disabled_modules = [module['name'] for module in module_list['disabled_modules']]
always_on_modules = module_list['always_on_modules']
mgr_initial_modules = get_mgr_initial_modules(module, cluster=cluster, container_image=container_image)
ok_report = []
fail_report = []
skip_report = []
cmd = []
out = []
err = []
rc = 0
if state in ['enable', 'disable']:
for m in name:
if m in always_on_modules:
skip_report.append((m, 0, "{m} is always on, skipping", '',))
continue
_list_to_check = disabled_modules if state == 'disable' else enabled_modules
if m not in _list_to_check:
_cmd = generate_cmd(sub_cmd=['mgr', 'module'],
args=[state, m],
cluster=cluster,
container_image=container_image)
rc, _out, _err = module.run_command(_cmd)
_report = (m, rc, _out, _err, _cmd,)
if not rc:
ok_report.append(_report)
else:
fail_report.append(_report)
else:
skip_report.append((m, 0, "{m} already {state}e, skipping.", '', [],))
if not fail_report and not skip_report:
changed = True
exit_module(
module=module,
out=out,
rc=rc,
cmd=cmd,
err=err,
startd=startd,
changed=changed
)
if ok_report:
out.append("Successfully {}d module(s): {}".format(state, get_modules_from_reports(ok_report)))
if fail_report:
err.append("Failed to {} module(s): {}".format(state, get_modules_from_reports(fail_report)))
cmd = get_cmd_from_reports(fail_report)
if skip_report:
out.append("Skipped module(s): {}".format(get_modules_from_reports(skip_report)))
if state == 'auto':
to_enable = list(set(name) - set(enabled_modules))
to_disable = list(set(enabled_modules) - set(name))
enable_report = []
disable_report = []
for m in to_enable:
_cmd = generate_cmd(sub_cmd=['mgr', 'module'],
args=['enable', m],
cluster=cluster,
container_image=container_image)
_rc, _out, _err = module.run_command(_cmd)
enable_report.append((m, _rc, _out, _err, _cmd,))
for m in to_disable:
if m in mgr_initial_modules:
skip_report.append(m)
else:
_cmd = generate_cmd(sub_cmd=['mgr', 'module'],
args=['disable', m],
cluster=cluster,
container_image=container_image)
_rc, _out, _err = module.run_command(_cmd)
disable_report.append((m, _rc, _out, _err, _cmd,))
if not to_enable and len(to_disable) == len(skip_report):
out = ['Nothing to do.']
else:
for _report in [enable_report, disable_report]:
action = 'enable' if _report == enable_report else 'disable'
if _report:
if any([report[1] for report in _report]):
rc = 1
module_ok = ','.join(sorted([report[0] for report in _report if not report[1]]))
module_failed = ','.join(sorted([report[0] for report in _report if report[1]]))
if module_ok:
out.append(f"action = {action} success for the following modules: {module_ok}")
if rc:
err_msg = "\n".join(sorted([report[3] for report in _report if report[1]]))
err.append(f'failed to enable module(s): {module_failed}. Error message(s):\n{err_msg}')
cmd.extend([report[4] for report in _report if report[4]])
if out:
changed = True
exit_module(
module=module,
out="\n".join(out),
rc=rc,
cmd=cmd,
err="\n".join(err),
startd=startd,
changed=changed
)
if __name__ == '__main__':

View File

@ -1,4 +1,5 @@
from mock.mock import patch
from ansible.module_utils.basic import AnsibleModule
import os
import pytest
import ca_test_common
@ -10,6 +11,9 @@ fake_container_image = 'quay.io/ceph/daemon:latest'
fake_module = 'noup'
fake_user = 'client.admin'
fake_keyring = '/etc/ceph/{}.{}.keyring'.format(fake_cluster, fake_user)
fake_mgr_module_ls_output = {"enabled_modules": ["iostat", "nfs", "restful"],
"disabled_modules": [{"name": "fake"}],
"always_on_modules": ["foo", "bar"]}
class TestCephMgrModuleModule(object):
@ -38,37 +42,64 @@ class TestCephMgrModuleModule(object):
result = result.value.args[0]
assert not result['changed']
assert result['cmd'] == ['ceph', '-n', fake_user, '-k', fake_keyring, '--cluster', fake_cluster, 'mgr', 'module', 'enable', fake_module]
assert result['cmd'] == ['ceph',
'-n',
fake_user,
'-k',
fake_keyring,
'--cluster',
fake_cluster,
'mgr', 'module',
'enable', fake_module]
assert result['rc'] == 0
assert not result['stdout']
assert not result['stderr']
@patch('ceph_mgr_module.get_mgr_initial_modules',
return_value=['restful', 'iostat', 'nfs'])
@patch('ceph_mgr_module.mgr_module_ls',
return_value=fake_mgr_module_ls_output)
@patch('ansible.module_utils.basic.AnsibleModule.exit_json')
@patch('ansible.module_utils.basic.AnsibleModule.run_command')
def test_with_failure(self, m_run_command, m_exit_json):
def test_with_failure(self,
m_run_command,
m_exit_json,
m_mgr_module_ls,
m_get_mgr_initial_modules):
ca_test_common.set_module_args({
'name': fake_module
})
m_exit_json.side_effect = ca_test_common.exit_json
stdout = ''
stderr = 'Error ENOENT: all mgr daemons do not support module \'{}\', pass --force to force enablement'.format(fake_module)
rc = 2
m_run_command.return_value = rc, stdout, stderr
m_run_command.return_value = 123, stdout, stderr
with pytest.raises(ca_test_common.AnsibleExitJson) as result:
ceph_mgr_module.main()
result = result.value.args[0]
assert result['changed']
assert result['cmd'] == ['ceph', '-n', fake_user, '-k', fake_keyring, '--cluster', fake_cluster, 'mgr', 'module', 'enable', fake_module]
assert result['rc'] == rc
assert result['stderr'] == stderr
assert not result['changed']
assert result['cmd'] == [['ceph',
'-n',
fake_user,
'-k',
fake_keyring,
'--cluster',
fake_cluster,
'mgr', 'module',
'enable', fake_module]]
assert result['rc']
assert result['stderr'] == "failed to enable module(s): noup. Error message(s):\nError ENOENT: " \
"all mgr daemons do not support module 'noup', pass --force to force enablement"
@patch('ceph_mgr_module.get_mgr_initial_modules', return_value=['restful', 'iostat', 'nfs'])
@patch('ceph_mgr_module.mgr_module_ls', return_value=fake_mgr_module_ls_output)
@patch('ansible.module_utils.basic.AnsibleModule.exit_json')
@patch('ansible.module_utils.basic.AnsibleModule.run_command')
def test_enable_module(self, m_run_command, m_exit_json):
def test_enable_module(self, m_run_command, m_exit_json, m_mgr_module_ls, m_get_mgr_initial_modules):
ca_test_common.set_module_args({
'name': fake_module,
'state': 'enable'
})
m_exit_json.side_effect = ca_test_common.exit_json
stdout = ''
@ -81,14 +112,19 @@ class TestCephMgrModuleModule(object):
result = result.value.args[0]
assert result['changed']
assert result['cmd'] == ['ceph', '-n', fake_user, '-k', fake_keyring, '--cluster', fake_cluster, 'mgr', 'module', 'enable', fake_module]
assert result['rc'] == rc
assert result['cmd'] == []
assert not result['rc']
assert result['stderr'] == stderr
assert result['stdout'] == stdout
assert result['stdout'] == "Successfully enabled module(s): noup"
@patch('ceph_mgr_module.get_mgr_initial_modules', return_value=['restful', 'iostat', 'nfs'])
@patch('ceph_mgr_module.mgr_module_ls', return_value={"enabled_modules": ["iostat", "nfs", "restful", "noup"],
"disabled_modules": [{"name": "fake"}],
"always_on_modules": ["foo", "bar"]
})
@patch('ansible.module_utils.basic.AnsibleModule.exit_json')
@patch('ansible.module_utils.basic.AnsibleModule.run_command')
def test_already_enable_module(self, m_run_command, m_exit_json):
def test_already_enabled_module(self, m_run_command, m_exit_json, m_mgr_module_ls, m_get_mgr_initial_modules):
ca_test_common.set_module_args({
'name': fake_module,
})
@ -103,14 +139,16 @@ class TestCephMgrModuleModule(object):
result = result.value.args[0]
assert not result['changed']
assert result['cmd'] == ['ceph', '-n', fake_user, '-k', fake_keyring, '--cluster', fake_cluster, 'mgr', 'module', 'enable', fake_module]
assert result['rc'] == rc
assert result['stderr'] == stderr
assert result['stdout'] == stdout
assert result['cmd'] == []
assert not result['rc']
assert result['stderr'] == ''
assert result['stdout'] == 'Nothing to do.'
@patch('ceph_mgr_module.get_mgr_initial_modules', return_value=['restful', 'iostat', 'nfs'])
@patch('ceph_mgr_module.mgr_module_ls', return_value=fake_mgr_module_ls_output)
@patch('ansible.module_utils.basic.AnsibleModule.exit_json')
@patch('ansible.module_utils.basic.AnsibleModule.run_command')
def test_disable_module(self, m_run_command, m_exit_json):
def test_disable_module(self, m_run_command, m_exit_json, m_mgr_module_ls, m_get_mgr_initial_modules):
ca_test_common.set_module_args({
'name': fake_module,
'state': 'disable'
@ -126,22 +164,23 @@ class TestCephMgrModuleModule(object):
result = result.value.args[0]
assert result['changed']
assert result['cmd'] == ['ceph', '-n', fake_user, '-k', fake_keyring, '--cluster', fake_cluster, 'mgr', 'module', 'disable', fake_module]
assert result['cmd'] == []
assert result['rc'] == rc
assert result['stderr'] == stderr
assert result['stdout'] == stdout
assert result['stdout'] == 'Successfully disabled module(s): noup'
@patch.dict(os.environ, {'CEPH_CONTAINER_BINARY': fake_container_binary})
@patch.dict(os.environ, {'CEPH_CONTAINER_IMAGE': fake_container_image})
@patch('ceph_mgr_module.get_mgr_initial_modules', return_value=['restful', 'iostat', 'nfs'])
@patch('ceph_mgr_module.mgr_module_ls', return_value=fake_mgr_module_ls_output)
@patch('ansible.module_utils.basic.AnsibleModule.exit_json')
@patch('ansible.module_utils.basic.AnsibleModule.run_command')
def test_with_container(self, m_run_command, m_exit_json):
def test_disable_module_already_disabled(self, m_run_command, m_exit_json, m_mgr_module_ls, m_get_mgr_initial_modules):
ca_test_common.set_module_args({
'name': fake_module,
'name': 'fake',
'state': 'disable'
})
m_exit_json.side_effect = ca_test_common.exit_json
stdout = ''
stderr = '{} is set'.format(fake_module)
stderr = ''
rc = 0
m_run_command.return_value = rc, stdout, stderr
@ -149,14 +188,164 @@ class TestCephMgrModuleModule(object):
ceph_mgr_module.main()
result = result.value.args[0]
assert result['changed']
assert result['cmd'] == [fake_container_binary, 'run', '--rm', '--net=host',
'-v', '/etc/ceph:/etc/ceph:z',
'-v', '/var/lib/ceph/:/var/lib/ceph/:z',
'-v', '/var/log/ceph/:/var/log/ceph/:z',
'--entrypoint=ceph', fake_container_image,
'-n', fake_user, '-k', fake_keyring,
'--cluster', fake_cluster, 'mgr', 'module', 'enable', fake_module]
assert not result['changed']
assert result['cmd'] == []
assert result['rc'] == rc
assert result['stderr'] == stderr
assert result['stdout'] == stdout
assert result['stdout'] == 'Skipped module(s): fake'
@patch.dict(os.environ, {'CEPH_CONTAINER_BINARY': fake_container_binary})
@patch.dict(os.environ, {'CEPH_CONTAINER_IMAGE': fake_container_image})
@patch('ceph_mgr_module.get_mgr_initial_modules', return_value=['restful', 'iostat', 'nfs'])
@patch('ceph_mgr_module.mgr_module_ls', return_value=fake_mgr_module_ls_output)
@patch('ansible.module_utils.basic.AnsibleModule.exit_json')
@patch('ansible.module_utils.basic.AnsibleModule.run_command')
def test_with_container(self, m_run_command, m_exit_json, m_mgr_module_ls, m_get_mgr_initial_modules):
ca_test_common.set_module_args({
'name': fake_module,
})
m_exit_json.side_effect = ca_test_common.exit_json
stderr = ''
rc = 0
m_run_command.return_value = rc, '', stderr
with pytest.raises(ca_test_common.AnsibleExitJson) as result:
ceph_mgr_module.main()
result = result.value.args[0]
assert result['changed']
assert result['cmd'] == [[fake_container_binary, 'run', '--rm', '--net=host',
'-v', '/etc/ceph:/etc/ceph:z',
'-v', '/var/lib/ceph/:/var/lib/ceph/:z',
'-v', '/var/log/ceph/:/var/log/ceph/:z',
'--entrypoint=ceph', fake_container_image,
'-n', fake_user, '-k', fake_keyring,
'--cluster', fake_cluster, 'mgr', 'module', 'enable', fake_module]]
assert result['rc'] == rc
assert result['stderr'] == ''
assert result['stdout'] == 'action = enable success for the following modules: noup'
@patch('ansible.module_utils.basic.AnsibleModule.run_command')
def test_get_run_dir(self, m_run_command):
m_run_command.return_value = 0, '/var/run/ceph', ''
assert ceph_mgr_module.get_run_dir(AnsibleModule, 'fake', 'fake') == '/var/run/ceph'
@patch('ansible.module_utils.basic.AnsibleModule.run_command')
def test_get_run_dir_fail(self, m_run_command):
m_run_command.return_value = 1, '', ''
with pytest.raises(RuntimeError):
ceph_mgr_module.get_run_dir(AnsibleModule, 'fake', 'fake')
@patch('ceph_mgr_module.get_run_dir', return_value='/var/run/ceph')
@patch('ansible.module_utils.basic.AnsibleModule.run_command')
def test_get_mgr_initial_modules(self, m_run_command, m_get_run_dir):
m_run_command.return_value = 0, '{"mgr_initial_modules":"foo bar"}', ''
m_get_run_dir.return_value = '/var/run/ceph'
assert ceph_mgr_module.get_mgr_initial_modules(AnsibleModule, 'ceph', None) == ["foo", "bar"]
@patch('ceph_mgr_module.get_run_dir', return_value='/var/run/ceph')
@patch('ansible.module_utils.basic.AnsibleModule.run_command')
def test_get_mgr_initial_modules_fail(self, m_run_command, m_get_run_dir):
m_run_command.return_value = 1, '', 'error'
with pytest.raises(RuntimeError):
ceph_mgr_module.get_mgr_initial_modules(AnsibleModule, 'ceph', None)
@patch('ansible.module_utils.basic.AnsibleModule.run_command')
def test_mgr_module_ls(self, m_run_command):
m_run_command.return_value = 0, '{}', ''
assert ceph_mgr_module.mgr_module_ls(AnsibleModule, 'ceph', None) == {}
@patch('ansible.module_utils.basic.AnsibleModule.run_command')
def test_mgr_module_ls_fail(self, m_run_command):
m_run_command.return_value = 1, '', 'Error'
with pytest.raises(RuntimeError):
ceph_mgr_module.mgr_module_ls(AnsibleModule, 'ceph', None)
@patch('ceph_mgr_module.get_mgr_initial_modules', return_value=['restful', 'iostat', 'nfs'])
@patch('ceph_mgr_module.mgr_module_ls',
return_value={"enabled_modules": ["iostat",
"nfs",
"restful",
"foobar",
"zabbix"],
"disabled_modules": [{"name": "fake"}],
"always_on_modules": ["foo", "bar"]})
@patch('ansible.module_utils.basic.AnsibleModule.exit_json')
@patch('ansible.module_utils.basic.AnsibleModule.run_command')
def test_mgr_module_auto(self, m_run_command, m_exit_json, m_mgr_module_ls,
m_get_mgr_initial_modules):
m_run_command.return_value = 0, '', ''
m_exit_json.side_effect = ca_test_common.exit_json
ca_test_common.set_module_args({
'name': ["foo", "bar"],
'state': 'auto'
})
with pytest.raises(ca_test_common.AnsibleExitJson) as result:
ceph_mgr_module.main()
result = result.value.args[0]
assert result['stdout'] == "action = enable success for the following modules: bar,foo\n" \
"action = disable success for the following modules: foobar,zabbix"
assert result['changed']
assert not result['rc']
def test_get_cmd_from_reports(self):
cmd = ceph_mgr_module.get_cmd_from_reports([("foo", 123, "", "Error", ['foo', 'bar'],),
("bar", 456, "", "Error", ['bar', 'foo'],)])
assert cmd == [['foo', 'bar'], ['bar', 'foo']]
@patch('ceph_mgr_module.get_mgr_initial_modules', return_value=['restful', 'iostat', 'nfs'])
@patch('ceph_mgr_module.mgr_module_ls', return_value=fake_mgr_module_ls_output)
@patch('ansible.module_utils.basic.AnsibleModule.exit_json')
@patch('ansible.module_utils.basic.AnsibleModule.run_command')
def test_enable_module_always_on(self, m_run_command, m_exit_json, m_mgr_module_ls, m_get_mgr_initial_modules):
ca_test_common.set_module_args({
'name': 'foo',
'state': 'enable'
})
m_exit_json.side_effect = ca_test_common.exit_json
stdout = ''
stderr = ''
rc = 0
m_run_command.return_value = rc, stdout, stderr
with pytest.raises(ca_test_common.AnsibleExitJson) as result:
ceph_mgr_module.main()
result = result.value.args[0]
assert not result['changed']
assert result['cmd'] == []
assert not result['rc']
assert result['stderr'] == ''
assert result['stdout'] == "Skipped module(s): foo"
@patch('ceph_mgr_module.get_mgr_initial_modules', return_value=['restful', 'iostat', 'nfs'])
@patch('ceph_mgr_module.mgr_module_ls', return_value=fake_mgr_module_ls_output)
@patch('ansible.module_utils.basic.AnsibleModule.exit_json')
@patch('ansible.module_utils.basic.AnsibleModule.run_command')
def test_enable_module_with_failure(self, m_run_command, m_exit_json, m_mgr_module_ls, m_get_mgr_initial_modules):
ca_test_common.set_module_args({
'name': ['fake', 'fake2'],
'state': 'enable'
})
m_exit_json.side_effect = ca_test_common.exit_json
rc = 1
m_run_command.return_value = rc, '', 'Error'
with pytest.raises(ca_test_common.AnsibleExitJson) as result:
ceph_mgr_module.main()
result = result.value.args[0]
assert not result['changed']
assert result['cmd'] == [['ceph',
'-n', 'client.admin',
'-k', '/etc/ceph/ceph.client.admin.keyring',
'--cluster', 'ceph', 'mgr', 'module',
'enable', 'fake'],
['ceph', '-n', 'client.admin',
'-k', '/etc/ceph/ceph.client.admin.keyring',
'--cluster', 'ceph', 'mgr', 'module',
'enable', 'fake2']]
assert result['rc']
assert result['stderr'] == 'Failed to enable module(s): fake,fake2'
assert result['stdout'] == ''