]> git.apps.os.sepia.ceph.com Git - ceph-ansible.git/commitdiff
library: add ceph_pool module
authorGuillaume Abrioux <gabrioux@redhat.com>
Sat, 4 Apr 2020 01:44:05 +0000 (03:44 +0200)
committerGuillaume Abrioux <gabrioux@redhat.com>
Fri, 22 May 2020 15:05:22 +0000 (17:05 +0200)
This commit adds a new module `ceph_pool`

Signed-off-by: Guillaume Abrioux <gabrioux@redhat.com>
(cherry picked from commit bddcb439ce1b46735946e9fd5d147bc6604bcda3)

library/ceph_pool.py [new file with mode: 0644]

diff --git a/library/ceph_pool.py b/library/ceph_pool.py
new file mode 100644 (file)
index 0000000..c97d371
--- /dev/null
@@ -0,0 +1,631 @@
+#!/usr/bin/python3
+# 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_pool
+
+author: Guillaume Abrioux <gabrioux@redhat.com>
+
+short_description: Manage Ceph Pools
+
+version_added: "2.8"
+
+description:
+    - Manage Ceph pool(s) creation, deletion and updates.
+options:
+    cluster:
+        description:
+            - The ceph cluster name.
+        required: false
+        default: ceph
+    name:
+        description:
+            - name of the Ceph pool
+        required: true
+    state:
+        description:
+            If 'present' is used, the module creates a pool if it doesn't exist or
+            update it if it already exists.
+            If 'absent' is used, the module will simply delete the pool.
+            If 'list' is used, the module will return all details about the existing pools
+            (json formatted).
+        required: true
+        choices: ['present', 'absent', 'list']
+        default: list
+    size:
+        description:
+            - set the replica size of the pool.
+        required: false
+        default: 3
+    min_size:
+        description:
+            - set the min_size parameter of the pool.
+        required: false
+        default: default to `osd_pool_default_min_size` (ceph)
+    pg_num:
+        description:
+            - set the pg_num of the pool.
+        required: false
+        default: default to `osd_pool_default_pg_num` (ceph)
+    pgp_num:
+        description:
+            - set the pgp_num of the pool.
+        required: false
+        default: default to `osd_pool_default_pgp_num` (ceph)
+    pg_autoscale_mode:
+        description:
+            - set the pg autoscaler on the pool.
+        required: false
+        default: 'on'
+    target_size_ratio:
+        description:
+            - set the target_size_ratio on the pool
+        required: false
+        default: None
+    pool_type:
+        description:
+            - set the pool type, either 'replicated' or 'erasure'
+        required: false
+        default: 'replicated'
+    erasure_profile:
+        description:
+            - When pool_type = 'erasure', set the erasure profile of the pool
+        required: false
+        default: 'default'
+    rule_name:
+        description:
+            - Set the crush rule name assigned to the pool
+        required: false
+        default: 'replicated_rule' when pool_type is 'erasure' else None
+    expected_num_objects:
+        description:
+            -   Set the expected_num_objects parameter of the pool.
+        required: false
+        default: '0'
+    application:
+        description:
+            - Set the pool application on the pool.
+        required: false
+        default: None
+'''
+
+EXAMPLES = '''
+
+pools:
+  - { name: foo, size: 3, application: rbd, pool_type: 'replicated', pg_autoscale_mode: 'on' }
+
+- hosts: all
+  become: true
+  tasks:
+    - name: create a pool
+      ceph_pool:
+        name: "{{ item.name }}"
+        state: present
+        size: "{{ item.size }}"
+        application: "{{ item.application }}"
+        pool_type: "{{ item.pool_type }}"
+        pg_autoscale_mode: "{{ item.pg_autoscale_mode }}"
+      with_items: "{{ pools }}"
+'''
+
+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):
+    if container_image:
+        binary = 'ceph'
+        cmd = container_exec(
+            binary, container_image)
+    else:
+        binary = ['ceph']
+        cmd = binary
+
+    return cmd
+
+def generate_ceph_cmd(cluster, args, user, user_key, container_image=None):
+    '''
+    Generate 'ceph' command line to execute
+    '''
+
+    cmd = pre_generate_ceph_cmd(container_image=container_image)
+
+    base_cmd = [
+        '-n',
+        user,
+        '-k',
+        user_key,
+        '--cluster',
+        cluster,
+        'osd',
+        'pool'
+    ]
+
+    cmd.extend(base_cmd + args)
+
+    return cmd
+
+
+def exec_commands(module, cmd_list):
+    '''
+    Execute command(s)
+    '''
+
+    for cmd in cmd_list:
+        rc, out, err = module.run_command(cmd)
+        if rc != 0:
+            return rc, cmd, out, err
+
+    return rc, cmd, out, err
+
+def check_pool_exist(cluster, name, user, user_key, output_format='json', container_image=None):
+    '''
+    Check if a given pool exists
+    '''
+
+    cmd_list = []
+
+    args = [ 'stats', name, '-f', output_format ]
+
+    cmd_list.append(generate_ceph_cmd(cluster=cluster, args=args, user=user, user_key=user_key, container_image=container_image))
+
+    return cmd_list
+
+
+def get_default_running_config(module, cluster, user, user_key, output_format='json', container_image=None):
+    '''
+    Get some default values set in the cluster
+    '''
+
+    params = ['osd_pool_default_size', 'osd_pool_default_min_size', 'osd_pool_default_pg_num', 'osd_pool_default_pgp_num']
+
+    default_running_values = {}
+
+    for param in params:
+        cmd_list = []
+        _cmd = pre_generate_ceph_cmd(container_image=container_image)
+        args = [
+            '-n',
+            user,
+            '-k',
+            user_key,
+            '--cluster',
+            cluster,
+            'config',
+            'get',
+            'mon.*',
+            param
+        ]
+
+        cmd_list.append(_cmd + args)
+
+        rc, cmd, out, err = exec_commands(module, cmd_list)
+
+        if rc == 0:
+            default_running_values[param] = out.strip()
+        else:
+            return rc, cmd, out, err
+
+    return rc, cmd, default_running_values, err
+
+
+def get_application_pool(module, cluster, name, user, user_key, output_format='json', container_image=None):
+    '''
+    Get application type enabled on a given pool
+    '''
+
+    cmd_list = []
+
+    args = [ 'application', 'get', name, '-f', output_format ]
+
+    cmd_list.append(generate_ceph_cmd(cluster=cluster, args=args, user=user, user_key=user_key, container_image=container_image))
+
+    rc, cmd, out, err = exec_commands(module, cmd_list)
+
+    return rc, cmd, list(json.loads(out.strip()).keys()), err
+
+
+def enable_application_pool(module, cluster, name, application, user, user_key, container_image=None):
+    '''
+    Enable application on a given pool
+    '''
+
+    cmd_list = []
+
+    args = [ 'application', 'enable', name, application ]
+
+    cmd_list.append(generate_ceph_cmd(cluster=cluster, args=args, user=user, user_key=user_key, container_image=container_image))
+
+    rc, cmd, out, err = exec_commands(module, cmd_list)
+
+    return rc, cmd, out, err
+
+
+def disable_application_pool(module, cluster, name, application, user, user_key, container_image=None):
+    '''
+    Disable application on a given pool
+    '''
+
+    cmd_list = []
+
+    args = [ 'application', 'disable', name, application, '--yes-i-really-mean-it' ]
+
+    cmd_list.append(generate_ceph_cmd(cluster=cluster, args=args, user=user, user_key=user_key, container_image=container_image))
+
+    rc, cmd, out, err = exec_commands(module, cmd_list)
+
+    return rc, cmd, out, err
+
+
+def get_pool_details(module, cluster, name, user, user_key, output_format='json', container_image=None):
+    '''
+    Get details about a given pool
+    '''
+
+    cmd_list = []
+
+    args = [ 'ls', 'detail', '-f', output_format ]
+
+    cmd_list.append(generate_ceph_cmd(cluster=cluster, args=args, user=user, user_key=user_key, container_image=container_image))
+
+    rc, cmd, out, err = exec_commands(module, cmd_list)
+
+    if rc == 0:
+        out = [p for p in json.loads(out.strip()) if p['pool_name'] == name][0]
+
+    _rc, _cmd, application_pool, _err = get_application_pool(module, cluster, name, user, user_key, container_image=container_image)
+
+    if len(application_pool) == 0:
+        out['application'] = ''
+    else:
+        out['application'] = application_pool
+
+    return rc, cmd, out, err
+
+
+def compare_pool_config(user_pool_config, running_pool_details):
+    '''
+    Compare user input config pool details with current running pool details
+    '''
+
+    delta = {}
+    filter_keys = [ 'pg_num', 'pg_placement_num', 'size', 'pg_autoscale_mode']
+    for key in filter_keys:
+        if str(running_pool_details[key]) != user_pool_config[key]['value']:
+            delta[key] = user_pool_config[key]
+
+    if str(running_pool_details['options'].get('target_size_ratio')) != user_pool_config['target_size_ratio']['value'] and user_pool_config['target_size_ratio']['value'] != None:
+        delta['target_size_ratio'] = user_pool_config['target_size_ratio']
+
+    if running_pool_details['application'] != user_pool_config['application']['value'] and user_pool_config['application']['value'] != None:
+        delta['application'] = {}
+        delta['application']['new_application'] = user_pool_config['application']['value']
+        # to be improved (for update_pools()...)
+        delta['application']['value'] = delta['application']['new_application']
+        delta['application']['old_application'] = running_pool_details['application']
+
+    return delta
+
+
+def list_pools(cluster, user, user_key, details, output_format='json', container_image=None):
+    '''
+    List existing pools
+    '''
+
+    cmd_list = []
+
+    args = [ 'ls' ]
+
+    if details:
+        args.append('detail')
+
+    args.extend([ '-f', output_format ])
+
+    cmd_list.append(generate_ceph_cmd(cluster=cluster, args=args, user=user, user_key=user_key, container_image=container_image))
+
+    return cmd_list
+
+
+def create_pool(cluster, name, user, user_key, user_pool_config, container_image=None):
+    '''
+    Create a new pool
+    '''
+
+    cmd_list = []
+
+    args = [ 'create', user_pool_config['pool_name']['value'], '--pg_num', user_pool_config['pg_num']['value'], '--pgp_num', user_pool_config['pgp_num']['value'], user_pool_config['type']['value'] ]
+
+    if user_pool_config['type']['value'] == 'replicated':
+        args.extend([ user_pool_config['crush_rule']['value'], '--expected_num_objects', user_pool_config['expected_num_objects']['value'], '--size', user_pool_config['size']['value'], '--autoscale-mode', user_pool_config['pg_autoscale_mode']['value'] ])
+
+    elif user_pool_config['type']['value'] == 'erasure':
+        args.extend([ user_pool_config['erasure_profile']['value'] ])
+
+        if user_pool_config['crush_rule']['value'] != None:
+            args.extend([ user_pool_config['crush_rule']['value'] ])
+
+        args.extend([ '--expected_num_objects', user_pool_config['expected_num_objects']['value'] , '--autoscale-mode', user_pool_config['pg_autoscale_mode']['value']])
+
+    cmd_list.append(generate_ceph_cmd(cluster=cluster, args=args, user=user, user_key=user_key, container_image=container_image))
+
+    return cmd_list
+
+
+def remove_pool(cluster, name, user, user_key, container_image=None):
+    '''
+    Remove a pool
+    '''
+
+    cmd_list = []
+
+    args = [ 'rm', name, name, '--yes-i-really-really-mean-it']
+
+    cmd_list.append(generate_ceph_cmd(cluster=cluster, args=args, user=user, user_key=user_key, container_image=container_image))
+
+    return cmd_list
+
+def update_pool(module, cluster, name, user, user_key, delta, container_image=None):
+    '''
+    Update an existing pool
+    '''
+
+    report = ""
+
+    for key in delta.keys():
+        cmd_list = []
+
+        if key != 'application':
+            args = [ 'set', name, delta[key]['cli_set_opt'], delta[key]['value'] ]
+
+            cmd_list.append(generate_ceph_cmd(cluster=cluster, args=args, user=user, user_key=user_key, container_image=container_image))
+
+            rc, cmd, out, err = exec_commands(module, cmd_list)
+            if rc != 0:
+                return rc, cmd, out, err
+
+        else:
+            rc, cmd, out, err = disable_application_pool(module, cluster, name, delta['application']['old_application'], user, user_key, container_image=container_image)
+            if rc != 0:
+                return rc, cmd, out, err
+
+            rc, cmd, out, err = enable_application_pool(module, cluster, name, delta['application']['new_application'], user, user_key, container_image=container_image)
+            if rc != 0:
+                return rc, cmd, out, err
+
+        report = report + "\n" + "{} has been updated: {} is now {}".format(name, key, delta[key]['value'])
+
+    out = report
+    return rc, cmd, out, err
+
+
+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=False),
+        state=dict(type='str', required=True, choices=['present', 'absent', 'list']),
+        details=dict(type='bool', required=False, default=False),
+        size=dict(type='str', required=False),
+        min_size=dict(type='str', required=False),
+        pg_num=dict(type='str', required=False, default=None),
+        pgp_num=dict(type='str', required=False, default=None),
+        pg_autoscale_mode=dict(type='str', required=False, default='on'),
+        target_size_ratio=dict(type='str', required=False, default=None),
+        pool_type=dict(type='str', required=False, default='replicated', choices=['replicated', 'erasure', '1', '3']),
+        erasure_profile=dict(type='str', required=False, default='default'),
+        rule_name=dict(type='str', required=False, default=None),
+        expected_num_objects=dict(type='str', required=False, default="0"),
+        application=dict(type='str', required=False, default=None),
+    )
+
+    module = AnsibleModule(
+        argument_spec=module_args,
+        supports_check_mode=True,
+        add_file_common_args=True,
+    )
+
+    # Gather module parameters in variables
+    cluster = module.params.get('cluster')
+    name = module.params.get('name')
+    state = module.params.get('state')
+    details = module.params.get('details')
+    pg_num = module.params.get('pg')
+    pgp_num = module.params.get('pgp')
+    pg_autoscale_mode = module.params.get('pg_autoscale_mode')
+    target_size_ratio = module.params.get('target_size_ratio')
+    application = module.params.get('application')
+
+    if module.params.get('pg_autoscale_mode').lower() in ['true', 'on', 'yes']:
+        pg_autoscale_mode = 'on'
+    elif module.params.get('pg_autoscale_mode').lower() in ['false', 'off', 'no']:
+        pg_autoscale_mode = 'off'
+    else:
+        pg_autoscale_mode = 'warn'
+
+    if module.params.get('pool_type') == '1':
+        pool_type = 'replicated'
+    elif module.params.get('pool_type') == '3':
+        pool_type = 'erasure'
+    else:
+        pool_type = module.params.get('pool_type')
+
+    if module.params.get('rule_name') == None:
+        rule_name = 'replicated_rule' if pool_type == 'replicated' else None
+    else:
+        rule_name = module.params.get('rule_name')
+
+    erasure_profile = module.params.get('erasure_profile')
+    expected_num_objects = module.params.get('expected_num_objects')
+
+
+
+    user_pool_config = {
+        'pool_name': { 'value': name },
+        'pg_num': { 'value': pg_num, 'cli_set_opt': 'pg_num' },
+        'pgp_num': { 'value': pgp_num, 'cli_set_opt': 'pgp_num' },
+        'pg_autoscale_mode': { 'value': pg_autoscale_mode, 'cli_set_opt': 'pg_autoscale_mode' },
+        'target_size_ratio': { 'value': target_size_ratio, 'cli_set_opt': 'target_size_ratio' },
+        'application': {'value': application },
+        'type': { 'value': pool_type },
+        'erasure_profile': { 'value': erasure_profile },
+        'crush_rule': { 'value': rule_name, 'cli_set_opt': 'crush_rule' },
+        'expected_num_objects': { 'value': expected_num_objects }
+    }
+
+    if module.check_mode:
+        return dict(
+            changed=False,
+            stdout='',
+            stderr='',
+            rc='',
+            start='',
+            end='',
+            delta='',
+        )
+
+    startd = datetime.datetime.now()
+    changed = False
+
+    # will return either the image name or None
+    container_image = is_containerized()
+
+    user = "client.admin"
+    keyring_filename = cluster + '.' + user + '.keyring'
+    user_key = os.path.join("/etc/ceph/", keyring_filename)
+
+    def_opt = {
+        'size': {
+            'conf_name': 'osd_pool_default_size',
+            'cli_set_opt': 'size'
+        },
+        'min_size': {
+            'conf_name': 'osd_pool_default_min_size',
+            'cli_set_opt': 'min_size'
+        },
+        'pg_num': {
+            'conf_name': 'osd_pool_default_pg_num',
+            'cli_set_opt': 'pg_num'
+        },
+        'pgp_num': {
+            'conf_name': 'osd_pool_default_pgp_num',
+            'cli_set_opt': 'pgp_num'
+        }
+    }
+
+    if state == "present":
+        rc, cmd, default_running_ceph_config, err = get_default_running_config(module, cluster, user, user_key, container_image=container_image)
+        if rc == 0:
+            for k, v in def_opt.items():
+                if module.params[k] == None:
+                    user_pool_config[k] = {'value': default_running_ceph_config[v['conf_name']], 'cli_set_opt': v['cli_set_opt']}
+                else:
+                    user_pool_config[k] = {'value': module.params.get(k), 'cli_set_opt': v['cli_set_opt']}
+            rc, cmd, out, err = exec_commands(module, check_pool_exist(cluster, name, user, user_key, container_image=container_image))
+            if rc == 0:
+                running_pool_details = get_pool_details(module, cluster, name, user, user_key, container_image=container_image)
+                user_pool_config['pg_placement_num'] = { 'value': str(running_pool_details[2]['pg_placement_num']), 'cli_set_opt': 'pgp_num' }
+                delta = compare_pool_config(user_pool_config, running_pool_details[2])
+                if len(delta) > 0 and running_pool_details[2]['erasure_code_profile'] == "" and 'size' not in delta.keys():
+                    rc, cmd, out, err = update_pool(module, cluster, name, user, user_key, delta, container_image=container_image)
+                    if rc == 0:
+                        changed = True
+                else:
+                    out = "Pool {} already exists and there is nothing to update.".format(name)
+            else:
+                rc, cmd, out, err = exec_commands(module, create_pool(cluster, name, user, user_key, user_pool_config=user_pool_config, container_image=container_image))
+                changed = True
+
+    elif state == "list":
+        rc, cmd, out, err = exec_commands(module, list_pools(cluster, name, user, user_key, details, container_image=container_image))
+        if rc != 0:
+            out = "Couldn't list pool(s) present on the cluster"
+
+    elif state == "absent":
+        rc, cmd, out, err = exec_commands(module, remove_pool(cluster, name, user, user_key, 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()
+