--- /dev/null
+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)