]> git.apps.os.sepia.ceph.com Git - ceph-ansible.git/commitdiff
library: add ec_profile module
authorGuillaume Abrioux <gabrioux@redhat.com>
Thu, 5 Nov 2020 13:52:47 +0000 (14:52 +0100)
committerGuillaume Abrioux <gabrioux@redhat.com>
Tue, 24 Nov 2020 09:38:28 +0000 (10:38 +0100)
This commit adds a new module `ceph_ec_profile` to manage erasure code
profiles.

Signed-off-by: Guillaume Abrioux <gabrioux@redhat.com>
library/ceph_ec_profile.py [new file with mode: 0644]
tests/library/ca_test_common.py [new file with mode: 0644]
tests/library/test_ceph_ec_profile.py [new file with mode: 0644]

diff --git a/library/ceph_ec_profile.py b/library/ceph_ec_profile.py
new file mode 100644 (file)
index 0000000..cba4c40
--- /dev/null
@@ -0,0 +1,246 @@
+# Copyright 2020, Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+try:
+    from ansible.module_utils.ca_common import is_containerized, \
+                                               generate_ceph_cmd, \
+                                               exec_command, \
+                                               exit_module
+except ImportError:
+    from module_utils.ca_common import is_containerized, \
+                                            generate_ceph_cmd, \
+                                            exec_command, \
+                                            exit_module
+import datetime
+import json
+
+
+ANSIBLE_METADATA = {
+    'metadata_version': '1.1',
+    'status': ['preview'],
+    'supported_by': 'community'
+}
+
+DOCUMENTATION = '''
+---
+module: ceph_ec_profile
+
+short_description: Manage Ceph Erasure Code profile
+
+version_added: "2.8"
+
+description:
+    - Manage Ceph Erasure Code profile
+options:
+    cluster:
+        description:
+            - The ceph cluster name.
+        required: false
+        default: ceph
+    name:
+        description:
+            - name of the profile.
+        required: true
+    state:
+        description:
+            If 'present' is used, the module creates a profile.
+            If 'absent' is used, the module will delete the profile.
+        required: false
+        choices: ['present', 'absent', 'info']
+        default: present
+    stripe_unit:
+        description:
+            - The amount of data in a data chunk, per stripe.
+        required: false
+    k:
+        description:
+            - Number of data-chunks the object will be split in
+        required: true
+    m:
+        description:
+            - Compute coding chunks for each object and store them on different
+              OSDs.
+        required: true
+    crush_root:
+        description:
+            - The name of the crush bucket used for the first step of the CRUSH
+              rule.
+        required: false
+    crush_device_class:
+        description:
+            - Restrict placement to devices of a specific class (hdd/ssd)
+        required: false
+
+author:
+    - Guillaume Abrioux <gabrioux@redhat.com>
+'''
+
+EXAMPLES = '''
+- name: create an erasure code profile
+  ceph_ec_profile:
+    name: foo
+    k: 4
+    m: 2
+
+- name: delete an erassure code profile
+  ceph_ec_profile:
+    name: foo
+    state: absent
+'''
+
+RETURN = '''#  '''
+
+
+def get_profile(module, name, cluster='ceph', container_image=None):
+    '''
+    Get existing profile
+    '''
+
+    args = ['get', name, '--format=json']
+
+    cmd = generate_ceph_cmd(cluster=cluster,
+                            sub_cmd=['osd', 'erasure-code-profile'],
+                            args=args,
+                            container_image=container_image)
+
+    return cmd
+
+
+def create_profile(module, name, k, m, stripe_unit, cluster='ceph', force=False, container_image=None):
+    '''
+    Create a profile
+    '''
+
+    args = ['set', name, 'k={}'.format(k), 'm={}'.format(m)]
+    if stripe_unit:
+        args.append('stripe_unit={}'.format(stripe_unit))
+    if force:
+        args.append('--force')
+
+    cmd = generate_ceph_cmd(cluster=cluster,
+                            sub_cmd=['osd', 'erasure-code-profile'],
+                            args=args,
+                            container_image=container_image)
+
+    return cmd
+
+
+def delete_profile(module, name, cluster='ceph', container_image=None):
+    '''
+    Delete a profile
+    '''
+
+    args = ['rm', name]
+
+    cmd = generate_ceph_cmd(cluster=cluster,
+                            sub_cmd=['osd', 'erasure-code-profile'],
+                            args=args,
+                            container_image=container_image)
+
+    return cmd
+
+
+def run_module():
+    module_args = dict(
+        cluster=dict(type='str', required=False, default='ceph'),
+        name=dict(type='str', required=True),
+        state=dict(type='str', required=False,
+                   choices=['present', 'absent'], default='present'),
+        stripe_unit=dict(type='str', required=False),
+        k=dict(type='str', required=False),
+        m=dict(type='str', required=False),
+    )
+
+    module = AnsibleModule(
+        argument_spec=module_args,
+        supports_check_mode=True,
+        required_if=[['state', 'present', ['k', 'm']]],
+    )
+
+    # Gather module parameters in variables
+    name = module.params.get('name')
+    cluster = module.params.get('cluster')
+    state = module.params.get('state')
+    stripe_unit = module.params.get('stripe_unit')
+    k = module.params.get('k')
+    m = module.params.get('m')
+
+    if module.check_mode:
+        module.exit_json(
+            changed=False,
+            stdout='',
+            stderr='',
+            rc=0,
+            start='',
+            end='',
+            delta='',
+        )
+
+    startd = datetime.datetime.now()
+    changed = False
+
+    # will return either the image name or None
+    container_image = is_containerized()
+
+    if state == "present":
+        rc, cmd, out, err = exec_command(module, get_profile(module, name, cluster, container_image=container_image))
+        if rc == 0:
+            # the profile already exists, let's check whether we have to update it
+            current_profile = json.loads(out)
+            if current_profile['k'] != k or \
+               current_profile['m'] != m or \
+               current_profile.get('stripe_unit', stripe_unit) != stripe_unit:
+                rc, cmd, out, err = exec_command(module,
+                                                 create_profile(module,
+                                                                name,
+                                                                k,
+                                                                m,
+                                                                stripe_unit,
+                                                                cluster,
+                                                                force=True, container_image=container_image))
+                changed = True
+        else:
+            # the profile doesn't exist, it has to be created
+            rc, cmd, out, err = exec_command(module, create_profile(module,
+                                                                    name,
+                                                                    k,
+                                                                    m,
+                                                                    stripe_unit,
+                                                                    cluster,
+                                                                    container_image=container_image))
+            if rc == 0:
+                changed = True
+
+    elif state == "absent":
+        rc, cmd, out, err = exec_command(module, delete_profile(module, name, cluster, container_image=container_image))
+        if not err:
+            out = 'Profile {} removed.'.format(name)
+            changed = True
+        else:
+            rc = 0
+            out = "Skipping, the profile {} doesn't exist".format(name)
+
+    exit_module(module=module, out=out, rc=rc, cmd=cmd, err=err, startd=startd, changed=changed)
+
+
+def main():
+    run_module()
+
+
+if __name__ == '__main__':
+    main()
diff --git a/tests/library/ca_test_common.py b/tests/library/ca_test_common.py
new file mode 100644 (file)
index 0000000..eaa0bd6
--- /dev/null
@@ -0,0 +1,29 @@
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+import json
+
+
+def set_module_args(args):
+    if '_ansible_remote_tmp' not in args:
+        args['_ansible_remote_tmp'] = '/tmp'
+    if '_ansible_keep_remote_files' not in args:
+        args['_ansible_keep_remote_files'] = False
+
+    args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
+    basic._ANSIBLE_ARGS = to_bytes(args)
+
+
+class AnsibleExitJson(Exception):
+    pass
+
+
+class AnsibleFailJson(Exception):
+    pass
+
+
+def exit_json(*args, **kwargs):
+    raise AnsibleExitJson(kwargs)
+
+
+def fail_json(*args, **kwargs):
+    raise AnsibleFailJson(kwargs)
diff --git a/tests/library/test_ceph_ec_profile.py b/tests/library/test_ceph_ec_profile.py
new file mode 100644 (file)
index 0000000..00c6e13
--- /dev/null
@@ -0,0 +1,232 @@
+from mock.mock import MagicMock, patch
+import ca_test_common
+import ceph_ec_profile
+import pytest
+
+
+class TestCephEcProfile(object):
+    def setup_method(self):
+        self.fake_params = []
+        self.fake_binary = 'ceph'
+        self.fake_cluster = 'ceph'
+        self.fake_name = 'foo'
+        self.fake_k = 2
+        self.fake_m = 4
+        self.fake_module = MagicMock()
+        self.fake_module.params = self.fake_params
+
+    def test_get_profile(self):
+        expected_cmd = [
+            self.fake_binary,
+            '-n', 'client.admin',
+            '-k', '/etc/ceph/ceph.client.admin.keyring',
+            '--cluster', self.fake_cluster,
+            'osd', 'erasure-code-profile',
+            'get', self.fake_name,
+            '--format=json'
+        ]
+
+        assert ceph_ec_profile.get_profile(self.fake_module, self.fake_name) == expected_cmd
+
+    @pytest.mark.parametrize("stripe_unit,force", [(False, False),
+                                                   (32, True),
+                                                   (False, True),
+                                                   (32, False)])
+    def test_create_profile(self, stripe_unit, force):
+        expected_cmd = [
+            self.fake_binary,
+            '-n', 'client.admin',
+            '-k', '/etc/ceph/ceph.client.admin.keyring',
+            '--cluster', self.fake_cluster,
+            'osd', 'erasure-code-profile',
+            'set', self.fake_name,
+            'k={}'.format(self.fake_k), 'm={}'.format(self.fake_m),
+        ]
+        if stripe_unit:
+            expected_cmd.append('stripe_unit={}'.format(stripe_unit))
+        if force:
+            expected_cmd.append('--force')
+
+        assert ceph_ec_profile.create_profile(self.fake_module,
+                                              self.fake_name,
+                                              self.fake_k,
+                                              self.fake_m,
+                                              stripe_unit,
+                                              self.fake_cluster,
+                                              force) == expected_cmd
+
+    def test_delete_profile(self):
+        expected_cmd = [
+            self.fake_binary,
+            '-n', 'client.admin',
+            '-k', '/etc/ceph/ceph.client.admin.keyring',
+            '--cluster', self.fake_cluster,
+            'osd', 'erasure-code-profile',
+            'rm', self.fake_name
+            ]
+
+        assert ceph_ec_profile.delete_profile(self.fake_module,
+                                              self.fake_name,
+                                              self.fake_cluster) == expected_cmd
+
+    @patch('ansible.module_utils.basic.AnsibleModule.fail_json')
+    @patch('ansible.module_utils.basic.AnsibleModule.exit_json')
+    @patch('ceph_ec_profile.exec_command')
+    def test_state_present_nothing_to_update(self, m_exec_command, m_exit_json, m_fail_json):
+        ca_test_common.set_module_args({"state": "present",
+                                        "name": "foo",
+                                        "k": 2,
+                                        "m": 4,
+                                        "stripe_unit": 32,
+                                        })
+        m_exit_json.side_effect = ca_test_common.exit_json
+        m_fail_json.side_effect = ca_test_common.fail_json
+        m_exec_command.return_value = (0,
+                                       ['ceph', 'osd', 'erasure-code-profile', 'get', 'foo', '--format', 'json'],
+                                       '{"crush-device-class":"","crush-failure-domain":"host","crush-root":"default","jerasure-per-chunk-alignment":"false","k":"2","m":"4","plugin":"jerasure","stripe_unit":"32","technique":"reed_sol_van","w":"8"}',  # noqa: E501
+                                       '')
+
+        with pytest.raises(ca_test_common.AnsibleExitJson) as r:
+            ceph_ec_profile.run_module()
+
+        result = r.value.args[0]
+        assert not result['changed']
+        assert result['cmd'] == ['ceph', 'osd', 'erasure-code-profile', 'get', 'foo', '--format', 'json']
+        assert result['stdout'] == '{"crush-device-class":"","crush-failure-domain":"host","crush-root":"default","jerasure-per-chunk-alignment":"false","k":"2","m":"4","plugin":"jerasure","stripe_unit":"32","technique":"reed_sol_van","w":"8"}'  # noqa: E501
+        assert not result['stderr']
+        assert result['rc'] == 0
+
+    @patch('ansible.module_utils.basic.AnsibleModule.fail_json')
+    @patch('ansible.module_utils.basic.AnsibleModule.exit_json')
+    @patch('ceph_ec_profile.exec_command')
+    def test_state_present_profile_to_update(self, m_exec_command, m_exit_json, m_fail_json):
+        ca_test_common.set_module_args({"state": "present",
+                                        "name": "foo",
+                                        "k": 2,
+                                        "m": 6,
+                                        "stripe_unit": 32
+                                        })
+        m_exit_json.side_effect = ca_test_common.exit_json
+        m_fail_json.side_effect = ca_test_common.fail_json
+        m_exec_command.side_effect = [
+                                       (0,
+                                        ['ceph', 'osd', 'erasure-code-profile', 'get', 'foo', '--format', 'json'],
+                                        '{"crush-device-class":"","crush-failure-domain":"host","crush-root":"default","jerasure-per-chunk-alignment":"false","k":"2","m":"4","plugin":"jerasure","stripe_unit":"32","technique":"reed_sol_van","w":"8"}',  # noqa: E501
+                                        ''),
+                                       (0,
+                                        ['ceph', 'osd', 'erasure-code-profile', 'set', 'foo', 'k=2', 'm=6', 'stripe_unit=32', '--force'],
+                                        '',
+                                        ''
+                                        )
+                                    ]
+
+        with pytest.raises(ca_test_common.AnsibleExitJson) as r:
+            ceph_ec_profile.run_module()
+
+        result = r.value.args[0]
+        assert result['changed']
+        assert result['cmd'] == ['ceph', 'osd', 'erasure-code-profile', 'set', 'foo', 'k=2', 'm=6', 'stripe_unit=32', '--force']
+        assert not result['stdout']
+        assert not result['stderr']
+        assert result['rc'] == 0
+
+    @patch('ansible.module_utils.basic.AnsibleModule.fail_json')
+    @patch('ansible.module_utils.basic.AnsibleModule.exit_json')
+    @patch('ceph_ec_profile.exec_command')
+    def test_state_present_profile_doesnt_exist(self, m_exec_command, m_exit_json, m_fail_json):
+        ca_test_common.set_module_args({"state": "present",
+                                        "name": "foo",
+                                        "k": 2,
+                                        "m": 4,
+                                        "stripe_unit": 32
+                                        })
+        m_exit_json.side_effect = ca_test_common.exit_json
+        m_fail_json.side_effect = ca_test_common.fail_json
+        m_exec_command.side_effect = [
+                                       (2,
+                                        ['ceph', 'osd', 'erasure-code-profile', 'get', 'foo', '--format', 'json'],
+                                        '',
+                                        "Error ENOENT: unknown erasure code profile 'foo'"),
+                                       (0,
+                                        ['ceph', 'osd', 'erasure-code-profile', 'set', 'foo', 'k=2', 'm=4', 'stripe_unit=32', '--force'],
+                                        '',
+                                        ''
+                                        )
+                                    ]
+
+        with pytest.raises(ca_test_common.AnsibleExitJson) as r:
+            ceph_ec_profile.run_module()
+
+        result = r.value.args[0]
+        assert result['changed']
+        assert result['cmd'] == ['ceph', 'osd', 'erasure-code-profile', 'set', 'foo', 'k=2', 'm=4', 'stripe_unit=32', '--force']
+        assert not result['stdout']
+        assert not result['stderr']
+        assert result['rc'] == 0
+
+    @patch('ansible.module_utils.basic.AnsibleModule.fail_json')
+    @patch('ansible.module_utils.basic.AnsibleModule.exit_json')
+    @patch('ceph_ec_profile.exec_command')
+    def test_state_absent_on_existing_profile(self, m_exec_command, m_exit_json, m_fail_json):
+        ca_test_common.set_module_args({"state": "absent",
+                                        "name": "foo"
+                                        })
+        m_exit_json.side_effect = ca_test_common.exit_json
+        m_fail_json.side_effect = ca_test_common.fail_json
+        m_exec_command.return_value = (0,
+                                       ['ceph', 'osd', 'erasure-code-profile', 'rm', 'foo'],
+                                       '',
+                                       '')
+
+        with pytest.raises(ca_test_common.AnsibleExitJson) as r:
+            ceph_ec_profile.run_module()
+
+        result = r.value.args[0]
+        assert result['changed']
+        assert result['cmd'] == ['ceph', 'osd', 'erasure-code-profile', 'rm', 'foo']
+        assert result['stdout'] == 'Profile foo removed.'
+        assert not result['stderr']
+        assert result['rc'] == 0
+
+    @patch('ansible.module_utils.basic.AnsibleModule.fail_json')
+    @patch('ansible.module_utils.basic.AnsibleModule.exit_json')
+    @patch('ceph_ec_profile.exec_command')
+    def test_state_absent_on_nonexisting_profile(self, m_exec_command, m_exit_json, m_fail_json):
+        ca_test_common.set_module_args({"state": "absent",
+                                        "name": "foo"
+                                        })
+        m_exit_json.side_effect = ca_test_common.exit_json
+        m_fail_json.side_effect = ca_test_common.fail_json
+        m_exec_command.return_value = (0,
+                                       ['ceph', 'osd', 'erasure-code-profile', 'rm', 'foo'],
+                                       '',
+                                       'erasure-code-profile foo does not exist')
+
+        with pytest.raises(ca_test_common.AnsibleExitJson) as r:
+            ceph_ec_profile.run_module()
+
+        result = r.value.args[0]
+        assert not result['changed']
+        assert result['cmd'] == ['ceph', 'osd', 'erasure-code-profile', 'rm', 'foo']
+        assert result['stdout'] == "Skipping, the profile foo doesn't exist"
+        assert result['stderr'] == 'erasure-code-profile foo does not exist'
+        assert result['rc'] == 0
+
+    @patch('ansible.module_utils.basic.AnsibleModule.exit_json')
+    def test_check_mode(self, m_exit_json):
+        ca_test_common.set_module_args({
+            'name': 'foo',
+            'k': 2,
+            'm': 4,
+            '_ansible_check_mode': True
+        })
+        m_exit_json.side_effect = ca_test_common.exit_json
+
+        with pytest.raises(ca_test_common.AnsibleExitJson) as result:
+            ceph_ec_profile.run_module()
+
+        result = result.value.args[0]
+        assert not result['changed']
+        assert result['rc'] == 0
+        assert not result['stdout']
+        assert not result['stderr']