ceph-ansible/library/ceph_volume.py

733 lines
22 KiB
Python

#!/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 <seb@redhat.com>
'''
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()