]>
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 | ) | |
52 | print(''.join(output)) | |
53 | ||
54 | ||
94b18763 FG |
55 | def direct_report(): |
56 | """ | |
57 | Other non-cli consumers of listing information will want to consume the | |
58 | report without the need to parse arguments or other flags. This helper | |
59 | bypasses the need to deal with the class interface which is meant for cli | |
60 | handling. | |
61 | """ | |
62 | _list = List([]) | |
63 | # this is crucial: make sure that all paths will reflect current | |
64 | # information. In the case of a system that has migrated, the disks will | |
65 | # have changed paths | |
66 | _list.update() | |
67 | return _list.full_report() | |
68 | ||
69 | ||
3efd9988 FG |
70 | class List(object): |
71 | ||
72 | help = 'list logical volumes and devices associated with Ceph' | |
73 | ||
74 | def __init__(self, argv): | |
75 | self.argv = argv | |
76 | ||
77 | @decorators.needs_root | |
78 | def list(self, args): | |
79 | # ensure everything is up to date before calling out | |
80 | # to list lv's | |
81 | self.update() | |
82 | report = self.generate(args) | |
83 | if args.format == 'json': | |
84 | # If the report is empty, we don't return a non-zero exit status | |
85 | # because it is assumed this is going to be consumed by automated | |
86 | # systems like ceph-ansible which would be forced to ignore the | |
87 | # non-zero exit status if all they need is the information in the | |
88 | # JSON object | |
89 | print(json.dumps(report, indent=4, sort_keys=True)) | |
90 | else: | |
91 | if not report: | |
92 | raise SystemExit('No valid Ceph devices found') | |
93 | pretty_report(report) | |
94 | ||
95 | def update(self): | |
96 | """ | |
97 | Ensure all journal devices are up to date if they aren't a logical | |
98 | volume | |
99 | """ | |
100 | lvs = api.Volumes() | |
101 | for lv in lvs: | |
102 | try: | |
103 | lv.tags['ceph.osd_id'] | |
104 | except KeyError: | |
105 | # only consider ceph-based logical volumes, everything else | |
106 | # will get ignored | |
107 | continue | |
108 | ||
109 | for device_type in ['journal', 'block', 'wal', 'db']: | |
110 | device_name = 'ceph.%s_device' % device_type | |
111 | device_uuid = lv.tags.get('ceph.%s_uuid' % device_type) | |
112 | if not device_uuid: | |
113 | # bluestore will not have a journal, filestore will not have | |
114 | # a block/wal/db, so we must skip if not present | |
115 | continue | |
116 | disk_device = disk.get_device_from_partuuid(device_uuid) | |
117 | if disk_device: | |
118 | if lv.tags[device_name] != disk_device: | |
119 | # this means that the device has changed, so it must be updated | |
120 | # on the API to reflect this | |
121 | lv.set_tags({device_name: disk_device}) | |
122 | ||
123 | def generate(self, args): | |
124 | """ | |
125 | Generate reports for an individual device or for all Ceph-related | |
126 | devices, logical or physical, as long as they have been prepared by | |
127 | this tool before and contain enough metadata. | |
128 | """ | |
129 | if args.device: | |
130 | return self.single_report(args.device) | |
131 | else: | |
132 | return self.full_report() | |
133 | ||
134 | def single_report(self, device): | |
135 | """ | |
136 | Generate a report for a single device. This can be either a logical | |
137 | volume in the form of vg/lv or a device with an absolute path like | |
94b18763 | 138 | /dev/sda1 or /dev/sda |
3efd9988 FG |
139 | """ |
140 | lvs = api.Volumes() | |
141 | report = {} | |
142 | lv = api.get_lv_from_argument(device) | |
94b18763 FG |
143 | |
144 | # check if there was a pv created with the | |
145 | # name of device | |
146 | pv = api.get_pv(pv_name=device) | |
147 | if pv and not lv: | |
148 | try: | |
149 | lv = api.get_lv(vg_name=pv.vg_name) | |
150 | except MultipleLVsError: | |
151 | lvs.filter(vg_name=pv.vg_name) | |
152 | return self.full_report(lvs=lvs) | |
153 | ||
3efd9988 FG |
154 | if lv: |
155 | try: | |
156 | _id = lv.tags['ceph.osd_id'] | |
157 | except KeyError: | |
158 | logger.warning('device is not part of ceph: %s', device) | |
159 | return report | |
160 | ||
161 | report.setdefault(_id, []) | |
162 | report[_id].append( | |
163 | lv.as_dict() | |
164 | ) | |
165 | ||
166 | else: | |
167 | # this has to be a journal/wal/db device (not a logical volume) so try | |
168 | # to find the PARTUUID that should be stored in the OSD logical | |
169 | # volume | |
170 | for device_type in ['journal', 'block', 'wal', 'db']: | |
171 | device_tag_name = 'ceph.%s_device' % device_type | |
172 | device_tag_uuid = 'ceph.%s_uuid' % device_type | |
173 | associated_lv = lvs.get(lv_tags={device_tag_name: device}) | |
174 | if associated_lv: | |
175 | _id = associated_lv.tags['ceph.osd_id'] | |
176 | uuid = associated_lv.tags[device_tag_uuid] | |
177 | ||
178 | report.setdefault(_id, []) | |
179 | report[_id].append( | |
180 | { | |
181 | 'tags': {'PARTUUID': uuid}, | |
182 | 'type': device_type, | |
183 | 'path': device, | |
184 | } | |
185 | ) | |
186 | return report | |
187 | ||
94b18763 | 188 | def full_report(self, lvs=None): |
3efd9988 FG |
189 | """ |
190 | Generate a report for all the logical volumes and associated devices | |
191 | that have been previously prepared by Ceph | |
192 | """ | |
94b18763 FG |
193 | if lvs is None: |
194 | lvs = api.Volumes() | |
3efd9988 FG |
195 | report = {} |
196 | for lv in lvs: | |
197 | try: | |
198 | _id = lv.tags['ceph.osd_id'] | |
199 | except KeyError: | |
200 | # only consider ceph-based logical volumes, everything else | |
201 | # will get ignored | |
202 | continue | |
203 | ||
204 | report.setdefault(_id, []) | |
205 | report[_id].append( | |
206 | lv.as_dict() | |
207 | ) | |
208 | ||
209 | for device_type in ['journal', 'block', 'wal', 'db']: | |
210 | device_uuid = lv.tags.get('ceph.%s_uuid' % device_type) | |
211 | if not device_uuid: | |
212 | # bluestore will not have a journal, filestore will not have | |
213 | # a block/wal/db, so we must skip if not present | |
214 | continue | |
215 | if not api.get_lv(lv_uuid=device_uuid): | |
216 | # means we have a regular device, so query blkid | |
217 | disk_device = disk.get_device_from_partuuid(device_uuid) | |
218 | if disk_device: | |
219 | report[_id].append( | |
220 | { | |
221 | 'tags': {'PARTUUID': device_uuid}, | |
222 | 'type': device_type, | |
223 | 'path': disk_device, | |
224 | } | |
225 | ) | |
226 | ||
227 | return report | |
228 | ||
229 | def main(self): | |
230 | sub_command_help = dedent(""" | |
231 | List devices or logical volumes associated with Ceph. An association is | |
232 | determined if a device has information relating to an OSD. This is | |
233 | verified by querying LVM's metadata and correlating it with devices. | |
234 | ||
235 | The lvs associated with the OSD need to have been prepared previously, | |
236 | so that all needed tags and metadata exist. | |
237 | ||
238 | Full listing of all system devices associated with a cluster:: | |
239 | ||
240 | ceph-volume lvm list | |
241 | ||
242 | List a particular device, reporting all metadata about it:: | |
243 | ||
244 | ceph-volume lvm list /dev/sda1 | |
245 | ||
246 | List a logical volume, along with all its metadata (vg is a volume | |
247 | group, and lv the logical volume name):: | |
248 | ||
249 | ceph-volume lvm list {vg/lv} | |
250 | """) | |
251 | parser = argparse.ArgumentParser( | |
252 | prog='ceph-volume lvm list', | |
253 | formatter_class=argparse.RawDescriptionHelpFormatter, | |
254 | description=sub_command_help, | |
255 | ) | |
256 | ||
257 | parser.add_argument( | |
258 | 'device', | |
259 | metavar='DEVICE', | |
260 | nargs='?', | |
261 | help='Path to an lv (as vg/lv) or to a device like /dev/sda1' | |
262 | ) | |
263 | ||
264 | parser.add_argument( | |
265 | '--format', | |
266 | help='output format, defaults to "pretty"', | |
267 | default='pretty', | |
268 | choices=['json', 'pretty'], | |
269 | ) | |
270 | ||
271 | args = parser.parse_args(self.argv) | |
272 | self.list(args) |