mirror of https://github.com/ceph/ceph-ansible.git
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
parent
78c1f1938f
commit
5fac3784f7
|
@ -18,3 +18,4 @@ group_vars/*.yml
|
|||
.tox
|
||||
ceph-ansible.spec
|
||||
*.retry
|
||||
*.pytest_cache
|
||||
|
|
|
@ -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()
|
|
@ -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
|
Loading…
Reference in New Issue