#!/usr/bin/python from ansible.module_utils.basic import AnsibleModule try: from ansible.module_utils.ca_common import exec_command, \ is_containerized, \ fatal except ImportError: from module_utils.ca_common import exec_command, \ is_containerized, \ fatal import datetime import copy import json import os import re ANSIBLE_METADATA = { 'metadata_version': '1.0', 'status': ['preview'], 'supported_by': 'community' } DOCUMENTATION = ''' --- module: ceph_volume short_description: Create ceph OSDs with ceph-volume description: - Using the ceph-volume utility available in Ceph this module can be used to create ceph OSDs that are backed by logical volumes. - Only available in ceph versions luminous or greater. options: cluster: description: - The ceph cluster name. required: false default: ceph objectstore: description: - The objectstore of the OSD. required: false choices: ['bluestore'] default: bluestore action: description: - The action to take. Creating OSDs and zapping or querying devices. required: true choices: ['create', 'zap', 'batch', 'prepare', 'activate', 'list', 'inventory'] default: create data: description: - The logical volume name or device to use for the OSD data. required: true data_vg: description: - If data is a lv, this must be the name of the volume group it belongs to. required: false osd_fsid: description: - The OSD FSID required: false osd_id: description: - The OSD ID required: false db: description: - A partition or logical volume name to use for block.db. required: false db_vg: description: - If db is a lv, this must be the name of the volume group it belongs to. # noqa: E501 required: false wal: description: - A partition or logical volume name to use for block.wal. required: false wal_vg: description: - If wal is a lv, this must be the name of the volume group it belongs to. # noqa: E501 required: false crush_device_class: description: - Will set the crush device class for the OSD. required: false dmcrypt: description: - If set to True the OSD will be encrypted with dmcrypt. required: false batch_devices: description: - A list of devices to pass to the 'ceph-volume lvm batch' subcommand. - Only applicable if action is 'batch'. required: false osds_per_device: description: - The number of OSDs to create per device. - Only applicable if action is 'batch'. required: false default: 1 block_db_size: description: - The size in bytes of bluestore block db lvs. - The default of -1 means to create them as big as possible. - Only applicable if action is 'batch'. required: false default: -1 block_db_devices: description: - A list of devices for bluestore block db to pass to the 'ceph-volume lvm batch' subcommand. - Only applicable if action is 'batch'. required: false wal_devices: description: - A list of devices for bluestore block wal to pass to the 'ceph-volume lvm batch' subcommand. - Only applicable if action is 'batch'. required: false report: description: - If provided the --report flag will be passed to 'ceph-volume lvm batch'. - No OSDs will be created. - Results will be returned in json format. - Only applicable if action is 'batch'. required: false list: description: - List potential Ceph LVM metadata on a device required: false inventory: description: - List storage device inventory. required: false author: - Andrew Schoen (@andrewschoen) - Sebastien Han ''' EXAMPLES = ''' - name: set up a bluestore osd with a raw device for data ceph_volume: objectstore: bluestore data: /dev/sdc action: create - name: set up a bluestore osd with an lv for data and partitions for block.wal and block.db # noqa: E501 ceph_volume: objectstore: bluestore data: data-lv data_vg: data-vg db: /dev/sdc1 wal: /dev/sdc2 action: create ''' def container_exec(binary, container_image, mounts=None): ''' Build the docker CLI to run a command inside a container ''' _mounts = {} _mounts['/run/lock/lvm'] = '/run/lock/lvm:z' _mounts['/var/run/udev'] = '/var/run/udev:z' _mounts['/dev'] = '/dev' _mounts['/etc/ceph'] = '/etc/ceph:z' _mounts['/run/lvm'] = '/run/lvm' _mounts['/var/lib/ceph'] = '/var/lib/ceph:z' _mounts['/var/log/ceph'] = '/var/log/ceph:z' if mounts is None: mounts = _mounts else: _mounts.update(mounts) volumes = sum( [['-v', '{}:{}'.format(src_dir, dst_dir)] for src_dir, dst_dir in _mounts.items()], []) container_binary = os.getenv('CEPH_CONTAINER_BINARY') command_exec = [container_binary, 'run', '--rm', '--privileged', '--net=host', '--ipc=host'] + volumes + \ ['--entrypoint=' + binary, container_image] return command_exec def build_cmd(action, container_image, cluster='ceph', binary='ceph-volume', mounts=None): ''' Build the ceph-volume command ''' _binary = binary if container_image: cmd = container_exec( binary, container_image, mounts=mounts) else: binary = [binary] cmd = binary if _binary == 'ceph-volume': cmd.extend(['--cluster', cluster]) cmd.extend(action) return cmd def get_data(data, data_vg): if data_vg: data = '{0}/{1}'.format(data_vg, data) return data def get_journal(journal, journal_vg): if journal_vg: journal = '{0}/{1}'.format(journal_vg, journal) return journal def get_db(db, db_vg): if db_vg: db = '{0}/{1}'.format(db_vg, db) return db def get_wal(wal, wal_vg): if wal_vg: wal = '{0}/{1}'.format(wal_vg, wal) return wal def batch(module, container_image, report=None): ''' Batch prepare OSD devices ''' # get module variables cluster = module.params['cluster'] objectstore = module.params['objectstore'] batch_devices = module.params.get('batch_devices', None) crush_device_class = module.params.get('crush_device_class', None) block_db_size = module.params.get('block_db_size', None) block_db_devices = module.params.get('block_db_devices', None) wal_devices = module.params.get('wal_devices', None) dmcrypt = module.params.get('dmcrypt', None) osds_per_device = module.params.get('osds_per_device', 1) if not osds_per_device: fatal('osds_per_device must be provided if action is "batch"', module) if osds_per_device < 1: fatal('osds_per_device must be greater than 0 if action is "batch"', module) # noqa: E501 if not batch_devices: fatal('batch_devices must be provided if action is "batch"', module) # Build the CLI action = ['lvm', 'batch'] cmd = build_cmd(action, container_image, cluster) cmd.extend(['--%s' % objectstore]) if not report: cmd.append('--yes') if container_image: cmd.append('--prepare') if crush_device_class: cmd.extend(['--crush-device-class', crush_device_class]) if dmcrypt: cmd.append('--dmcrypt') if osds_per_device > 1: cmd.extend(['--osds-per-device', str(osds_per_device)]) if objectstore == 'bluestore' and block_db_size != '-1': cmd.extend(['--block-db-size', block_db_size]) cmd.extend(batch_devices) if block_db_devices and objectstore == 'bluestore': cmd.append('--db-devices') cmd.extend(block_db_devices) if wal_devices and objectstore == 'bluestore': cmd.append('--wal-devices') cmd.extend(wal_devices) return cmd def ceph_volume_cmd(subcommand, container_image, cluster=None): ''' Build ceph-volume initial command ''' if container_image: binary = 'ceph-volume' cmd = container_exec( binary, container_image) else: binary = ['ceph-volume'] cmd = binary if cluster: cmd.extend(['--cluster', cluster]) cmd.append('lvm') cmd.append(subcommand) return cmd def prepare_or_create_osd(module, action, container_image): ''' Prepare or create OSD devices ''' # get module variables cluster = module.params['cluster'] objectstore = module.params['objectstore'] data = module.params['data'] data_vg = module.params.get('data_vg', None) data = get_data(data, data_vg) db = module.params.get('db', None) db_vg = module.params.get('db_vg', None) wal = module.params.get('wal', None) wal_vg = module.params.get('wal_vg', None) crush_device_class = module.params.get('crush_device_class', None) dmcrypt = module.params.get('dmcrypt', None) # Build the CLI action = ['lvm', action] cmd = build_cmd(action, container_image, cluster) cmd.extend(['--%s' % objectstore]) cmd.append('--data') cmd.append(data) if db and objectstore == 'bluestore': db = get_db(db, db_vg) cmd.extend(['--block.db', db]) if wal and objectstore == 'bluestore': wal = get_wal(wal, wal_vg) cmd.extend(['--block.wal', wal]) if crush_device_class: cmd.extend(['--crush-device-class', crush_device_class]) if dmcrypt: cmd.append('--dmcrypt') return cmd def list_osd(module, container_image): ''' List will detect wether or not a device has Ceph LVM Metadata ''' # get module variables cluster = module.params['cluster'] data = module.params.get('data', None) data_vg = module.params.get('data_vg', None) data = get_data(data, data_vg) # Build the CLI action = ['lvm', 'list'] cmd = build_cmd(action, container_image, cluster, mounts={'/var/lib/ceph': '/var/lib/ceph:ro'}) if data: cmd.append(data) cmd.append('--format=json') return cmd def list_storage_inventory(module, container_image): ''' List storage inventory. ''' action = ['inventory'] cmd = build_cmd(action, container_image) cmd.append('--format=json') return cmd def activate_osd(): ''' Activate all the OSDs on a machine ''' # build the CLI action = ['lvm', 'activate'] container_image = None cmd = build_cmd(action, container_image) cmd.append('--all') return cmd def is_lv(module, vg, lv, container_image): ''' Check if an LV exists ''' args = ['--noheadings', '--reportformat', 'json', '--select', 'lv_name={},vg_name={}'.format(lv, vg)] # noqa: E501 cmd = build_cmd(args, container_image, binary='lvs') rc, cmd, out, err = exec_command(module, cmd) if rc == 0: result = json.loads(out)['report'][0]['lv'] if len(result) > 0: return True return False def zap_devices(module, container_image): ''' Will run 'ceph-volume lvm zap' on all devices, lvs and partitions used to create the OSD. The --destroy flag is always passed so that if an OSD was originally created with a raw device or partition for 'data' then any lvs that were created by ceph-volume are removed. ''' # get module variables data = module.params.get('data', None) data_vg = module.params.get('data_vg', None) db = module.params.get('db', None) db_vg = module.params.get('db_vg', None) wal = module.params.get('wal', None) wal_vg = module.params.get('wal_vg', None) osd_fsid = module.params.get('osd_fsid', None) osd_id = module.params.get('osd_id', None) destroy = module.params.get('destroy', True) # build the CLI action = ['lvm', 'zap'] cmd = build_cmd(action, container_image) if destroy: cmd.append('--destroy') if osd_fsid: cmd.extend(['--osd-fsid', osd_fsid]) if osd_id: cmd.extend(['--osd-id', osd_id]) if data: data = get_data(data, data_vg) cmd.append(data) if db: db = get_db(db, db_vg) cmd.extend([db]) if wal: wal = get_wal(wal, wal_vg) cmd.extend([wal]) return cmd def allowed_in_check_mode(module): ''' Check if the action is allowed in check mode ''' action = module.params['action'] report = module.params.get('report', False) # batch is allowed in check mode if report is set if action == 'batch' and report: return True allowed_actions = ['list', 'inventory'] return action in allowed_actions def run_module(): module_args = dict( cluster=dict(type='str', required=False, default='ceph'), objectstore=dict(type='str', required=False, choices=[ 'bluestore'], default='bluestore'), action=dict(type='str', required=False, choices=[ 'create', 'zap', 'batch', 'prepare', 'activate', 'list', 'inventory'], default='create'), # noqa: 4502 data=dict(type='str', required=False), data_vg=dict(type='str', required=False), db=dict(type='str', required=False), db_vg=dict(type='str', required=False), wal=dict(type='str', required=False), wal_vg=dict(type='str', required=False), crush_device_class=dict(type='str', required=False), dmcrypt=dict(type='bool', required=False, default=False), batch_devices=dict(type='list', required=False, default=[]), osds_per_device=dict(type='int', required=False, default=1), block_db_size=dict(type='str', required=False, default='-1'), block_db_devices=dict(type='list', required=False, default=[]), wal_devices=dict(type='list', required=False, default=[]), report=dict(type='bool', required=False, default=False), osd_fsid=dict(type='str', required=False), osd_id=dict(type='str', required=False), destroy=dict(type='bool', required=False, default=True), ) module = AnsibleModule( argument_spec=module_args, supports_check_mode=True, mutually_exclusive=[ ('data', 'osd_fsid', 'osd_id'), ], required_if=[ ('action', 'zap', ('data', 'osd_fsid', 'osd_id'), True) ] ) result = dict( changed=False, stdout='', stderr='', rc=0, start='', end='', delta='', ) if module.check_mode and not allowed_in_check_mode(module): module.exit_json(**result) # start execution startd = datetime.datetime.now() # get the desired action action = module.params['action'] # will return either the image name or None container_image = is_containerized() # Assume the task's status will be 'changed' changed = True if action == 'create' or action == 'prepare': # First test if the device has Ceph LVM Metadata rc, cmd, out, err = exec_command( module, list_osd(module, container_image)) # list_osd returns a dict, if the dict is empty this means # we can not check the return code since it's not consistent # with the plain output # see: http://tracker.ceph.com/issues/36329 # FIXME: it's probably less confusing to check for rc # convert out to json, ansible returns a string... try: out_dict = json.loads(out) except ValueError: fatal("Could not decode json output: {} from the command {}".format(out, cmd), module) # noqa: E501 if out_dict: data = module.params['data'] result['stdout'] = 'skipped, since {0} is already used for an osd'.format(data) # noqa: E501 result['rc'] = 0 module.exit_json(**result) # Prepare or create the OSD rc, cmd, out, err = exec_command( module, prepare_or_create_osd(module, action, container_image)) err = re.sub('[a-zA-Z0-9+/]{38}==', '*' * 8, err) elif action == 'activate': if container_image: fatal( "This is not how container's activation happens, nothing to activate", module) # noqa: E501 # Activate the OSD rc, cmd, out, err = exec_command( module, activate_osd()) elif action == 'zap': # Zap the OSD skip = [] for device_type in ['journal', 'data', 'db', 'wal']: # 1/ if we passed vg/lv if module.params.get('{}_vg'.format(device_type), None) and module.params.get(device_type, None): # noqa: E501 # 2/ check this is an actual lv/vg ret = is_lv(module, module.params['{}_vg'.format(device_type)], module.params[device_type], container_image) # noqa: E501 skip.append(ret) # 3/ This isn't a lv/vg device if not ret: module.params['{}_vg'.format(device_type)] = False module.params[device_type] = False # 4/ no journal|data|db|wal|_vg was passed, so it must be a raw device # noqa: E501 elif not module.params.get('{}_vg'.format(device_type), None) and module.params.get(device_type, None): # noqa: E501 skip.append(True) cmd = zap_devices(module, container_image) if any(skip) or module.params.get('osd_fsid', None) \ or module.params.get('osd_id', None): rc, cmd, out, err = exec_command( module, cmd) for scan_cmd in ['vgscan', 'lvscan']: module.run_command([scan_cmd, '--cache']) else: out = 'Skipped, nothing to zap' err = '' changed = False rc = 0 elif action == 'list': # List Ceph LVM Metadata on a device changed = False rc, cmd, out, err = exec_command( module, list_osd(module, container_image)) elif action == 'inventory': # List storage device inventory. changed = False rc, cmd, out, err = exec_command( module, list_storage_inventory(module, container_image)) elif action == 'batch': # Batch prepare AND activate OSDs report = module.params.get('report', None) # Add --report flag for the idempotency test report_flags = [ '--report', '--format=json', ] cmd = batch(module, container_image, report=True) batch_report_cmd = copy.copy(cmd) batch_report_cmd.extend(report_flags) # Run batch --report to see what's going to happen # Do not run the batch command if there is nothing to do rc, cmd, out, err = exec_command( module, batch_report_cmd) try: if not out: out = '{}' report_result = json.loads(out) except ValueError: strategy_changed_in_out = "strategy changed" in out strategy_changed_in_err = "strategy changed" in err strategy_changed = strategy_changed_in_out or \ strategy_changed_in_err if strategy_changed: if strategy_changed_in_out: out = json.dumps({"changed": False, "stdout": out.rstrip("\r\n")}) elif strategy_changed_in_err: out = json.dumps({"changed": False, "stderr": err.rstrip("\r\n")}) rc = 0 changed = False else: out = out.rstrip("\r\n") result = dict( cmd=cmd, stdout=out.rstrip('\r\n'), stderr=err.rstrip('\r\n'), rc=rc, changed=changed, ) if strategy_changed: module.exit_json(**result) module.fail_json(msg='non-zero return code', **result) if not report: if 'changed' in report_result: # we have the old batch implementation # if not asking for a report, let's just run the batch command changed = report_result['changed'] if changed: # Batch prepare the OSD rc, cmd, out, err = exec_command( module, batch(module, container_image)) err = re.sub('[a-zA-Z0-9+/]{38}==', '*' * 8, err) else: # we have the refactored batch, its idempotent so lets just # run it rc, cmd, out, err = exec_command( module, batch(module, container_image)) err = re.sub('[a-zA-Z0-9+/]{38}==', '*' * 8, err) else: cmd = batch_report_cmd 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, ) if rc != 0: module.fail_json(msg='non-zero return code', **result) module.exit_json(**result) def main(): run_module() if __name__ == '__main__': main()