]> git.proxmox.com Git - ceph.git/blame - ceph/src/ceph-volume/ceph_volume/devices/lvm/listing.py
import ceph 14.2.5
[ceph.git] / ceph / src / ceph-volume / ceph_volume / devices / lvm / listing.py
CommitLineData
3efd9988
FG
1from __future__ import print_function
2import argparse
3import json
4import logging
5from textwrap import dedent
6from ceph_volume import decorators
7from ceph_volume.util import disk
8from ceph_volume.api import lvm as api
94b18763 9from ceph_volume.exceptions import MultipleLVsError
3efd9988
FG
10
11logger = logging.getLogger(__name__)
12
13
14osd_list_header_template = """\n
15{osd_id:=^20}"""
16
17
18osd_device_header_template = """
19
11fdf7f2 20 {type: <13} {path}
3efd9988
FG
21"""
22
23device_metadata_item_template = """
24 {tag_name: <25} {value}"""
25
26
27def readable_tag(tag):
28 actual_name = tag.split('.')[-1]
29 return actual_name.replace('_', ' ')
30
31
32def pretty_report(report):
33 output = []
11fdf7f2 34 for _id, devices in sorted(report.items()):
3efd9988
FG
35 output.append(
36 osd_list_header_template.format(osd_id=" osd.%s " % _id)
37 )
38 for device in devices:
39 output.append(
40 osd_device_header_template.format(
11fdf7f2 41 type='[%s]' % device['type'],
3efd9988
FG
42 path=device['path']
43 )
44 )
11fdf7f2 45 for tag_name, value in sorted(device.get('tags', {}).items()):
3efd9988
FG
46 output.append(
47 device_metadata_item_template.format(
48 tag_name=readable_tag(tag_name),
49 value=value
50 )
51 )
1adf2230
AA
52 if not device.get('devices'):
53 continue
54 else:
55 output.append(
56 device_metadata_item_template.format(
57 tag_name='devices',
58 value=','.join(device['devices'])
59 )
60 )
28e407b8 61
3efd9988
FG
62 print(''.join(output))
63
64
94b18763
FG
65def direct_report():
66 """
67 Other non-cli consumers of listing information will want to consume the
68 report without the need to parse arguments or other flags. This helper
69 bypasses the need to deal with the class interface which is meant for cli
70 handling.
71 """
72 _list = List([])
73 # this is crucial: make sure that all paths will reflect current
74 # information. In the case of a system that has migrated, the disks will
75 # have changed paths
76 _list.update()
77 return _list.full_report()
78
79
3efd9988
FG
80class List(object):
81
82 help = 'list logical volumes and devices associated with Ceph'
83
84 def __init__(self, argv):
85 self.argv = argv
86
28e407b8
AA
87 @property
88 def pvs(self):
89 """
90 To avoid having to make an LVM API call for every single item being
91 reported, the call gets set only once, using that stored call for
92 subsequent calls
93 """
94 if getattr(self, '_pvs', None) is not None:
95 return self._pvs
96 self._pvs = api.get_api_pvs()
97 return self._pvs
98
99 def match_devices(self, lv_uuid):
100 """
101 It is possible to have more than one PV reported *with the same name*,
102 to avoid incorrect or duplicate contents we correlated the lv uuid to
103 the one on the physical device.
104 """
105 devices = []
106 for device in self.pvs:
107 if device.get('lv_uuid') == lv_uuid:
108 devices.append(device['pv_name'])
109 return devices
110
3efd9988
FG
111 @decorators.needs_root
112 def list(self, args):
113 # ensure everything is up to date before calling out
114 # to list lv's
eafe8130
TL
115 lvs = self.update()
116 report = self.generate(args, lvs)
3efd9988
FG
117 if args.format == 'json':
118 # If the report is empty, we don't return a non-zero exit status
119 # because it is assumed this is going to be consumed by automated
120 # systems like ceph-ansible which would be forced to ignore the
121 # non-zero exit status if all they need is the information in the
122 # JSON object
123 print(json.dumps(report, indent=4, sort_keys=True))
124 else:
125 if not report:
126 raise SystemExit('No valid Ceph devices found')
127 pretty_report(report)
128
129 def update(self):
130 """
131 Ensure all journal devices are up to date if they aren't a logical
132 volume
133 """
134 lvs = api.Volumes()
135 for lv in lvs:
136 try:
137 lv.tags['ceph.osd_id']
138 except KeyError:
139 # only consider ceph-based logical volumes, everything else
140 # will get ignored
141 continue
142
143 for device_type in ['journal', 'block', 'wal', 'db']:
144 device_name = 'ceph.%s_device' % device_type
145 device_uuid = lv.tags.get('ceph.%s_uuid' % device_type)
146 if not device_uuid:
147 # bluestore will not have a journal, filestore will not have
148 # a block/wal/db, so we must skip if not present
149 continue
150 disk_device = disk.get_device_from_partuuid(device_uuid)
151 if disk_device:
152 if lv.tags[device_name] != disk_device:
153 # this means that the device has changed, so it must be updated
154 # on the API to reflect this
155 lv.set_tags({device_name: disk_device})
eafe8130 156 return lvs
3efd9988 157
eafe8130 158 def generate(self, args, lvs=None):
3efd9988
FG
159 """
160 Generate reports for an individual device or for all Ceph-related
161 devices, logical or physical, as long as they have been prepared by
162 this tool before and contain enough metadata.
163 """
164 if args.device:
eafe8130 165 return self.single_report(args.device, lvs)
3efd9988 166 else:
eafe8130 167 return self.full_report(lvs)
3efd9988 168
eafe8130 169 def single_report(self, device, lvs=None):
3efd9988
FG
170 """
171 Generate a report for a single device. This can be either a logical
172 volume in the form of vg/lv or a device with an absolute path like
94b18763 173 /dev/sda1 or /dev/sda
3efd9988 174 """
eafe8130
TL
175 if lvs is None:
176 lvs = api.Volumes()
3efd9988
FG
177 report = {}
178 lv = api.get_lv_from_argument(device)
94b18763
FG
179
180 # check if there was a pv created with the
181 # name of device
182 pv = api.get_pv(pv_name=device)
183 if pv and not lv:
184 try:
185 lv = api.get_lv(vg_name=pv.vg_name)
186 except MultipleLVsError:
187 lvs.filter(vg_name=pv.vg_name)
188 return self.full_report(lvs=lvs)
189
3efd9988
FG
190 if lv:
191 try:
192 _id = lv.tags['ceph.osd_id']
193 except KeyError:
194 logger.warning('device is not part of ceph: %s', device)
195 return report
196
197 report.setdefault(_id, [])
28e407b8
AA
198 lv_report = lv.as_dict()
199 lv_report['devices'] = self.match_devices(lv.lv_uuid)
200 report[_id].append(lv_report)
3efd9988
FG
201
202 else:
203 # this has to be a journal/wal/db device (not a logical volume) so try
204 # to find the PARTUUID that should be stored in the OSD logical
205 # volume
206 for device_type in ['journal', 'block', 'wal', 'db']:
207 device_tag_name = 'ceph.%s_device' % device_type
208 device_tag_uuid = 'ceph.%s_uuid' % device_type
209 associated_lv = lvs.get(lv_tags={device_tag_name: device})
210 if associated_lv:
211 _id = associated_lv.tags['ceph.osd_id']
212 uuid = associated_lv.tags[device_tag_uuid]
213
214 report.setdefault(_id, [])
215 report[_id].append(
216 {
217 'tags': {'PARTUUID': uuid},
218 'type': device_type,
219 'path': device,
220 }
221 )
222 return report
223
94b18763 224 def full_report(self, lvs=None):
3efd9988
FG
225 """
226 Generate a report for all the logical volumes and associated devices
227 that have been previously prepared by Ceph
228 """
94b18763
FG
229 if lvs is None:
230 lvs = api.Volumes()
3efd9988 231 report = {}
eafe8130 232
3efd9988
FG
233 for lv in lvs:
234 try:
235 _id = lv.tags['ceph.osd_id']
236 except KeyError:
237 # only consider ceph-based logical volumes, everything else
238 # will get ignored
239 continue
240
241 report.setdefault(_id, [])
28e407b8
AA
242 lv_report = lv.as_dict()
243 lv_report['devices'] = self.match_devices(lv.lv_uuid)
244 report[_id].append(lv_report)
3efd9988
FG
245
246 for device_type in ['journal', 'block', 'wal', 'db']:
247 device_uuid = lv.tags.get('ceph.%s_uuid' % device_type)
248 if not device_uuid:
249 # bluestore will not have a journal, filestore will not have
250 # a block/wal/db, so we must skip if not present
251 continue
eafe8130 252 if not api.get_lv(lv_uuid=device_uuid, lvs=lvs):
3efd9988
FG
253 # means we have a regular device, so query blkid
254 disk_device = disk.get_device_from_partuuid(device_uuid)
255 if disk_device:
256 report[_id].append(
257 {
258 'tags': {'PARTUUID': device_uuid},
259 'type': device_type,
260 'path': disk_device,
261 }
262 )
263
264 return report
265
266 def main(self):
267 sub_command_help = dedent("""
268 List devices or logical volumes associated with Ceph. An association is
269 determined if a device has information relating to an OSD. This is
270 verified by querying LVM's metadata and correlating it with devices.
271
272 The lvs associated with the OSD need to have been prepared previously,
273 so that all needed tags and metadata exist.
274
275 Full listing of all system devices associated with a cluster::
276
277 ceph-volume lvm list
278
279 List a particular device, reporting all metadata about it::
280
281 ceph-volume lvm list /dev/sda1
282
283 List a logical volume, along with all its metadata (vg is a volume
284 group, and lv the logical volume name)::
285
286 ceph-volume lvm list {vg/lv}
287 """)
288 parser = argparse.ArgumentParser(
289 prog='ceph-volume lvm list',
290 formatter_class=argparse.RawDescriptionHelpFormatter,
291 description=sub_command_help,
292 )
293
294 parser.add_argument(
295 'device',
296 metavar='DEVICE',
297 nargs='?',
298 help='Path to an lv (as vg/lv) or to a device like /dev/sda1'
299 )
300
301 parser.add_argument(
302 '--format',
303 help='output format, defaults to "pretty"',
304 default='pretty',
305 choices=['json', 'pretty'],
306 )
307
308 args = parser.parse_args(self.argv)
309 self.list(args)