]>
Commit | Line | Data |
---|---|---|
3efd9988 | 1 | import argparse |
f64942e4 | 2 | import os |
3efd9988 | 3 | import logging |
eafe8130 | 4 | import time |
3efd9988 FG |
5 | |
6 | from textwrap import dedent | |
7 | ||
8 | from ceph_volume import decorators, terminal, process | |
9 | from ceph_volume.api import lvm as api | |
f6b5b4d7 | 10 | from ceph_volume.util import system, encryption, disk, arg_validators, str_to_int, merge_dict |
f64942e4 AA |
11 | from ceph_volume.util.device import Device |
12 | from ceph_volume.systemd import systemctl | |
3efd9988 FG |
13 | |
14 | logger = logging.getLogger(__name__) | |
b32b8144 | 15 | mlogger = terminal.MultiLogger(__name__) |
3efd9988 FG |
16 | |
17 | ||
18 | def wipefs(path): | |
19 | """ | |
20 | Removes the filesystem from an lv or partition. | |
eafe8130 TL |
21 | |
22 | Environment variables supported:: | |
23 | ||
24 | * ``CEPH_VOLUME_WIPEFS_TRIES``: Defaults to 8 | |
25 | * ``CEPH_VOLUME_WIPEFS_INTERVAL``: Defaults to 5 | |
26 | ||
3efd9988 | 27 | """ |
eafe8130 TL |
28 | tries = str_to_int( |
29 | os.environ.get('CEPH_VOLUME_WIPEFS_TRIES', 8) | |
30 | ) | |
31 | interval = str_to_int( | |
32 | os.environ.get('CEPH_VOLUME_WIPEFS_INTERVAL', 5) | |
33 | ) | |
34 | ||
35 | for trying in range(tries): | |
36 | stdout, stderr, exit_code = process.call([ | |
37 | 'wipefs', | |
38 | '--all', | |
39 | path | |
40 | ]) | |
41 | if exit_code != 0: | |
42 | # this could narrow the retry by poking in the stderr of the output | |
43 | # to verify that 'probing initialization failed' appears, but | |
44 | # better to be broad in this retry to prevent missing on | |
45 | # a different message that needs to be retried as well | |
46 | terminal.warning( | |
47 | 'failed to wipefs device, will try again to workaround probable race condition' | |
48 | ) | |
49 | time.sleep(interval) | |
50 | else: | |
51 | return | |
52 | raise RuntimeError("could not complete wipefs on device: %s" % path) | |
3efd9988 FG |
53 | |
54 | ||
55 | def zap_data(path): | |
56 | """ | |
57 | Clears all data from the given path. Path should be | |
58 | an absolute path to an lv or partition. | |
59 | ||
60 | 10M of data is written to the path to make sure that | |
61 | there is no trace left of any previous Filesystem. | |
62 | """ | |
63 | process.run([ | |
64 | 'dd', | |
65 | 'if=/dev/zero', | |
66 | 'of={path}'.format(path=path), | |
67 | 'bs=1M', | |
68 | 'count=10', | |
92f5a8d4 | 69 | 'conv=fsync' |
3efd9988 FG |
70 | ]) |
71 | ||
72 | ||
f64942e4 AA |
73 | def find_associated_devices(osd_id=None, osd_fsid=None): |
74 | """ | |
75 | From an ``osd_id`` and/or an ``osd_fsid``, filter out all the LVs in the | |
76 | system that match those tag values, further detect if any partitions are | |
77 | part of the OSD, and then return the set of LVs and partitions (if any). | |
78 | """ | |
79 | lv_tags = {} | |
80 | if osd_id: | |
81 | lv_tags['ceph.osd_id'] = osd_id | |
82 | if osd_fsid: | |
83 | lv_tags['ceph.osd_fsid'] = osd_fsid | |
f64942e4 | 84 | |
f6b5b4d7 TL |
85 | lvs = api.get_lvs(tags=lv_tags) |
86 | if not lvs: | |
87 | raise RuntimeError('Unable to find any LV for zapping OSD: ' | |
88 | '%s' % osd_id or osd_fsid) | |
f64942e4 | 89 | |
f6b5b4d7 | 90 | devices_to_zap = ensure_associated_lvs(lvs, lv_tags) |
f64942e4 AA |
91 | return [Device(path) for path in set(devices_to_zap) if path] |
92 | ||
93 | ||
f6b5b4d7 | 94 | def ensure_associated_lvs(lvs, lv_tags={}): |
f64942e4 AA |
95 | """ |
96 | Go through each LV and ensure if backing devices (journal, wal, block) | |
97 | are LVs or partitions, so that they can be accurately reported. | |
98 | """ | |
99 | # look for many LVs for each backing type, because it is possible to | |
100 | # receive a filtering for osd.1, and have multiple failed deployments | |
101 | # leaving many journals with osd.1 - usually, only a single LV will be | |
102 | # returned | |
f6b5b4d7 | 103 | |
f6b5b4d7 TL |
104 | db_lvs = api.get_lvs(tags=merge_dict(lv_tags, {'ceph.type': 'db'})) |
105 | wal_lvs = api.get_lvs(tags=merge_dict(lv_tags, {'ceph.type': 'wal'})) | |
05a536ef | 106 | backing_devices = [(db_lvs, 'db'), |
f6b5b4d7 | 107 | (wal_lvs, 'wal')] |
f64942e4 AA |
108 | |
109 | verified_devices = [] | |
110 | ||
111 | for lv in lvs: | |
112 | # go through each lv and append it, otherwise query `blkid` to find | |
113 | # a physical device. Do this for each type (journal,db,wal) regardless | |
114 | # if they have been processed in the previous LV, so that bad devices | |
115 | # with the same ID can be caught | |
116 | for ceph_lvs, _type in backing_devices: | |
117 | if ceph_lvs: | |
118 | verified_devices.extend([l.lv_path for l in ceph_lvs]) | |
119 | continue | |
120 | ||
121 | # must be a disk partition, by querying blkid by the uuid we are | |
122 | # ensuring that the device path is always correct | |
123 | try: | |
124 | device_uuid = lv.tags['ceph.%s_uuid' % _type] | |
125 | except KeyError: | |
126 | # Bluestore will not have ceph.journal_uuid, and Filestore | |
127 | # will not not have ceph.db_uuid | |
128 | continue | |
129 | ||
130 | osd_device = disk.get_device_from_partuuid(device_uuid) | |
131 | if not osd_device: | |
132 | # if the osd_device is not found by the partuuid, then it is | |
133 | # not possible to ensure this device exists anymore, so skip it | |
134 | continue | |
135 | verified_devices.append(osd_device) | |
136 | ||
137 | verified_devices.append(lv.lv_path) | |
138 | ||
139 | # reduce the list from all the duplicates that were added | |
140 | return list(set(verified_devices)) | |
141 | ||
142 | ||
3efd9988 FG |
143 | class Zap(object): |
144 | ||
145 | help = 'Removes all data and filesystems from a logical volume or partition.' | |
146 | ||
147 | def __init__(self, argv): | |
148 | self.argv = argv | |
149 | ||
1adf2230 AA |
150 | def unmount_lv(self, lv): |
151 | if lv.tags.get('ceph.cluster_name') and lv.tags.get('ceph.osd_id'): | |
152 | lv_path = "/var/lib/ceph/osd/{}-{}".format(lv.tags['ceph.cluster_name'], lv.tags['ceph.osd_id']) | |
3a9019d9 | 153 | else: |
1adf2230 AA |
154 | lv_path = lv.lv_path |
155 | dmcrypt_uuid = lv.lv_uuid | |
156 | dmcrypt = lv.encrypted | |
157 | if system.path_is_mounted(lv_path): | |
158 | mlogger.info("Unmounting %s", lv_path) | |
159 | system.unmount(lv_path) | |
3a9019d9 | 160 | if dmcrypt and dmcrypt_uuid: |
1adf2230 | 161 | self.dmcrypt_close(dmcrypt_uuid) |
3efd9988 | 162 | |
f64942e4 AA |
163 | def zap_lv(self, device): |
164 | """ | |
165 | Device examples: vg-name/lv-name, /dev/vg-name/lv-name | |
166 | Requirements: Must be a logical volume (LV) | |
167 | """ | |
a4b75251 TL |
168 | lv = api.get_single_lv(filters={'lv_name': device.lv_name, 'vg_name': |
169 | device.vg_name}) | |
f64942e4 AA |
170 | self.unmount_lv(lv) |
171 | ||
2a845540 TL |
172 | wipefs(device.path) |
173 | zap_data(device.path) | |
f64942e4 AA |
174 | |
175 | if self.args.destroy: | |
f6b5b4d7 TL |
176 | lvs = api.get_lvs(filters={'vg_name': device.vg_name}) |
177 | if lvs == []: | |
178 | mlogger.info('No LVs left, exiting', device.vg_name) | |
179 | return | |
180 | elif len(lvs) <= 1: | |
181 | mlogger.info('Only 1 LV left in VG, will proceed to destroy ' | |
182 | 'volume group %s', device.vg_name) | |
1e59de90 | 183 | pvs = api.get_pvs(filters={'lv_uuid': lv.lv_uuid}) |
f64942e4 | 184 | api.remove_vg(device.vg_name) |
1e59de90 TL |
185 | for pv in pvs: |
186 | api.remove_pv(pv.pv_name) | |
f64942e4 | 187 | else: |
f6b5b4d7 TL |
188 | mlogger.info('More than 1 LV left in VG, will proceed to ' |
189 | 'destroy LV only') | |
190 | mlogger.info('Removing LV because --destroy was given: %s', | |
2a845540 TL |
191 | device.path) |
192 | api.remove_lv(device.path) | |
f64942e4 AA |
193 | elif lv: |
194 | # just remove all lvm metadata, leaving the LV around | |
195 | lv.clear_tags() | |
196 | ||
197 | def zap_partition(self, device): | |
198 | """ | |
199 | Device example: /dev/sda1 | |
200 | Requirements: Must be a partition | |
201 | """ | |
202 | if device.is_encrypted: | |
203 | # find the holder | |
204 | holders = [ | |
205 | '/dev/%s' % holder for holder in device.sys_api.get('holders', []) | |
206 | ] | |
207 | for mapper_uuid in os.listdir('/dev/mapper'): | |
208 | mapper_path = os.path.join('/dev/mapper', mapper_uuid) | |
209 | if os.path.realpath(mapper_path) in holders: | |
210 | self.dmcrypt_close(mapper_uuid) | |
211 | ||
2a845540 TL |
212 | if system.device_is_mounted(device.path): |
213 | mlogger.info("Unmounting %s", device.path) | |
214 | system.unmount(device.path) | |
f64942e4 | 215 | |
2a845540 TL |
216 | wipefs(device.path) |
217 | zap_data(device.path) | |
f64942e4 AA |
218 | |
219 | if self.args.destroy: | |
2a845540 | 220 | mlogger.info("Destroying partition since --destroy was used: %s" % device.path) |
f64942e4 AA |
221 | disk.remove_partition(device) |
222 | ||
223 | def zap_lvm_member(self, device): | |
224 | """ | |
225 | An LVM member may have more than one LV and or VG, for example if it is | |
226 | a raw device with multiple partitions each belonging to a different LV | |
227 | ||
228 | Device example: /dev/sda | |
229 | Requirements: An LV or VG present in the device, making it an LVM member | |
230 | """ | |
231 | for lv in device.lvs: | |
92f5a8d4 | 232 | if lv.lv_name: |
2a845540 | 233 | mlogger.info('Zapping lvm member {}. lv_path is {}'.format(device.path, lv.lv_path)) |
92f5a8d4 TL |
234 | self.zap_lv(Device(lv.lv_path)) |
235 | else: | |
a4b75251 | 236 | vg = api.get_single_vg(filters={'vg_name': lv.vg_name}) |
92f5a8d4 TL |
237 | if vg: |
238 | mlogger.info('Found empty VG {}, removing'.format(vg.vg_name)) | |
239 | api.remove_vg(vg.vg_name) | |
240 | ||
f64942e4 AA |
241 | |
242 | ||
243 | def zap_raw_device(self, device): | |
244 | """ | |
245 | Any whole (raw) device passed in as input will be processed here, | |
246 | checking for LVM membership and partitions (if any). | |
247 | ||
248 | Device example: /dev/sda | |
249 | Requirements: None | |
250 | """ | |
251 | if not self.args.destroy: | |
252 | # the use of dd on a raw device causes the partition table to be | |
253 | # destroyed | |
254 | mlogger.warning( | |
255 | '--destroy was not specified, but zapping a whole device will remove the partition table' | |
256 | ) | |
257 | ||
258 | # look for partitions and zap those | |
259 | for part_name in device.sys_api.get('partitions', {}).keys(): | |
260 | self.zap_partition(Device('/dev/%s' % part_name)) | |
261 | ||
2a845540 TL |
262 | wipefs(device.path) |
263 | zap_data(device.path) | |
f64942e4 | 264 | |
1adf2230 | 265 | @decorators.needs_root |
f64942e4 AA |
266 | def zap(self, devices=None): |
267 | devices = devices or self.args.devices | |
268 | ||
269 | for device in devices: | |
2a845540 | 270 | mlogger.info("Zapping: %s", device.path) |
20effc67 | 271 | if device.is_mapper and not device.is_mpath: |
1adf2230 AA |
272 | terminal.error("Refusing to zap the mapper device: {}".format(device)) |
273 | raise SystemExit(1) | |
f64942e4 AA |
274 | if device.is_lvm_member: |
275 | self.zap_lvm_member(device) | |
276 | if device.is_lv: | |
277 | self.zap_lv(device) | |
278 | if device.is_partition: | |
279 | self.zap_partition(device) | |
280 | if device.is_device: | |
281 | self.zap_raw_device(device) | |
282 | ||
283 | if self.args.devices: | |
284 | terminal.success( | |
285 | "Zapping successful for: %s" % ", ".join([str(d) for d in self.args.devices]) | |
286 | ) | |
287 | else: | |
eafe8130 | 288 | identifier = self.args.osd_id or self.args.osd_fsid |
f64942e4 | 289 | terminal.success( |
eafe8130 | 290 | "Zapping successful for OSD: %s" % identifier |
f64942e4 AA |
291 | ) |
292 | ||
293 | @decorators.needs_root | |
294 | def zap_osd(self): | |
f91f0fd5 | 295 | if self.args.osd_id and not self.args.no_systemd: |
f64942e4 AA |
296 | osd_is_running = systemctl.osd_is_active(self.args.osd_id) |
297 | if osd_is_running: | |
298 | mlogger.error("OSD ID %s is running, stop it with:" % self.args.osd_id) | |
299 | mlogger.error("systemctl stop ceph-osd@%s" % self.args.osd_id) | |
300 | raise SystemExit("Unable to zap devices associated with OSD ID: %s" % self.args.osd_id) | |
301 | devices = find_associated_devices(self.args.osd_id, self.args.osd_fsid) | |
302 | self.zap(devices) | |
1adf2230 AA |
303 | |
304 | def dmcrypt_close(self, dmcrypt_uuid): | |
aee94f69 TL |
305 | mlogger.info("Closing encrypted volume %s", dmcrypt_uuid) |
306 | encryption.dmcrypt_close(mapping=dmcrypt_uuid, skip_path_check=True) | |
3efd9988 FG |
307 | |
308 | def main(self): | |
309 | sub_command_help = dedent(""" | |
1adf2230 | 310 | Zaps the given logical volume(s), raw device(s) or partition(s) for reuse by ceph-volume. |
b32b8144 FG |
311 | If given a path to a logical volume it must be in the format of vg/lv. Any |
312 | filesystems present on the given device, vg/lv, or partition will be removed and | |
313 | all data will be purged. | |
314 | ||
315 | If the logical volume, raw device or partition is being used for any ceph related | |
316 | mount points they will be unmounted. | |
3efd9988 FG |
317 | |
318 | However, the lv or partition will be kept intact. | |
319 | ||
320 | Example calls for supported scenarios: | |
321 | ||
322 | Zapping a logical volume: | |
323 | ||
324 | ceph-volume lvm zap {vg name/lv name} | |
325 | ||
326 | Zapping a partition: | |
327 | ||
328 | ceph-volume lvm zap /dev/sdc1 | |
329 | ||
1adf2230 AA |
330 | Zapping many raw devices: |
331 | ||
332 | ceph-volume lvm zap /dev/sda /dev/sdb /db/sdc | |
333 | ||
f64942e4 AA |
334 | Zapping devices associated with an OSD ID: |
335 | ||
336 | ceph-volume lvm zap --osd-id 1 | |
337 | ||
338 | Optionally include the OSD FSID | |
339 | ||
340 | ceph-volume lvm zap --osd-id 1 --osd-fsid 55BD4219-16A7-4037-BC20-0F158EFCC83D | |
341 | ||
b32b8144 FG |
342 | If the --destroy flag is given and you are zapping a raw device or partition |
343 | then all vgs and lvs that exist on that raw device or partition will be destroyed. | |
344 | ||
345 | This is especially useful if a raw device or partition was used by ceph-volume lvm create | |
346 | or ceph-volume lvm prepare commands previously and now you want to reuse that device. | |
347 | ||
348 | For example: | |
349 | ||
350 | ceph-volume lvm zap /dev/sda --destroy | |
351 | ||
352 | If the --destroy flag is given and you are zapping an lv then the lv is still | |
353 | kept intact for reuse. | |
354 | ||
3efd9988 FG |
355 | """) |
356 | parser = argparse.ArgumentParser( | |
357 | prog='ceph-volume lvm zap', | |
358 | formatter_class=argparse.RawDescriptionHelpFormatter, | |
359 | description=sub_command_help, | |
360 | ) | |
361 | ||
362 | parser.add_argument( | |
1adf2230 AA |
363 | 'devices', |
364 | metavar='DEVICES', | |
365 | nargs='*', | |
33c7a0ef | 366 | type=arg_validators.ValidZapDevice(gpt_ok=True), |
1adf2230 AA |
367 | default=[], |
368 | help='Path to one or many lv (as vg/lv), partition (as /dev/sda1) or device (as /dev/sda)' | |
b32b8144 | 369 | ) |
f64942e4 | 370 | |
b32b8144 FG |
371 | parser.add_argument( |
372 | '--destroy', | |
373 | action='store_true', | |
374 | default=False, | |
375 | help='Destroy all volume groups and logical volumes if you are zapping a raw device or partition', | |
3efd9988 | 376 | ) |
f64942e4 AA |
377 | |
378 | parser.add_argument( | |
379 | '--osd-id', | |
20effc67 | 380 | type=arg_validators.valid_osd_id, |
f64942e4 AA |
381 | help='Specify an OSD ID to detect associated devices for zapping', |
382 | ) | |
383 | ||
384 | parser.add_argument( | |
385 | '--osd-fsid', | |
386 | help='Specify an OSD FSID to detect associated devices for zapping', | |
387 | ) | |
388 | ||
f91f0fd5 TL |
389 | parser.add_argument( |
390 | '--no-systemd', | |
391 | dest='no_systemd', | |
392 | action='store_true', | |
393 | help='Skip systemd unit checks', | |
394 | ) | |
395 | ||
3efd9988 FG |
396 | if len(self.argv) == 0: |
397 | print(sub_command_help) | |
398 | return | |
f64942e4 AA |
399 | |
400 | self.args = parser.parse_args(self.argv) | |
401 | ||
402 | if self.args.osd_id or self.args.osd_fsid: | |
403 | self.zap_osd() | |
404 | else: | |
405 | self.zap() |