diff --git a/library/ceph_mgr_module.py b/library/ceph_mgr_module.py index 0c2d177e4..c896c9faa 100644 --- a/library/ceph_mgr_module.py +++ b/library/ceph_mgr_module.py @@ -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 ''' 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__': diff --git a/tests/library/test_ceph_mgr_module.py b/tests/library/test_ceph_mgr_module.py index d426a9503..03c4294f8 100644 --- a/tests/library/test_ceph_mgr_module.py +++ b/tests/library/test_ceph_mgr_module.py @@ -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'] == ''