from __future__ import print_function import argparse import json import logging from textwrap import dedent from ceph_volume import decorators from ceph_volume.util import disk from ceph_volume.api import lvm as api logger = logging.getLogger(__name__) osd_list_header_template = """\n {osd_id:=^20}""" osd_device_header_template = """ [{type: >4}] {path} """ device_metadata_item_template = """ {tag_name: <25} {value}""" def readable_tag(tag): actual_name = tag.split('.')[-1] return actual_name.replace('_', ' ') def pretty_report(report): output = [] for _id, devices in report.items(): output.append( osd_list_header_template.format(osd_id=" osd.%s " % _id) ) for device in devices: output.append( osd_device_header_template.format( type=device['type'], path=device['path'] ) ) for tag_name, value in device.get('tags', {}).items(): output.append( device_metadata_item_template.format( tag_name=readable_tag(tag_name), value=value ) ) print(''.join(output)) class List(object): help = 'list logical volumes and devices associated with Ceph' def __init__(self, argv): self.argv = argv @decorators.needs_root def list(self, args): # ensure everything is up to date before calling out # to list lv's self.update() report = self.generate(args) if args.format == 'json': # If the report is empty, we don't return a non-zero exit status # because it is assumed this is going to be consumed by automated # systems like ceph-ansible which would be forced to ignore the # non-zero exit status if all they need is the information in the # JSON object print(json.dumps(report, indent=4, sort_keys=True)) else: if not report: raise SystemExit('No valid Ceph devices found') pretty_report(report) def update(self): """ Ensure all journal devices are up to date if they aren't a logical volume """ lvs = api.Volumes() for lv in lvs: try: lv.tags['ceph.osd_id'] except KeyError: # only consider ceph-based logical volumes, everything else # will get ignored continue for device_type in ['journal', 'block', 'wal', 'db']: device_name = 'ceph.%s_device' % device_type device_uuid = lv.tags.get('ceph.%s_uuid' % device_type) if not device_uuid: # bluestore will not have a journal, filestore will not have # a block/wal/db, so we must skip if not present continue disk_device = disk.get_device_from_partuuid(device_uuid) if disk_device: if lv.tags[device_name] != disk_device: # this means that the device has changed, so it must be updated # on the API to reflect this lv.set_tags({device_name: disk_device}) def generate(self, args): """ Generate reports for an individual device or for all Ceph-related devices, logical or physical, as long as they have been prepared by this tool before and contain enough metadata. """ if args.device: return self.single_report(args.device) else: return self.full_report() def single_report(self, device): """ Generate a report for a single device. This can be either a logical volume in the form of vg/lv or a device with an absolute path like /dev/sda1 """ lvs = api.Volumes() report = {} lv = api.get_lv_from_argument(device) if lv: try: _id = lv.tags['ceph.osd_id'] except KeyError: logger.warning('device is not part of ceph: %s', device) return report report.setdefault(_id, []) report[_id].append( lv.as_dict() ) else: # this has to be a journal/wal/db device (not a logical volume) so try # to find the PARTUUID that should be stored in the OSD logical # volume for device_type in ['journal', 'block', 'wal', 'db']: device_tag_name = 'ceph.%s_device' % device_type device_tag_uuid = 'ceph.%s_uuid' % device_type associated_lv = lvs.get(lv_tags={device_tag_name: device}) if associated_lv: _id = associated_lv.tags['ceph.osd_id'] uuid = associated_lv.tags[device_tag_uuid] report.setdefault(_id, []) report[_id].append( { 'tags': {'PARTUUID': uuid}, 'type': device_type, 'path': device, } ) return report def full_report(self): """ Generate a report for all the logical volumes and associated devices that have been previously prepared by Ceph """ lvs = api.Volumes() report = {} for lv in lvs: try: _id = lv.tags['ceph.osd_id'] except KeyError: # only consider ceph-based logical volumes, everything else # will get ignored continue report.setdefault(_id, []) report[_id].append( lv.as_dict() ) for device_type in ['journal', 'block', 'wal', 'db']: device_uuid = lv.tags.get('ceph.%s_uuid' % device_type) if not device_uuid: # bluestore will not have a journal, filestore will not have # a block/wal/db, so we must skip if not present continue if not api.get_lv(lv_uuid=device_uuid): # means we have a regular device, so query blkid disk_device = disk.get_device_from_partuuid(device_uuid) if disk_device: report[_id].append( { 'tags': {'PARTUUID': device_uuid}, 'type': device_type, 'path': disk_device, } ) return report def main(self): sub_command_help = dedent(""" List devices or logical volumes associated with Ceph. An association is determined if a device has information relating to an OSD. This is verified by querying LVM's metadata and correlating it with devices. The lvs associated with the OSD need to have been prepared previously, so that all needed tags and metadata exist. Full listing of all system devices associated with a cluster:: ceph-volume lvm list List a particular device, reporting all metadata about it:: ceph-volume lvm list /dev/sda1 List a logical volume, along with all its metadata (vg is a volume group, and lv the logical volume name):: ceph-volume lvm list {vg/lv} """) parser = argparse.ArgumentParser( prog='ceph-volume lvm list', formatter_class=argparse.RawDescriptionHelpFormatter, description=sub_command_help, ) parser.add_argument( 'device', metavar='DEVICE', nargs='?', help='Path to an lv (as vg/lv) or to a device like /dev/sda1' ) parser.add_argument( '--format', help='output format, defaults to "pretty"', default='pretty', choices=['json', 'pretty'], ) args = parser.parse_args(self.argv) self.list(args)