]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph-ansible.git/commitdiff
add ceph_crush module
authorSébastien Han <seb@redhat.com>
Mon, 19 Feb 2018 09:13:06 +0000 (10:13 +0100)
committerGuillaume Abrioux <gabrioux@redhat.com>
Tue, 6 Mar 2018 15:24:31 +0000 (15:24 +0000)
This module allows us to create Ceph CRUSH hierarchy. The module works
with
hostvars from individual OSD hosts.
Here is an example of the expected configuration in the inventory file:

[osds]
ceph-osd-01 osd_crush_location="{ 'root': 'mon-roottt', 'rack':
'mon-rackkkk', 'pod': 'monpod', 'host': 'localhost' }"  # valid case

Then, if create_crush_tree is enabled the module will create the
appropriate CRUSH buckets and their types in Ceph.

Some pre-requesites:

* a 'host' bucket must be defined
* at least two buckets must be defined (this includes the 'host')

Signed-off-by: Sébastien Han <seb@redhat.com>
.gitignore
library/ceph_crush.py [new file with mode: 0644]
library/test_ceph_crush.py [new file with mode: 0644]

index 8373872df151e7018031df7e5dd1503718b91073..834fae1d6a7b389c1d8b0d9760371e8e709d3cc2 100644 (file)
@@ -18,3 +18,4 @@ group_vars/*.yml
 .tox
 ceph-ansible.spec
 *.retry
+*.pytest_cache
diff --git a/library/ceph_crush.py b/library/ceph_crush.py
new file mode 100644 (file)
index 0000000..1289e90
--- /dev/null
@@ -0,0 +1,212 @@
+#!/usr/bin/python
+
+#
+# Copyright (c) 2018 Red Hat, Inc.
+#
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {
+    'metadata_version': '1.1',
+    'status': ['preview'],
+    'supported_by': 'community'
+}
+
+DOCUMENTATION = '''
+---
+module: ceph_crush
+
+author: Sebastien Han <seb@redhat.com>
+
+short_description: Create Ceph CRUSH hierarchy
+
+version_added: "2.6"
+
+description:
+    - By using the hostvar variable 'osd_crush_location'
+    ceph_crush creates buckets and places them in the right CRUSH hierarchy
+
+options:
+    cluster:
+        description:
+            - The ceph cluster name.
+        required: false
+        default: ceph
+    location:
+        description:
+            - osd_crush_location dict from the inventory file. It contains
+            the placement of each host in the CRUSH map.
+        required: true
+    containerized:
+        description:
+            - Weither or not this is a containerized cluster. The value is
+            assigned or not depending on how the playbook runs.
+        required: false
+        default: None
+'''
+
+EXAMPLES = '''
+- name: configure crush hierarchy
+  ceph_crush:
+    cluster: "{{ cluster }}"
+    location: "{{ hostvars[item]['osd_crush_location'] }}"
+    containerized: "{{ docker_exec_cmd }}"
+  with_items: "{{ groups[osd_group_name] }}"
+  when:
+    - crush_rule_config
+'''
+
+RETURN = '''#  '''
+
+from ansible.module_utils.basic import AnsibleModule
+import datetime
+
+
+def fatal(message, module):
+    '''
+    Report a fatal error and exit
+    '''
+    if module:
+        module.fail_json(msg=message, rc=1)
+    else:
+        raise(Exception(message))
+
+
+def generate_cmd(cluster, subcommand, bucket, bucket_type, containerized=None):
+    '''
+    Generate command line to execute
+    '''
+    cmd = [
+        'ceph',
+        '--cluster',
+        cluster,
+        'osd',
+        'crush',
+        subcommand,
+        bucket,
+        bucket_type,
+    ]
+    if containerized:
+        cmd = containerized.split() + cmd
+    return cmd
+
+
+def sort_osd_crush_location(location, module):
+    '''
+    Sort location tuple
+    '''
+    if len(location) < 2:
+        fatal("You must specify at least 2 buckets.", module)
+
+    if not any(item for item in location if item[0] == "host"):
+        fatal("You must specify a 'host' bucket.", module)
+
+    try:
+        crush_bucket_types = [
+            "host",
+            "chassis",
+            "rack",
+            "row",
+            "pdu",
+            "pod",
+            "room",
+            "datacenter",
+            "region",
+            "root",
+        ]
+        return sorted(location, key=lambda crush: crush_bucket_types.index(crush[0]))
+    except ValueError as error:
+        fatal("{} is not a valid CRUSH bucket, valid bucket types are {}".format(error.args[0].split()[0], crush_bucket_types), module)
+
+
+def create_and_move_buckets_list(cluster, location, containerized=None):
+    '''
+    Creates Ceph CRUSH buckets and arrange the hierarchy
+    '''
+    previous_bucket = None
+    cmd_list = []
+    for item in location:
+        bucket_type, bucket_name = item
+        # ceph osd crush add-bucket maroot root
+        cmd_list.append(generate_cmd(cluster, "add-bucket", bucket_name, bucket_type, containerized))
+        if previous_bucket:
+            # ceph osd crush move monrack root=maroot
+            cmd_list.append(generate_cmd(cluster, "move", previous_bucket, "%s=%s" % (bucket_type, bucket_name), containerized))
+        previous_bucket = item[1]
+    return cmd_list
+
+
+def exec_commands(module, cmd_list):
+    '''
+    Creates Ceph commands
+    '''
+    for cmd in cmd_list:
+        rc, out, err = module.run_command(cmd, encoding=None)
+    return rc, cmd, out, err
+
+
+def run_module():
+    module_args = dict(
+        cluster=dict(type='str', required=False, default='ceph'),
+        location=dict(type='dict', required=True),
+        containerized=dict(type='str', required=True, default=None),
+    )
+
+    module = AnsibleModule(
+        argument_spec=module_args,
+        supports_check_mode=True
+    )
+
+    cluster = module.params['cluster']
+    location_dict = module.params['location']
+    location = sort_osd_crush_location(tuple(location_dict.items()), module)
+    containerized = module.params['containerized']
+
+    result = dict(
+        changed=False,
+        stdout='',
+        stderr='',
+        rc='',
+        start='',
+        end='',
+        delta='',
+    )
+
+    if module.check_mode:
+        return result
+
+    startd = datetime.datetime.now()
+
+    # run the Ceph command to add buckets
+    rc, cmd, out, err = exec_commands(module, create_and_move_buckets_list(cluster, location, containerized))
+
+    endd = datetime.datetime.now()
+    delta = endd - startd
+
+    result = dict(
+        cmd=cmd,
+        start=str(startd),
+        end=str(endd),
+        delta=str(delta),
+        rc=rc,
+        stdout=out.rstrip(b"\r\n"),
+        stderr=err.rstrip(b"\r\n"),
+        changed=True,
+    )
+
+    if rc != 0:
+        module.fail_json(msg='non-zero return code', **result)
+
+    module.exit_json(**result)
+
+
+def main():
+    run_module()
+
+
+if __name__ == '__main__':
+    main()
diff --git a/library/test_ceph_crush.py b/library/test_ceph_crush.py
new file mode 100644 (file)
index 0000000..7c46b4a
--- /dev/null
@@ -0,0 +1,90 @@
+from . import ceph_crush
+import pytest
+
+
+class TestCephCrushModule(object):
+
+    def test_no_host(self):
+        location = [
+            ("chassis", "monchassis"),
+            ("rack", "monrack"),
+            ("row", "marow"),
+            ("pdu", "monpdu"),
+            ("pod", "monpod"),
+            ("room", "maroom"),
+            ("datacenter", "mondc"),
+            ("region", "maregion"),
+            ("root", "maroute"),
+        ]
+        with pytest.raises(Exception):
+            result = ceph_crush.sort_osd_crush_location(location, None)
+
+    def test_lower_than_two_bucket(self):
+        location = [
+            ("chassis", "monchassis"),
+        ]
+        with pytest.raises(Exception):
+            result = ceph_crush.sort_osd_crush_location(location, None)
+
+    def test_invalid_bucket_type(self):
+        location = [
+            ("host", "monhost"),
+            ("chassis", "monchassis"),
+            ("rackyyyyy", "monrack"),
+        ]
+        with pytest.raises(Exception):
+            result = ceph_crush.sort_osd_crush_location(location, None)
+
+    def test_ordering(self):
+        expected_result = [
+            ("host", "monhost"),
+            ("chassis", "monchassis"),
+            ("rack", "monrack"),
+            ("row", "marow"),
+            ("pdu", "monpdu"),
+            ("pod", "monpod"),
+            ("room", "maroom"),
+            ("datacenter", "mondc"),
+            ("region", "maregion"),
+            ("root", "maroute"),
+        ]
+        expected_result_reverse = expected_result[::-1]
+        result = ceph_crush.sort_osd_crush_location(expected_result_reverse, None)
+        assert expected_result == result
+
+    def test_generate_commands(self):
+        cluster = "test"
+        expected_command_list = [
+            ['ceph', '--cluster', cluster, 'osd', 'crush', "add-bucket", "monhost", "host"],
+            ['ceph', '--cluster', cluster, 'osd', 'crush', "add-bucket", "monchassis", "chassis"],
+            ['ceph', '--cluster', cluster, 'osd', 'crush', "move", "monhost", "chassis=monchassis"],
+            ['ceph', '--cluster', cluster, 'osd', 'crush', "add-bucket", "monrack", "rack"],
+            ['ceph', '--cluster', cluster, 'osd', 'crush', "move", "monchassis", "rack=monrack"],
+        ]
+
+        location = [
+            ("host", "monhost"),
+            ("chassis", "monchassis"),
+            ("rack", "monrack"),
+        ]
+        result = ceph_crush.create_and_move_buckets_list(cluster, location)
+        assert result == expected_command_list
+
+    def test_generate_commands_container(self):
+        cluster = "test"
+        containerized = "docker exec -ti ceph-mon"
+        expected_command_list = [
+            ['docker', 'exec', '-ti', 'ceph-mon', 'ceph', '--cluster', cluster, 'osd', 'crush', "add-bucket", "monhost", "host"],
+            ['docker', 'exec', '-ti', 'ceph-mon', 'ceph', '--cluster', cluster, 'osd', 'crush', "add-bucket", "monchassis", "chassis"],
+            ['docker', 'exec', '-ti', 'ceph-mon', 'ceph', '--cluster', cluster, 'osd', 'crush', "move", "monhost", "chassis=monchassis"],
+            ['docker', 'exec', '-ti', 'ceph-mon', 'ceph', '--cluster', cluster, 'osd', 'crush', "add-bucket", "monrack", "rack"],
+            ['docker', 'exec', '-ti', 'ceph-mon', 'ceph', '--cluster', cluster, 'osd', 'crush', "move", "monchassis", "rack=monrack"],
+        ]
+
+        location = [
+            ("host", "monhost"),
+            ("chassis", "monchassis"),
+            ("rack", "monrack"),
+        ]
+        result = ceph_crush.create_and_move_buckets_list(cluster, location, containerized)
+        assert result == expected_command_list