]> git.proxmox.com Git - ceph.git/blob - ceph/src/ceph-volume/ceph_volume/devices/lvm/zap.py
63624e55f5bd228b912bc5eaf09fa39944ea115f
[ceph.git] / ceph / src / ceph-volume / ceph_volume / devices / lvm / zap.py
1 import argparse
2 import os
3 import logging
4 import time
5
6 from textwrap import dedent
7
8 from ceph_volume import decorators, terminal, process
9 from ceph_volume.api import lvm as api
10 from ceph_volume.util import system, encryption, disk, arg_validators, str_to_int, merge_dict
11 from ceph_volume.util.device import Device
12 from ceph_volume.systemd import systemctl
13
14 logger = logging.getLogger(__name__)
15 mlogger = terminal.MultiLogger(__name__)
16
17
18 def wipefs(path):
19 """
20 Removes the filesystem from an lv or partition.
21
22 Environment variables supported::
23
24 * ``CEPH_VOLUME_WIPEFS_TRIES``: Defaults to 8
25 * ``CEPH_VOLUME_WIPEFS_INTERVAL``: Defaults to 5
26
27 """
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)
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',
69 'conv=fsync'
70 ])
71
72
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
84
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)
89
90 devices_to_zap = ensure_associated_lvs(lvs, lv_tags)
91 return [Device(path) for path in set(devices_to_zap) if path]
92
93
94 def ensure_associated_lvs(lvs, lv_tags={}):
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
103
104 journal_lvs = api.get_lvs(tags=merge_dict(lv_tags, {'ceph.type': 'journal'}))
105 db_lvs = api.get_lvs(tags=merge_dict(lv_tags, {'ceph.type': 'db'}))
106 wal_lvs = api.get_lvs(tags=merge_dict(lv_tags, {'ceph.type': 'wal'}))
107 backing_devices = [(journal_lvs, 'journal'), (db_lvs, 'db'),
108 (wal_lvs, 'wal')]
109
110 verified_devices = []
111
112 for lv in lvs:
113 # go through each lv and append it, otherwise query `blkid` to find
114 # a physical device. Do this for each type (journal,db,wal) regardless
115 # if they have been processed in the previous LV, so that bad devices
116 # with the same ID can be caught
117 for ceph_lvs, _type in backing_devices:
118 if ceph_lvs:
119 verified_devices.extend([l.lv_path for l in ceph_lvs])
120 continue
121
122 # must be a disk partition, by querying blkid by the uuid we are
123 # ensuring that the device path is always correct
124 try:
125 device_uuid = lv.tags['ceph.%s_uuid' % _type]
126 except KeyError:
127 # Bluestore will not have ceph.journal_uuid, and Filestore
128 # will not not have ceph.db_uuid
129 continue
130
131 osd_device = disk.get_device_from_partuuid(device_uuid)
132 if not osd_device:
133 # if the osd_device is not found by the partuuid, then it is
134 # not possible to ensure this device exists anymore, so skip it
135 continue
136 verified_devices.append(osd_device)
137
138 verified_devices.append(lv.lv_path)
139
140 # reduce the list from all the duplicates that were added
141 return list(set(verified_devices))
142
143
144 class Zap(object):
145
146 help = 'Removes all data and filesystems from a logical volume or partition.'
147
148 def __init__(self, argv):
149 self.argv = argv
150
151 def unmount_lv(self, lv):
152 if lv.tags.get('ceph.cluster_name') and lv.tags.get('ceph.osd_id'):
153 lv_path = "/var/lib/ceph/osd/{}-{}".format(lv.tags['ceph.cluster_name'], lv.tags['ceph.osd_id'])
154 else:
155 lv_path = lv.lv_path
156 dmcrypt_uuid = lv.lv_uuid
157 dmcrypt = lv.encrypted
158 if system.path_is_mounted(lv_path):
159 mlogger.info("Unmounting %s", lv_path)
160 system.unmount(lv_path)
161 if dmcrypt and dmcrypt_uuid:
162 self.dmcrypt_close(dmcrypt_uuid)
163
164 def zap_lv(self, device):
165 """
166 Device examples: vg-name/lv-name, /dev/vg-name/lv-name
167 Requirements: Must be a logical volume (LV)
168 """
169 lv = api.get_first_lv(filters={'lv_name': device.lv_name, 'vg_name':
170 device.vg_name})
171 self.unmount_lv(lv)
172
173 wipefs(device.abspath)
174 zap_data(device.abspath)
175
176 if self.args.destroy:
177 lvs = api.get_lvs(filters={'vg_name': device.vg_name})
178 if lvs == []:
179 mlogger.info('No LVs left, exiting', device.vg_name)
180 return
181 elif len(lvs) <= 1:
182 mlogger.info('Only 1 LV left in VG, will proceed to destroy '
183 'volume group %s', device.vg_name)
184 api.remove_vg(device.vg_name)
185 else:
186 mlogger.info('More than 1 LV left in VG, will proceed to '
187 'destroy LV only')
188 mlogger.info('Removing LV because --destroy was given: %s',
189 device.abspath)
190 api.remove_lv(device.abspath)
191 elif lv:
192 # just remove all lvm metadata, leaving the LV around
193 lv.clear_tags()
194
195 def zap_partition(self, device):
196 """
197 Device example: /dev/sda1
198 Requirements: Must be a partition
199 """
200 if device.is_encrypted:
201 # find the holder
202 holders = [
203 '/dev/%s' % holder for holder in device.sys_api.get('holders', [])
204 ]
205 for mapper_uuid in os.listdir('/dev/mapper'):
206 mapper_path = os.path.join('/dev/mapper', mapper_uuid)
207 if os.path.realpath(mapper_path) in holders:
208 self.dmcrypt_close(mapper_uuid)
209
210 if system.device_is_mounted(device.abspath):
211 mlogger.info("Unmounting %s", device.abspath)
212 system.unmount(device.abspath)
213
214 wipefs(device.abspath)
215 zap_data(device.abspath)
216
217 if self.args.destroy:
218 mlogger.info("Destroying partition since --destroy was used: %s" % device.abspath)
219 disk.remove_partition(device)
220
221 def zap_lvm_member(self, device):
222 """
223 An LVM member may have more than one LV and or VG, for example if it is
224 a raw device with multiple partitions each belonging to a different LV
225
226 Device example: /dev/sda
227 Requirements: An LV or VG present in the device, making it an LVM member
228 """
229 for lv in device.lvs:
230 if lv.lv_name:
231 mlogger.info('Zapping lvm member {}. lv_path is {}'.format(device.abspath, lv.lv_path))
232 self.zap_lv(Device(lv.lv_path))
233 else:
234 vg = api.get_first_vg(filters={'vg_name': lv.vg_name})
235 if vg:
236 mlogger.info('Found empty VG {}, removing'.format(vg.vg_name))
237 api.remove_vg(vg.vg_name)
238
239
240
241 def zap_raw_device(self, device):
242 """
243 Any whole (raw) device passed in as input will be processed here,
244 checking for LVM membership and partitions (if any).
245
246 Device example: /dev/sda
247 Requirements: None
248 """
249 if not self.args.destroy:
250 # the use of dd on a raw device causes the partition table to be
251 # destroyed
252 mlogger.warning(
253 '--destroy was not specified, but zapping a whole device will remove the partition table'
254 )
255
256 # look for partitions and zap those
257 for part_name in device.sys_api.get('partitions', {}).keys():
258 self.zap_partition(Device('/dev/%s' % part_name))
259
260 wipefs(device.abspath)
261 zap_data(device.abspath)
262
263 @decorators.needs_root
264 def zap(self, devices=None):
265 devices = devices or self.args.devices
266
267 for device in devices:
268 mlogger.info("Zapping: %s", device.abspath)
269 if device.is_mapper:
270 terminal.error("Refusing to zap the mapper device: {}".format(device))
271 raise SystemExit(1)
272 if device.is_lvm_member:
273 self.zap_lvm_member(device)
274 if device.is_lv:
275 self.zap_lv(device)
276 if device.is_partition:
277 self.zap_partition(device)
278 if device.is_device:
279 self.zap_raw_device(device)
280
281 if self.args.devices:
282 terminal.success(
283 "Zapping successful for: %s" % ", ".join([str(d) for d in self.args.devices])
284 )
285 else:
286 identifier = self.args.osd_id or self.args.osd_fsid
287 terminal.success(
288 "Zapping successful for OSD: %s" % identifier
289 )
290
291 @decorators.needs_root
292 def zap_osd(self):
293 if self.args.osd_id:
294 osd_is_running = systemctl.osd_is_active(self.args.osd_id)
295 if osd_is_running:
296 mlogger.error("OSD ID %s is running, stop it with:" % self.args.osd_id)
297 mlogger.error("systemctl stop ceph-osd@%s" % self.args.osd_id)
298 raise SystemExit("Unable to zap devices associated with OSD ID: %s" % self.args.osd_id)
299 devices = find_associated_devices(self.args.osd_id, self.args.osd_fsid)
300 self.zap(devices)
301
302 def dmcrypt_close(self, dmcrypt_uuid):
303 dmcrypt_path = "/dev/mapper/{}".format(dmcrypt_uuid)
304 mlogger.info("Closing encrypted path %s", dmcrypt_path)
305 encryption.dmcrypt_close(dmcrypt_path)
306
307 def main(self):
308 sub_command_help = dedent("""
309 Zaps the given logical volume(s), raw device(s) or partition(s) for reuse by ceph-volume.
310 If given a path to a logical volume it must be in the format of vg/lv. Any
311 filesystems present on the given device, vg/lv, or partition will be removed and
312 all data will be purged.
313
314 If the logical volume, raw device or partition is being used for any ceph related
315 mount points they will be unmounted.
316
317 However, the lv or partition will be kept intact.
318
319 Example calls for supported scenarios:
320
321 Zapping a logical volume:
322
323 ceph-volume lvm zap {vg name/lv name}
324
325 Zapping a partition:
326
327 ceph-volume lvm zap /dev/sdc1
328
329 Zapping many raw devices:
330
331 ceph-volume lvm zap /dev/sda /dev/sdb /db/sdc
332
333 Zapping devices associated with an OSD ID:
334
335 ceph-volume lvm zap --osd-id 1
336
337 Optionally include the OSD FSID
338
339 ceph-volume lvm zap --osd-id 1 --osd-fsid 55BD4219-16A7-4037-BC20-0F158EFCC83D
340
341 If the --destroy flag is given and you are zapping a raw device or partition
342 then all vgs and lvs that exist on that raw device or partition will be destroyed.
343
344 This is especially useful if a raw device or partition was used by ceph-volume lvm create
345 or ceph-volume lvm prepare commands previously and now you want to reuse that device.
346
347 For example:
348
349 ceph-volume lvm zap /dev/sda --destroy
350
351 If the --destroy flag is given and you are zapping an lv then the lv is still
352 kept intact for reuse.
353
354 """)
355 parser = argparse.ArgumentParser(
356 prog='ceph-volume lvm zap',
357 formatter_class=argparse.RawDescriptionHelpFormatter,
358 description=sub_command_help,
359 )
360
361 parser.add_argument(
362 'devices',
363 metavar='DEVICES',
364 nargs='*',
365 type=arg_validators.ValidDevice(gpt_ok=True),
366 default=[],
367 help='Path to one or many lv (as vg/lv), partition (as /dev/sda1) or device (as /dev/sda)'
368 )
369
370 parser.add_argument(
371 '--destroy',
372 action='store_true',
373 default=False,
374 help='Destroy all volume groups and logical volumes if you are zapping a raw device or partition',
375 )
376
377 parser.add_argument(
378 '--osd-id',
379 help='Specify an OSD ID to detect associated devices for zapping',
380 )
381
382 parser.add_argument(
383 '--osd-fsid',
384 help='Specify an OSD FSID to detect associated devices for zapping',
385 )
386
387 if len(self.argv) == 0:
388 print(sub_command_help)
389 return
390
391 self.args = parser.parse_args(self.argv)
392
393 if self.args.osd_id or self.args.osd_fsid:
394 self.zap_osd()
395 else:
396 self.zap()