add ceph_crush module

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>
pull/2436/head
Sébastien Han 2018-02-19 10:13:06 +01:00 committed by Guillaume Abrioux
parent 78c1f1938f
commit 5fac3784f7
3 changed files with 303 additions and 0 deletions

1
.gitignore vendored
View File

@ -18,3 +18,4 @@ group_vars/*.yml
.tox
ceph-ansible.spec
*.retry
*.pytest_cache

View File

@ -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()

View File

@ -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