library: add ceph_fs module

This adds the ceph_fs ansible module for replacing the command module
usage with the ceph fs command.

Signed-off-by: Dimitri Savineau <dsavinea@redhat.com>
pull/5783/head
Dimitri Savineau 2020-09-30 11:57:20 -04:00 committed by Guillaume Abrioux
parent 00b7ee27df
commit bd611a785b
4 changed files with 508 additions and 24 deletions

View File

@ -469,8 +469,15 @@
when: groups.get(mds_group_name, []) | length > 1
block:
- name: set max_mds 1 on ceph fs
command: "{{ container_exec_cmd | default('') }} ceph --cluster {{ cluster }} fs set {{ cephfs }} max_mds 1"
changed_when: false
ceph_fs:
name: "{{ cephfs }}"
cluster: "{{ cluster }}"
data: "{{ cephfs_data_pool.name }}"
metadata: "{{ cephfs_metadata_pool.name }}"
max_mds: 1
environment:
CEPH_CONTAINER_IMAGE: "{{ ceph_docker_registry + '/' + ceph_docker_image + ':' + ceph_docker_image_tag if containerized_deployment | bool else None }}"
CEPH_CONTAINER_BINARY: "{{ container_binary }}"
- name: wait until only rank 0 is up
command: "{{ container_exec_cmd | default('') }} ceph --cluster {{ cluster }} fs get {{ cephfs }} -f json"
@ -618,9 +625,16 @@
name: ceph-mds
- name: set max_mds
command: "{{ container_exec_cmd | default('') }} ceph --cluster {{ cluster }} fs set {{ cephfs }} max_mds {{ mds_max_mds }}"
changed_when: false
ceph_fs:
name: "{{ cephfs }}"
cluster: "{{ cluster }}"
max_mds: "{{ mds_max_mds }}"
data: "{{ cephfs_data_pool.name }}"
metadata: "{{ cephfs_metadata_pool.name }}"
delegate_to: "{{ groups[mon_group_name][0] }}"
environment:
CEPH_CONTAINER_IMAGE: "{{ ceph_docker_registry + '/' + ceph_docker_image + ':' + ceph_docker_image_tag if containerized_deployment | bool else None }}"
CEPH_CONTAINER_BINARY: "{{ container_binary }}"
when: inventory_hostname == groups['standby_mdss'] | last

342
library/ceph_fs.py 100644
View File

@ -0,0 +1,342 @@
# 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
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: ceph_fs
short_description: Manage Ceph File System
version_added: "2.8"
description:
- Manage Ceph File System(s) creation, deletion and updates.
options:
cluster:
description:
- The ceph cluster name.
required: false
default: ceph
name:
description:
- name of the Ceph File System.
required: true
state:
description:
If 'present' is used, the module creates a filesystem if it
doesn't exist or update it if it already exists.
If 'absent' is used, the module will simply delete the filesystem.
If 'info' is used, the module will return all details about the
existing filesystem (json formatted).
required: false
choices: ['present', 'absent', 'info']
default: present
data:
description:
- name of the data pool.
required: false
metadata:
description:
- name of the metadata pool.
required: false
max_mds:
description:
- name of the max_mds attribute.
required: false
author:
- Dimitri Savineau <dsavinea@redhat.com>
'''
EXAMPLES = '''
- name: create a Ceph File System
ceph_fs:
name: foo
data: bar_data
metadata: bar_metadata
max_mds: 2
- name: get a Ceph File System information
ceph_fs:
name: foo
state: info
- name: delete a Ceph File System
ceph_fs:
name: foo
state: absent
'''
RETURN = '''# '''
from ansible.module_utils.basic import AnsibleModule # noqa E402
import datetime # noqa E402
import json # noqa E402
import os # noqa E402
import stat # noqa E402
import time # noqa E402
def container_exec(binary, container_image):
'''
Build the docker CLI to run a command inside a container
'''
container_binary = os.getenv('CEPH_CONTAINER_BINARY')
command_exec = [container_binary,
'run',
'--rm',
'--net=host',
'-v', '/etc/ceph:/etc/ceph:z',
'-v', '/var/lib/ceph/:/var/lib/ceph/:z',
'-v', '/var/log/ceph/:/var/log/ceph/:z',
'--entrypoint=' + binary, container_image]
return command_exec
def is_containerized():
'''
Check if we are running on a containerized cluster
'''
if 'CEPH_CONTAINER_IMAGE' in os.environ:
container_image = os.getenv('CEPH_CONTAINER_IMAGE')
else:
container_image = None
return container_image
def pre_generate_ceph_cmd(container_image=None):
'''
Generate ceph prefix comaand
'''
if container_image:
cmd = container_exec('ceph', container_image)
else:
cmd = ['ceph']
return cmd
def generate_ceph_cmd(cluster, args, container_image=None):
'''
Generate 'ceph' command line to execute
'''
cmd = pre_generate_ceph_cmd(container_image=container_image)
base_cmd = [
'--cluster',
cluster,
'fs'
]
cmd.extend(base_cmd + args)
return cmd
def exec_commands(module, cmd):
'''
Execute command(s)
'''
rc, out, err = module.run_command(cmd)
return rc, cmd, out, err
def create_fs(module, container_image=None):
'''
Create a new fs
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
data = module.params.get('data')
metadata = module.params.get('metadata')
args = ['new', name, metadata, data]
cmd = generate_ceph_cmd(cluster=cluster, args=args, container_image=container_image)
return cmd
def get_fs(module, container_image=None):
'''
Get existing fs
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
args = ['get', name, '--format=json']
cmd = generate_ceph_cmd(cluster=cluster, args=args, container_image=container_image)
return cmd
def remove_fs(module, container_image=None):
'''
Remove a fs
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
args = ['rm', name, '--yes-i-really-mean-it']
cmd = generate_ceph_cmd(cluster=cluster, args=args, container_image=container_image)
return cmd
def fail_fs(module, container_image=None):
'''
Fail a fs
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
args = ['fail', name]
cmd = generate_ceph_cmd(cluster=cluster, args=args, container_image=container_image)
return cmd
def set_fs(module, container_image=None):
'''
Set parameter to a fs
'''
cluster = module.params.get('cluster')
name = module.params.get('name')
max_mds = module.params.get('max_mds')
args = ['set', name, 'max_mds', str(max_mds)]
cmd = generate_ceph_cmd(cluster=cluster, args=args, container_image=container_image)
return cmd
def exit_module(module, out, rc, cmd, err, startd, changed=False):
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("\r\n"),
stderr=err.rstrip("\r\n"),
changed=changed,
)
module.exit_json(**result)
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', 'info'], default='present'),
data=dict(type='str', required=False),
metadata=dict(type='str', required=False),
max_mds=dict(type='int', required=False),
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True,
required_if=[['state', 'present', ['data', 'metadata']]],
)
# Gather module parameters in variables
name = module.params.get('name')
state = module.params.get('state')
max_mds = module.params.get('max_mds')
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_commands(module, get_fs(module, container_image=container_image))
if rc == 0:
fs = json.loads(out)
if max_mds and fs["mdsmap"]["max_mds"] != max_mds:
rc, cmd, out, err = exec_commands(module, set_fs(module, container_image=container_image))
if rc == 0:
changed = True
else:
rc, cmd, out, err = exec_commands(module, create_fs(module, container_image=container_image))
if max_mds and max_mds > 1:
exec_commands(module, set_fs(module, container_image=container_image))
if rc == 0:
changed = True
elif state == "absent":
rc, cmd, out, err = exec_commands(module, get_fs(module, container_image=container_image))
if rc == 0:
exec_commands(module, fail_fs(module, container_image=container_image))
rc, cmd, out, err = exec_commands(module, remove_fs(module, container_image=container_image))
if rc == 0:
changed = True
else:
rc = 0
out = "Ceph File System {} doesn't exist".format(name)
elif state == "info":
rc, cmd, out, err = exec_commands(module, get_fs(module, container_image=container_image))
exit_module(module=module, out=out, rc=rc, cmd=cmd, err=err, startd=startd, changed=changed)
def main():
run_module()
if __name__ == '__main__':
main()

View File

@ -22,24 +22,14 @@
CEPH_CONTAINER_IMAGE: "{{ ceph_docker_registry + '/' + ceph_docker_image + ':' + ceph_docker_image_tag if containerized_deployment | bool else None }}"
CEPH_CONTAINER_BINARY: "{{ container_binary }}"
- name: check and create ceph filesystem
- name: create ceph filesystem
ceph_fs:
name: "{{ cephfs }}"
cluster: "{{ cluster }}"
data: "{{ cephfs_data_pool.name }}"
metadata: "{{ cephfs_metadata_pool.name }}"
max_mds: "{{ mds_max_mds if not rolling_update | bool else omit }}"
delegate_to: "{{ groups[mon_group_name][0] }}"
block:
- name: check if ceph filesystem already exists
command: "{{ ceph_run_cmd }} --cluster {{ cluster }} fs get {{ cephfs }}"
register: check_existing_cephfs
changed_when: false
failed_when: false
- name: create ceph filesystem
command: "{{ ceph_run_cmd }} --cluster {{ cluster }} fs new {{ cephfs }} {{ cephfs_metadata_pool.name }} {{ cephfs_data_pool.name }}"
changed_when: false
when: check_existing_cephfs.rc != 0
- name: set max_mds
command: "{{ ceph_run_cmd }} --cluster {{ cluster }} fs set {{ cephfs }} max_mds {{ mds_max_mds }}"
changed_when: false
delegate_to: "{{ groups[mon_group_name][0] }}"
when:
- mds_max_mds > 1
- not rolling_update | bool
environment:
CEPH_CONTAINER_IMAGE: "{{ ceph_docker_registry + '/' + ceph_docker_image + ':' + ceph_docker_image_tag if containerized_deployment | bool else None }}"
CEPH_CONTAINER_BINARY: "{{ container_binary }}"

View File

@ -0,0 +1,138 @@
import os
import sys
from mock.mock import patch, MagicMock
import pytest
sys.path.append('./library')
import ceph_fs # noqa : E402
fake_binary = 'ceph'
fake_cluster = 'ceph'
fake_container_binary = 'podman'
fake_container_image = 'docker.io/ceph/daemon:latest'
fake_container_cmd = [
fake_container_binary,
'run',
'--rm',
'--net=host',
'-v', '/etc/ceph:/etc/ceph:z',
'-v', '/var/lib/ceph/:/var/lib/ceph/:z',
'-v', '/var/log/ceph/:/var/log/ceph/:z',
'--entrypoint=' + fake_binary,
fake_container_image
]
fake_fs = 'foo'
fake_data_pool = 'bar_data'
fake_metadata_pool = 'bar_metadata'
fake_max_mds = 2
fake_params = {'cluster': fake_cluster,
'name': fake_fs,
'data': fake_data_pool,
'metadata': fake_metadata_pool,
'max_mds': fake_max_mds}
class TestCephFsModule(object):
@patch.dict(os.environ, {'CEPH_CONTAINER_BINARY': fake_container_binary})
def test_container_exec(self):
cmd = ceph_fs.container_exec(fake_binary, fake_container_image)
assert cmd == fake_container_cmd
def test_not_is_containerized(self):
assert ceph_fs.is_containerized() is None
@patch.dict(os.environ, {'CEPH_CONTAINER_IMAGE': fake_container_image})
def test_is_containerized(self):
assert ceph_fs.is_containerized() == fake_container_image
@pytest.mark.parametrize('image', [None, fake_container_image])
@patch.dict(os.environ, {'CEPH_CONTAINER_BINARY': fake_container_binary})
def test_pre_generate_ceph_cmd(self, image):
if image:
expected_cmd = fake_container_cmd
else:
expected_cmd = [fake_binary]
assert ceph_fs.pre_generate_ceph_cmd(image) == expected_cmd
@pytest.mark.parametrize('image', [None, fake_container_image])
@patch.dict(os.environ, {'CEPH_CONTAINER_BINARY': fake_container_binary})
def test_generate_ceph_cmd(self, image):
if image:
expected_cmd = fake_container_cmd
else:
expected_cmd = [fake_binary]
expected_cmd.extend([
'--cluster',
fake_cluster,
'fs'
])
assert ceph_fs.generate_ceph_cmd(fake_cluster, [], image) == expected_cmd
def test_create_fs(self):
fake_module = MagicMock()
fake_module.params = fake_params
expected_cmd = [
fake_binary,
'--cluster', fake_cluster,
'fs', 'new',
fake_fs,
fake_metadata_pool,
fake_data_pool
]
assert ceph_fs.create_fs(fake_module) == expected_cmd
def test_set_fs(self):
fake_module = MagicMock()
fake_module.params = fake_params
expected_cmd = [
fake_binary,
'--cluster', fake_cluster,
'fs', 'set',
fake_fs,
'max_mds',
str(fake_max_mds)
]
assert ceph_fs.set_fs(fake_module) == expected_cmd
def test_get_fs(self):
fake_module = MagicMock()
fake_module.params = fake_params
expected_cmd = [
fake_binary,
'--cluster', fake_cluster,
'fs', 'get',
fake_fs,
'--format=json'
]
assert ceph_fs.get_fs(fake_module) == expected_cmd
def test_remove_fs(self):
fake_module = MagicMock()
fake_module.params = fake_params
expected_cmd = [
fake_binary,
'--cluster', fake_cluster,
'fs', 'rm',
fake_fs,
'--yes-i-really-mean-it'
]
assert ceph_fs.remove_fs(fake_module) == expected_cmd
def test_fail_fs(self):
fake_module = MagicMock()
fake_module.params = fake_params
expected_cmd = [
fake_binary,
'--cluster', fake_cluster,
'fs', 'fail',
fake_fs
]
assert ceph_fs.fail_fs(fake_module) == expected_cmd