Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions plugins/modules/ceph_config_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
from __future__ import absolute_import, division, print_function
from typing import Any, Dict, List, Tuple, Union
__metaclass__ = type

from ansible.module_utils.basic import AnsibleModule # type: ignore
try:
from ansible.module_utils.ceph_common import exit_module, build_base_cmd_shell, fatal # type: ignore
except ImportError:
from module_utils.ceph_common import exit_module, build_base_cmd_shell, fatal # type: ignore

import datetime
import json

ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}

DOCUMENTATION = '''
---
module: ceph_config_key
short_description: Ensure a Ceph config-key is present or absent
version_added: ""
description:
- Manage Ceph config-key entries declaratively. Ensures the given key
has the specified value (state=present) or is removed (state=absent).
options:
option:
type: str
description:
- Name of the config-key to manage.
required: true
value:
type: str
description:
- Value to set for the config-key. Required when state is present.
required: true when state is present
state:
type: str
description:
- Whether the config-key should exist with the given value (present)
or be removed (absent).
required: false
default: present
choices: [ present, absent ]
fsid:
type: str
description:
- The fsid of the Ceph cluster to interact with.
required: false
image:
type: str
description:
- The Ceph container image to use.
required: false
'''

EXAMPLES = '''
- name: Ensure config/mgr/mgr/prometheus/scrape_interval is set to 15
ceph_config_key:
option: config/mgr/mgr/prometheus/scrape_interval
value: "15"
state: present

- name: Set mgr/cephadm/services/prometheus/prometheus.yml from file
ceph_config_key:
option: mgr/cephadm/services/prometheus/prometheus.yml
value: "{{ lookup('file', '/path/to/prometheus.yml') }}"
state: present

- name: Remove config-key if present
ceph_config_key:
option: config/mgr/mgr/prometheus/scrape_interval
state: absent
'''

RETURN = '''
stdout:
description: The current value of the config-key after the run (when state is present).
type: str
returned: always
'''


def set_config_key_value(module: "AnsibleModule",
option: str,
value: str) -> Tuple[int, List[str], str, str]:
cmd = build_base_cmd_shell(module)
cmd.extend(['ceph', 'config-key', 'set', option, '-i', '-'])

# pass the value through stdin to avoid issues with multi-line values, encoding, etc.
rc, out, err = module.run_command(args=cmd, data=value)

return rc, cmd, out.strip(), err


def del_config_key(module: "AnsibleModule", option: str) -> Tuple[int, List[str], str, str]:
cmd = build_base_cmd_shell(module)
cmd.extend(['ceph', 'config-key', 'del', option])
rc, out, err = module.run_command(cmd)
return rc, cmd, out.strip(), err


def get_config_key_dump(module: "AnsibleModule") -> Tuple[int, List[str], str, str]:
cmd = build_base_cmd_shell(module)
cmd.extend(['ceph', 'config-key', 'dump', '--format', 'json'])
rc, out, err = module.run_command(cmd)
if rc:
fatal(message=f"Can't get current configuration via `ceph config-key dump`.Error:\n{err}", module=module)
out = out.strip()
return rc, cmd, out, err


def get_config_key_current_value(option: str, config_dump: Dict[str, Any]) -> Union[str, None]:
for key in config_dump:
if key == option:
v = config_dump[key]
return v if v is None else str(v)
return None


def run(module: "AnsibleModule") -> None:
option = module.params.get('option')
value = module.params.get('value')
state = module.params.get('state')

startd = datetime.datetime.now()
changed = False
diff = None
out = ''

rc, cmd, dumpout, err = get_config_key_dump(module)
config_dump = json.loads(dumpout)
current_value = get_config_key_current_value(option, config_dump)

if state == 'present':
if value == current_value:
out = current_value or ''
else:
changed = True
diff = dict(before=current_value, after=value)
if not module.check_mode:
rc, cmd, out, err = set_config_key_value(module, option, value)
if rc:
fatal(message=f"Failed to set config-key '{option}'. Error:\n{err}", module=module)
else:
out = value
else:
# state == 'absent'
if current_value is None:
out = ''
else:
changed = True
diff = dict(before=current_value, after='')
if not module.check_mode:
rc, cmd, out, err = del_config_key(module, option)
if rc:
fatal(message=f"Failed to delete config-key '{option}'. Error:\n{err}", module=module)
else:
out = ''

exit_module(module=module, out=out, rc=rc,
cmd=cmd, err=err, startd=startd,
changed=changed, diff=diff)


def main() -> None:
module = AnsibleModule(
argument_spec=dict(
option=dict(type='str', required=True),
value=dict(type='str', required=False),
state=dict(type='str', required=False, choices=['present', 'absent'], default='present'),
fsid=dict(type='str', required=False),
image=dict(type='str', required=False)
),
supports_check_mode=True,
required_if=[['state', 'present', ['value']]]
)
run(module)


if __name__ == '__main__':
main()
191 changes: 191 additions & 0 deletions tests/library/test_ceph_config_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
from __future__ import annotations

from typing import Any, Dict, Optional
from unittest.mock import MagicMock
import pytest
import common
import ceph_config_key


def _module_params(
option: str,
state: str = 'present',
value: Optional[str] = None,
**kwargs: Any,
) -> Dict[str, Any]:
params: Dict[str, Any] = {
'option': option,
'state': state,
'fsid': None,
'image': None,
}
if value is not None:
params['value'] = value
params.update(kwargs)
return params


class TestCephConfigKey(object):

def test_state_present_sets_config_key(self) -> None:
"""Test state=present sets config key when value differs."""
module: MagicMock = MagicMock()
module.params = _module_params(
'config/mgr/mgr/prometheus/scrape_interval',
state='present',
value='15',
)
module.check_mode = False
module.exit_json.side_effect = common.exit_json
module.fail_json.side_effect = common.fail_json

module.run_command.side_effect = [
(0, '{"config/mgr/mgr/prometheus/scrape_interval": "10"}', ''), # dump
(0, '', ''), # set
]

with pytest.raises(common.AnsibleExitJson) as result:
ceph_config_key.run(module)

res: Dict[str, Any] = result.value.args[0]
assert res['changed'] is True
assert res['cmd'] == [
'cephadm', 'shell', 'ceph', 'config-key', 'set',
'config/mgr/mgr/prometheus/scrape_interval', '-i', '-'
]
assert res['diff'] == {'before': '10', 'after': '15'}
assert res['stdout'] == ''
assert res['stderr'] == ''
assert res['rc'] == 0

def test_state_present_idempotent(self) -> None:
"""Test state=present when value already matches (no change)."""
module: MagicMock = MagicMock()
module.params = _module_params(
'config/mgr/mgr/prometheus/scrape_interval',
state='present',
value='15',
)
module.check_mode = False
module.exit_json.side_effect = common.exit_json
module.fail_json.side_effect = common.fail_json

module.run_command.return_value = (
0,
'{"config/mgr/mgr/prometheus/scrape_interval": "15"}',
'',
)

with pytest.raises(common.AnsibleExitJson) as result:
ceph_config_key.run(module)

res: Dict[str, Any] = result.value.args[0]
assert not res['changed']
assert res['stdout'] == '15'
assert res['stderr'] == ''
assert res['rc'] == 0

def test_state_present_check_mode(self) -> None:
"""Test state=present in check mode (reports change, no set run)."""
module: MagicMock = MagicMock()
module.params = _module_params(
'config/mgr/mgr/prometheus/scrape_interval',
state='present',
value='15',
)
module.check_mode = True
module.exit_json.side_effect = common.exit_json
module.fail_json.side_effect = common.fail_json

module.run_command.return_value = (
0,
'{"config/mgr/mgr/prometheus/scrape_interval": "10"}',
'',
)

with pytest.raises(common.AnsibleExitJson) as result:
ceph_config_key.run(module)

res: Dict[str, Any] = result.value.args[0]
assert res['changed']
assert res['diff'] == {'before': '10', 'after': '15'}
assert module.run_command.call_count == 1 # only dump
assert res['stdout'] == '15'
assert res['stderr'] == ''
assert res['rc'] == 0

def test_state_absent_removes_config_key(self) -> None:
"""Test state=absent removes config key when it exists."""
module: MagicMock = MagicMock()
module.params = _module_params(
'config/mgr/mgr/prometheus/scrape_interval',
state='absent',
)
module.check_mode = False
module.exit_json.side_effect = common.exit_json
module.fail_json.side_effect = common.fail_json

module.run_command.side_effect = [
(0, '{"config/mgr/mgr/prometheus/scrape_interval": "15"}', ''), # dump
(0, '', ''), # del
]

with pytest.raises(common.AnsibleExitJson) as result:
ceph_config_key.run(module)

res: Dict[str, Any] = result.value.args[0]
assert res['changed'] is True
assert res['cmd'] == [
'cephadm', 'shell', 'ceph', 'config-key', 'del',
'config/mgr/mgr/prometheus/scrape_interval',
]
assert res['diff'] == {'before': '15', 'after': ''}
assert res['stdout'] == ''
assert res['stderr'] == ''
assert res['rc'] == 0

def test_state_absent_idempotent(self) -> None:
"""Test state=absent when key does not exist (no change)."""
module: MagicMock = MagicMock()
module.params = _module_params('nonexistent/key', state='absent')
module.check_mode = False
module.exit_json.side_effect = common.exit_json
module.fail_json.side_effect = common.fail_json

module.run_command.return_value = (0, '{}', '')

with pytest.raises(common.AnsibleExitJson) as result:
ceph_config_key.run(module)

res: Dict[str, Any] = result.value.args[0]
assert not res['changed']
assert res['stdout'] == ''
assert res['rc'] == 0
assert module.run_command.call_count == 1 # only dump, no del

def test_state_absent_check_mode(self) -> None:
"""Test state=absent in check mode (reports change, no del run)."""
module: MagicMock = MagicMock()
module.params = _module_params(
'config/mgr/mgr/prometheus/scrape_interval',
state='absent',
)
module.check_mode = True
module.exit_json.side_effect = common.exit_json
module.fail_json.side_effect = common.fail_json

module.run_command.return_value = (
0,
'{"config/mgr/mgr/prometheus/scrape_interval": "15"}',
'',
)

with pytest.raises(common.AnsibleExitJson) as result:
ceph_config_key.run(module)

res: Dict[str, Any] = result.value.args[0]
assert res['changed']
assert res['diff'] == {'before': '15', 'after': ''}
assert module.run_command.call_count == 1 # only dump
assert res['stdout'] == ''
assert res['rc'] == 0