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