]> git.proxmox.com Git - ceph.git/blob - ceph/src/ceph-volume/ceph_volume/api/lvm.py
461e010506c2f92abce3223218e1a9164a084ec3
[ceph.git] / ceph / src / ceph-volume / ceph_volume / api / lvm.py
1 """
2 API for CRUD lvm tag operations. Follows the Ceph LVM tag naming convention
3 that prefixes tags with ``ceph.`` and uses ``=`` for assignment, and provides
4 set of utilities for interacting with LVM.
5 """
6 import logging
7 import os
8 import uuid
9 from itertools import repeat
10 from math import floor
11 from ceph_volume import process, util
12 from ceph_volume.exceptions import SizeAllocationError
13
14 logger = logging.getLogger(__name__)
15
16
17 def convert_filters_to_str(filters):
18 """
19 Convert filter args from dictionary to following format -
20 filters={filter_name=filter_val,...}
21 """
22 if not filters:
23 return filters
24
25 filter_arg = ''
26 for k, v in filters.items():
27 filter_arg += k + '=' + v + ','
28 # get rid of extra comma at the end
29 filter_arg = filter_arg[:len(filter_arg) - 1]
30
31 return filter_arg
32
33
34 def convert_tags_to_str(tags):
35 """
36 Convert tags from dictionary to following format -
37 tags={tag_name=tag_val,...}
38 """
39 if not tags:
40 return tags
41
42 tag_arg = 'tags={'
43 for k, v in tags.items():
44 tag_arg += k + '=' + v + ','
45 # get rid of extra comma at the end
46 tag_arg = tag_arg[:len(tag_arg) - 1] + '}'
47
48 return tag_arg
49
50
51 def make_filters_lvmcmd_ready(filters, tags):
52 """
53 Convert filters (including tags) from dictionary to following format -
54 filter_name=filter_val...,tags={tag_name=tag_val,...}
55
56 The command will look as follows =
57 lvs -S filter_name=filter_val...,tags={tag_name=tag_val,...}
58 """
59 filters = convert_filters_to_str(filters)
60 tags = convert_tags_to_str(tags)
61
62 if filters and tags:
63 return filters + ',' + tags
64 if filters and not tags:
65 return filters
66 if not filters and tags:
67 return tags
68 else:
69 return ''
70
71
72 def _output_parser(output, fields):
73 """
74 Newer versions of LVM allow ``--reportformat=json``, but older versions,
75 like the one included in Xenial do not. LVM has the ability to filter and
76 format its output so we assume the output will be in a format this parser
77 can handle (using ';' as a delimiter)
78
79 :param fields: A string, possibly using ',' to group many items, as it
80 would be used on the CLI
81 :param output: The CLI output from the LVM call
82 """
83 field_items = fields.split(',')
84 report = []
85 for line in output:
86 # clear the leading/trailing whitespace
87 line = line.strip()
88
89 # remove the extra '"' in each field
90 line = line.replace('"', '')
91
92 # prevent moving forward with empty contents
93 if not line:
94 continue
95
96 # splitting on ';' because that is what the lvm call uses as
97 # '--separator'
98 output_items = [i.strip() for i in line.split(';')]
99 # map the output to the fields
100 report.append(
101 dict(zip(field_items, output_items))
102 )
103
104 return report
105
106
107 def _splitname_parser(line):
108 """
109 Parses the output from ``dmsetup splitname``, that should contain prefixes
110 (--nameprefixes) and set the separator to ";"
111
112 Output for /dev/mapper/vg-lv will usually look like::
113
114 DM_VG_NAME='/dev/mapper/vg';DM_LV_NAME='lv';DM_LV_LAYER=''
115
116
117 The ``VG_NAME`` will usually not be what other callers need (e.g. just 'vg'
118 in the example), so this utility will split ``/dev/mapper/`` out, so that
119 the actual volume group name is kept
120
121 :returns: dictionary with stripped prefixes
122 """
123 parsed = {}
124 try:
125 parts = line[0].split(';')
126 except IndexError:
127 logger.exception('Unable to parse mapper device: %s', line)
128 return parsed
129
130 for part in parts:
131 part = part.replace("'", '')
132 key, value = part.split('=')
133 if 'DM_VG_NAME' in key:
134 value = value.split('/dev/mapper/')[-1]
135 key = key.split('DM_')[-1]
136 parsed[key] = value
137
138 return parsed
139
140
141 def sizing(device_size, parts=None, size=None):
142 """
143 Calculate proper sizing to fully utilize the volume group in the most
144 efficient way possible. To prevent situations where LVM might accept
145 a percentage that is beyond the vg's capabilities, it will refuse with
146 an error when requesting a larger-than-possible parameter, in addition
147 to rounding down calculations.
148
149 A dictionary with different sizing parameters is returned, to make it
150 easier for others to choose what they need in order to create logical
151 volumes::
152
153 >>> sizing(100, parts=2)
154 >>> {'parts': 2, 'percentages': 50, 'sizes': 50}
155
156 """
157 if parts is not None and size is not None:
158 raise ValueError(
159 "Cannot process sizing with both parts (%s) and size (%s)" % (parts, size)
160 )
161
162 if size and size > device_size:
163 raise SizeAllocationError(size, device_size)
164
165 def get_percentage(parts):
166 return int(floor(100 / float(parts)))
167
168 if parts is not None:
169 # Prevent parts being 0, falling back to 1 (100% usage)
170 parts = parts or 1
171 percentages = get_percentage(parts)
172
173 if size:
174 parts = int(device_size / size) or 1
175 percentages = get_percentage(parts)
176
177 sizes = device_size / parts if parts else int(floor(device_size))
178
179 return {
180 'parts': parts,
181 'percentages': percentages,
182 'sizes': int(sizes/1024/1024/1024),
183 }
184
185
186 def parse_tags(lv_tags):
187 """
188 Return a dictionary mapping of all the tags associated with
189 a Volume from the comma-separated tags coming from the LVM API
190
191 Input look like::
192
193 "ceph.osd_fsid=aaa-fff-bbbb,ceph.osd_id=0"
194
195 For the above example, the expected return value would be::
196
197 {
198 "ceph.osd_fsid": "aaa-fff-bbbb",
199 "ceph.osd_id": "0"
200 }
201 """
202 if not lv_tags:
203 return {}
204 tag_mapping = {}
205 tags = lv_tags.split(',')
206 for tag_assignment in tags:
207 if not tag_assignment.startswith('ceph.'):
208 continue
209 key, value = tag_assignment.split('=', 1)
210 tag_mapping[key] = value
211
212 return tag_mapping
213
214
215 def _vdo_parents(devices):
216 """
217 It is possible we didn't get a logical volume, or a mapper path, but
218 a device like /dev/sda2, to resolve this, we must look at all the slaves of
219 every single device in /sys/block and if any of those devices is related to
220 VDO devices, then we can add the parent
221 """
222 parent_devices = []
223 for parent in os.listdir('/sys/block'):
224 for slave in os.listdir('/sys/block/%s/slaves' % parent):
225 if slave in devices:
226 parent_devices.append('/dev/%s' % parent)
227 parent_devices.append(parent)
228 return parent_devices
229
230
231 def _vdo_slaves(vdo_names):
232 """
233 find all the slaves associated with each vdo name (from realpath) by going
234 into /sys/block/<realpath>/slaves
235 """
236 devices = []
237 for vdo_name in vdo_names:
238 mapper_path = '/dev/mapper/%s' % vdo_name
239 if not os.path.exists(mapper_path):
240 continue
241 # resolve the realpath and realname of the vdo mapper
242 vdo_realpath = os.path.realpath(mapper_path)
243 vdo_realname = vdo_realpath.split('/')[-1]
244 slaves_path = '/sys/block/%s/slaves' % vdo_realname
245 if not os.path.exists(slaves_path):
246 continue
247 devices.append(vdo_realpath)
248 devices.append(mapper_path)
249 devices.append(vdo_realname)
250 for slave in os.listdir(slaves_path):
251 devices.append('/dev/%s' % slave)
252 devices.append(slave)
253 return devices
254
255
256 def _is_vdo(path):
257 """
258 A VDO device can be composed from many different devices, go through each
259 one of those devices and its slaves (if any) and correlate them back to
260 /dev/mapper and their realpaths, and then check if they appear as part of
261 /sys/kvdo/<name>/statistics
262
263 From the realpath of a logical volume, determine if it is a VDO device or
264 not, by correlating it to the presence of the name in
265 /sys/kvdo/<name>/statistics and all the previously captured devices
266 """
267 if not os.path.isdir('/sys/kvdo'):
268 return False
269 realpath = os.path.realpath(path)
270 realpath_name = realpath.split('/')[-1]
271 devices = []
272 vdo_names = set()
273 # get all the vdo names
274 for dirname in os.listdir('/sys/kvdo/'):
275 if os.path.isdir('/sys/kvdo/%s/statistics' % dirname):
276 vdo_names.add(dirname)
277
278 # find all the slaves associated with each vdo name (from realpath) by
279 # going into /sys/block/<realpath>/slaves
280 devices.extend(_vdo_slaves(vdo_names))
281
282 # Find all possible parents, looking into slaves that are related to VDO
283 devices.extend(_vdo_parents(devices))
284
285 return any([
286 path in devices,
287 realpath in devices,
288 realpath_name in devices])
289
290
291 def is_vdo(path):
292 """
293 Detect if a path is backed by VDO, proxying the actual call to _is_vdo so
294 that we can prevent an exception breaking OSD creation. If an exception is
295 raised, it will get captured and logged to file, while returning
296 a ``False``.
297 """
298 try:
299 if _is_vdo(path):
300 return '1'
301 return '0'
302 except Exception:
303 logger.exception('Unable to properly detect device as VDO: %s', path)
304 return '0'
305
306
307 def dmsetup_splitname(dev):
308 """
309 Run ``dmsetup splitname`` and parse the results.
310
311 .. warning:: This call does not ensure that the device is correct or that
312 it exists. ``dmsetup`` will happily take a non existing path and still
313 return a 0 exit status.
314 """
315 command = [
316 'dmsetup', 'splitname', '--noheadings',
317 "--separator=';'", '--nameprefixes', dev
318 ]
319 out, err, rc = process.call(command)
320 return _splitname_parser(out)
321
322
323 def is_ceph_device(lv):
324 try:
325 lv.tags['ceph.osd_id']
326 except (KeyError, AttributeError):
327 logger.warning('device is not part of ceph: %s', lv)
328 return False
329
330 if lv.tags['ceph.osd_id'] == 'null':
331 return False
332 else:
333 return True
334
335
336 ####################################
337 #
338 # Code for LVM Physical Volumes
339 #
340 ################################
341
342 PV_FIELDS = 'pv_name,pv_tags,pv_uuid,vg_name,lv_uuid'
343
344 class PVolume(object):
345 """
346 Represents a Physical Volume from LVM, with some top-level attributes like
347 ``pv_name`` and parsed tags as a dictionary of key/value pairs.
348 """
349
350 def __init__(self, **kw):
351 for k, v in kw.items():
352 setattr(self, k, v)
353 self.pv_api = kw
354 self.name = kw['pv_name']
355 self.tags = parse_tags(kw['pv_tags'])
356
357 def __str__(self):
358 return '<%s>' % self.pv_api['pv_name']
359
360 def __repr__(self):
361 return self.__str__()
362
363 def set_tags(self, tags):
364 """
365 :param tags: A dictionary of tag names and values, like::
366
367 {
368 "ceph.osd_fsid": "aaa-fff-bbbb",
369 "ceph.osd_id": "0"
370 }
371
372 At the end of all modifications, the tags are refreshed to reflect
373 LVM's most current view.
374 """
375 for k, v in tags.items():
376 self.set_tag(k, v)
377 # after setting all the tags, refresh them for the current object, use the
378 # pv_* identifiers to filter because those shouldn't change
379 pv_object = self.get_first_pv(filter={'pv_name': self.pv_name,
380 'pv_uuid': self.pv_uuid})
381 self.tags = pv_object.tags
382
383 def set_tag(self, key, value):
384 """
385 Set the key/value pair as an LVM tag. Does not "refresh" the values of
386 the current object for its tags. Meant to be a "fire and forget" type
387 of modification.
388
389 **warning**: Altering tags on a PV has to be done ensuring that the
390 device is actually the one intended. ``pv_name`` is *not* a persistent
391 value, only ``pv_uuid`` is. Using ``pv_uuid`` is the best way to make
392 sure the device getting changed is the one needed.
393 """
394 # remove it first if it exists
395 if self.tags.get(key):
396 current_value = self.tags[key]
397 tag = "%s=%s" % (key, current_value)
398 process.call(['pvchange', '--deltag', tag, self.pv_name])
399
400 process.call(
401 [
402 'pvchange',
403 '--addtag', '%s=%s' % (key, value), self.pv_name
404 ]
405 )
406
407
408 def create_pv(device):
409 """
410 Create a physical volume from a device, useful when devices need to be later mapped
411 to journals.
412 """
413 process.run([
414 'pvcreate',
415 '-v', # verbose
416 '-f', # force it
417 '--yes', # answer yes to any prompts
418 device
419 ])
420
421
422 def remove_pv(pv_name):
423 """
424 Removes a physical volume using a double `-f` to prevent prompts and fully
425 remove anything related to LVM. This is tremendously destructive, but so is all other actions
426 when zapping a device.
427
428 In the case where multiple PVs are found, it will ignore that fact and
429 continue with the removal, specifically in the case of messages like::
430
431 WARNING: PV $UUID /dev/DEV-1 was already found on /dev/DEV-2
432
433 These situations can be avoided with custom filtering rules, which this API
434 cannot handle while accommodating custom user filters.
435 """
436 fail_msg = "Unable to remove vg %s" % pv_name
437 process.run(
438 [
439 'pvremove',
440 '-v', # verbose
441 '-f', # force it
442 '-f', # force it
443 pv_name
444 ],
445 fail_msg=fail_msg,
446 )
447
448
449 def get_pvs(fields=PV_FIELDS, filters='', tags=None):
450 """
451 Return a list of PVs that are available on the system and match the
452 filters and tags passed. Argument filters takes a dictionary containing
453 arguments required by -S option of LVM. Passing a list of LVM tags can be
454 quite tricky to pass as a dictionary within dictionary, therefore pass
455 dictionary of tags via tags argument and tricky part will be taken care of
456 by the helper methods.
457
458 :param fields: string containing list of fields to be displayed by the
459 pvs command
460 :param sep: string containing separator to be used between two fields
461 :param filters: dictionary containing LVM filters
462 :param tags: dictionary containng LVM tags
463 :returns: list of class PVolume object representing pvs on the system
464 """
465 filters = make_filters_lvmcmd_ready(filters, tags)
466 args = ['pvs', '--no-heading', '--readonly', '--separator=";"', '-S',
467 filters, '-o', fields]
468
469 stdout, stderr, returncode = process.call(args, verbose_on_failure=False)
470 pvs_report = _output_parser(stdout, fields)
471 return [PVolume(**pv_report) for pv_report in pvs_report]
472
473
474 def get_first_pv(fields=PV_FIELDS, filters=None, tags=None):
475 """
476 Wrapper of get_pv meant to be a convenience method to avoid the phrase::
477 pvs = get_pvs()
478 if len(pvs) >= 1:
479 pv = pvs[0]
480 """
481 pvs = get_pvs(fields=fields, filters=filters, tags=tags)
482 return pvs[0] if len(pvs) > 0 else []
483
484
485 ################################
486 #
487 # Code for LVM Volume Groups
488 #
489 #############################
490
491 VG_FIELDS = 'vg_name,pv_count,lv_count,vg_attr,vg_extent_count,vg_free_count,vg_extent_size'
492 VG_CMD_OPTIONS = ['--noheadings', '--readonly', '--units=b', '--nosuffix', '--separator=";"']
493
494
495 class VolumeGroup(object):
496 """
497 Represents an LVM group, with some top-level attributes like ``vg_name``
498 """
499
500 def __init__(self, **kw):
501 for k, v in kw.items():
502 setattr(self, k, v)
503 self.name = kw['vg_name']
504 if not self.name:
505 raise ValueError('VolumeGroup must have a non-empty name')
506 self.tags = parse_tags(kw.get('vg_tags', ''))
507
508 def __str__(self):
509 return '<%s>' % self.name
510
511 def __repr__(self):
512 return self.__str__()
513
514 @property
515 def free(self):
516 """
517 Return free space in VG in bytes
518 """
519 return int(self.vg_extent_size) * int(self.vg_free_count)
520
521 @property
522 def free_percent(self):
523 """
524 Return free space in VG in bytes
525 """
526 return int(self.vg_free_count) / int(self.vg_extent_count)
527
528 @property
529 def size(self):
530 """
531 Returns VG size in bytes
532 """
533 return int(self.vg_extent_size) * int(self.vg_extent_count)
534
535 def sizing(self, parts=None, size=None):
536 """
537 Calculate proper sizing to fully utilize the volume group in the most
538 efficient way possible. To prevent situations where LVM might accept
539 a percentage that is beyond the vg's capabilities, it will refuse with
540 an error when requesting a larger-than-possible parameter, in addition
541 to rounding down calculations.
542
543 A dictionary with different sizing parameters is returned, to make it
544 easier for others to choose what they need in order to create logical
545 volumes::
546
547 >>> data_vg.free
548 1024
549 >>> data_vg.sizing(parts=4)
550 {'parts': 4, 'sizes': 256, 'percentages': 25}
551 >>> data_vg.sizing(size=512)
552 {'parts': 2, 'sizes': 512, 'percentages': 50}
553
554
555 :param parts: Number of parts to create LVs from
556 :param size: Size in gigabytes to divide the VG into
557
558 :raises SizeAllocationError: When requested size cannot be allocated with
559 :raises ValueError: If both ``parts`` and ``size`` are given
560 """
561 if parts is not None and size is not None:
562 raise ValueError(
563 "Cannot process sizing with both parts (%s) and size (%s)" % (parts, size)
564 )
565
566 # if size is given we need to map that to extents so that we avoid
567 # issues when trying to get this right with a size in gigabytes find
568 # the percentage first, cheating, because these values are thrown out
569 vg_free_count = util.str_to_int(self.vg_free_count)
570
571 if size:
572 size = size * 1024 * 1024 * 1024
573 extents = int(size / int(self.vg_extent_size))
574 disk_sizing = sizing(self.free, size=size, parts=parts)
575 else:
576 if parts is not None:
577 # Prevent parts being 0, falling back to 1 (100% usage)
578 parts = parts or 1
579 size = int(self.free / parts)
580 extents = size * vg_free_count / self.free
581 disk_sizing = sizing(self.free, parts=parts)
582
583 extent_sizing = sizing(vg_free_count, size=extents)
584
585 disk_sizing['extents'] = int(extents)
586 disk_sizing['percentages'] = extent_sizing['percentages']
587 return disk_sizing
588
589 def bytes_to_extents(self, size):
590 '''
591 Return a how many extents we can fit into a size in bytes.
592 '''
593 return int(size / int(self.vg_extent_size))
594
595 def slots_to_extents(self, slots):
596 '''
597 Return how many extents fit the VG slot times
598 '''
599 return int(int(self.vg_extent_count) / slots)
600
601
602 def create_vg(devices, name=None, name_prefix=None):
603 """
604 Create a Volume Group. Command looks like::
605
606 vgcreate --force --yes group_name device
607
608 Once created the volume group is returned as a ``VolumeGroup`` object
609
610 :param devices: A list of devices to create a VG. Optionally, a single
611 device (as a string) can be used.
612 :param name: Optionally set the name of the VG, defaults to 'ceph-{uuid}'
613 :param name_prefix: Optionally prefix the name of the VG, which will get combined
614 with a UUID string
615 """
616 if isinstance(devices, set):
617 devices = list(devices)
618 if not isinstance(devices, list):
619 devices = [devices]
620 if name_prefix:
621 name = "%s-%s" % (name_prefix, str(uuid.uuid4()))
622 elif name is None:
623 name = "ceph-%s" % str(uuid.uuid4())
624 process.run([
625 'vgcreate',
626 '--force',
627 '--yes',
628 name] + devices
629 )
630
631 return get_first_vg(filters={'vg_name': name})
632
633
634 def extend_vg(vg, devices):
635 """
636 Extend a Volume Group. Command looks like::
637
638 vgextend --force --yes group_name [device, ...]
639
640 Once created the volume group is extended and returned as a ``VolumeGroup`` object
641
642 :param vg: A VolumeGroup object
643 :param devices: A list of devices to extend the VG. Optionally, a single
644 device (as a string) can be used.
645 """
646 if not isinstance(devices, list):
647 devices = [devices]
648 process.run([
649 'vgextend',
650 '--force',
651 '--yes',
652 vg.name] + devices
653 )
654
655 return get_first_vg(filters={'vg_name': vg.name})
656
657
658 def reduce_vg(vg, devices):
659 """
660 Reduce a Volume Group. Command looks like::
661
662 vgreduce --force --yes group_name [device, ...]
663
664 :param vg: A VolumeGroup object
665 :param devices: A list of devices to remove from the VG. Optionally, a
666 single device (as a string) can be used.
667 """
668 if not isinstance(devices, list):
669 devices = [devices]
670 process.run([
671 'vgreduce',
672 '--force',
673 '--yes',
674 vg.name] + devices
675 )
676
677 return get_first_vg(filter={'vg_name': vg.name})
678
679
680 def remove_vg(vg_name):
681 """
682 Removes a volume group.
683 """
684 if not vg_name:
685 logger.warning('Skipping removal of invalid VG name: "%s"', vg_name)
686 return
687 fail_msg = "Unable to remove vg %s" % vg_name
688 process.run(
689 [
690 'vgremove',
691 '-v', # verbose
692 '-f', # force it
693 vg_name
694 ],
695 fail_msg=fail_msg,
696 )
697
698
699 def get_vgs(fields=VG_FIELDS, filters='', tags=None):
700 """
701 Return a list of VGs that are available on the system and match the
702 filters and tags passed. Argument filters takes a dictionary containing
703 arguments required by -S option of LVM. Passing a list of LVM tags can be
704 quite tricky to pass as a dictionary within dictionary, therefore pass
705 dictionary of tags via tags argument and tricky part will be taken care of
706 by the helper methods.
707
708 :param fields: string containing list of fields to be displayed by the
709 vgs command
710 :param sep: string containing separator to be used between two fields
711 :param filters: dictionary containing LVM filters
712 :param tags: dictionary containng LVM tags
713 :returns: list of class VolumeGroup object representing vgs on the system
714 """
715 filters = make_filters_lvmcmd_ready(filters, tags)
716 args = ['vgs'] + VG_CMD_OPTIONS + ['-S', filters, '-o', fields]
717
718 stdout, stderr, returncode = process.call(args, verbose_on_failure=False)
719 vgs_report =_output_parser(stdout, fields)
720 return [VolumeGroup(**vg_report) for vg_report in vgs_report]
721
722
723 def get_first_vg(fields=VG_FIELDS, filters=None, tags=None):
724 """
725 Wrapper of get_vg meant to be a convenience method to avoid the phrase::
726 vgs = get_vgs()
727 if len(vgs) >= 1:
728 vg = vgs[0]
729 """
730 vgs = get_vgs(fields=fields, filters=filters, tags=tags)
731 return vgs[0] if len(vgs) > 0 else []
732
733
734 def get_device_vgs(device, name_prefix=''):
735 stdout, stderr, returncode = process.call(
736 ['pvs'] + VG_CMD_OPTIONS + ['-o', VG_FIELDS, device],
737 verbose_on_failure=False
738 )
739 vgs = _output_parser(stdout, VG_FIELDS)
740 return [VolumeGroup(**vg) for vg in vgs if vg['vg_name'] and vg['vg_name'].startswith(name_prefix)]
741
742
743 #################################
744 #
745 # Code for LVM Logical Volumes
746 #
747 ###############################
748
749 LV_FIELDS = 'lv_tags,lv_path,lv_name,vg_name,lv_uuid,lv_size'
750 LV_CMD_OPTIONS = ['--noheadings', '--readonly', '--separator=";"', '-a',
751 '--units=b', '--nosuffix']
752
753
754 class Volume(object):
755 """
756 Represents a Logical Volume from LVM, with some top-level attributes like
757 ``lv_name`` and parsed tags as a dictionary of key/value pairs.
758 """
759
760 def __init__(self, **kw):
761 for k, v in kw.items():
762 setattr(self, k, v)
763 self.lv_api = kw
764 self.name = kw['lv_name']
765 if not self.name:
766 raise ValueError('Volume must have a non-empty name')
767 self.tags = parse_tags(kw['lv_tags'])
768 self.encrypted = self.tags.get('ceph.encrypted', '0') == '1'
769 self.used_by_ceph = 'ceph.osd_id' in self.tags
770
771 def __str__(self):
772 return '<%s>' % self.lv_api['lv_path']
773
774 def __repr__(self):
775 return self.__str__()
776
777 def as_dict(self):
778 obj = {}
779 obj.update(self.lv_api)
780 obj['tags'] = self.tags
781 obj['name'] = self.name
782 obj['type'] = self.tags['ceph.type']
783 obj['path'] = self.lv_path
784 return obj
785
786 def report(self):
787 if not self.used_by_ceph:
788 return {
789 'name': self.lv_name,
790 'comment': 'not used by ceph'
791 }
792 else:
793 type_ = self.tags['ceph.type']
794 report = {
795 'name': self.lv_name,
796 'osd_id': self.tags['ceph.osd_id'],
797 'cluster_name': self.tags['ceph.cluster_name'],
798 'type': type_,
799 'osd_fsid': self.tags['ceph.osd_fsid'],
800 'cluster_fsid': self.tags['ceph.cluster_fsid'],
801 'osdspec_affinity': self.tags.get('ceph.osdspec_affinity', ''),
802 }
803 type_uuid = '{}_uuid'.format(type_)
804 report[type_uuid] = self.tags['ceph.{}'.format(type_uuid)]
805 return report
806
807 def _format_tag_args(self, op, tags):
808 tag_args = ['{}={}'.format(k, v) for k, v in tags.items()]
809 # weird but efficient way of ziping two lists and getting a flat list
810 return list(sum(zip(repeat(op), tag_args), ()))
811
812 def clear_tags(self, keys=None):
813 """
814 Removes all or passed tags from the Logical Volume.
815 """
816 if not keys:
817 keys = self.tags.keys()
818
819 del_tags = {k: self.tags[k] for k in keys if k in self.tags}
820 if not del_tags:
821 # nothing to clear
822 return
823 del_tag_args = self._format_tag_args('--deltag', del_tags)
824 # --deltag returns successful even if the to be deleted tag is not set
825 process.call(['lvchange'] + del_tag_args + [self.lv_path])
826 for k in del_tags.keys():
827 del self.tags[k]
828
829
830 def set_tags(self, tags):
831 """
832 :param tags: A dictionary of tag names and values, like::
833
834 {
835 "ceph.osd_fsid": "aaa-fff-bbbb",
836 "ceph.osd_id": "0"
837 }
838
839 At the end of all modifications, the tags are refreshed to reflect
840 LVM's most current view.
841 """
842 self.clear_tags(tags.keys())
843 add_tag_args = self._format_tag_args('--addtag', tags)
844 process.call(['lvchange'] + add_tag_args + [self.lv_path])
845 for k, v in tags.items():
846 self.tags[k] = v
847
848
849 def clear_tag(self, key):
850 if self.tags.get(key):
851 current_value = self.tags[key]
852 tag = "%s=%s" % (key, current_value)
853 process.call(['lvchange', '--deltag', tag, self.lv_path])
854 del self.tags[key]
855
856
857 def set_tag(self, key, value):
858 """
859 Set the key/value pair as an LVM tag.
860 """
861 # remove it first if it exists
862 self.clear_tag(key)
863
864 process.call(
865 [
866 'lvchange',
867 '--addtag', '%s=%s' % (key, value), self.lv_path
868 ]
869 )
870 self.tags[key] = value
871
872 def deactivate(self):
873 """
874 Deactivate the LV by calling lvchange -an
875 """
876 process.call(['lvchange', '-an', self.lv_path])
877
878
879 def create_lv(name_prefix,
880 uuid,
881 vg=None,
882 device=None,
883 slots=None,
884 extents=None,
885 size=None,
886 tags=None):
887 """
888 Create a Logical Volume in a Volume Group. Command looks like::
889
890 lvcreate -L 50G -n gfslv vg0
891
892 ``name_prefix`` is required. If ``size`` is provided its expected to be a
893 byte count. Tags are an optional dictionary and is expected to
894 conform to the convention of prefixing them with "ceph." like::
895
896 {"ceph.block_device": "/dev/ceph/osd-1"}
897
898 :param name_prefix: name prefix for the LV, typically somehting like ceph-osd-block
899 :param uuid: UUID to ensure uniqueness; is combined with name_prefix to
900 form the LV name
901 :param vg: optional, pass an existing VG to create LV
902 :param device: optional, device to use. Either device of vg must be passed
903 :param slots: optional, number of slots to divide vg up, LV will occupy one
904 one slot if enough space is available
905 :param extends: optional, how many lvm extends to use, supersedes slots
906 :param size: optional, target LV size in bytes, supersedes extents,
907 resulting LV might be smaller depending on extent
908 size of the underlying VG
909 :param tags: optional, a dict of lvm tags to set on the LV
910 """
911 name = '{}-{}'.format(name_prefix, uuid)
912 if not vg:
913 if not device:
914 raise RuntimeError("Must either specify vg or device, none given")
915 # check if a vgs starting with ceph already exists
916 vgs = get_device_vgs(device, 'ceph')
917 if vgs:
918 vg = vgs[0]
919 else:
920 # create on if not
921 vg = create_vg(device, name_prefix='ceph')
922 assert(vg)
923
924 if size:
925 extents = vg.bytes_to_extents(size)
926 logger.debug('size was passed: {} -> {}'.format(size, extents))
927 elif slots and not extents:
928 extents = vg.slots_to_extents(slots)
929 logger.debug('slots was passed: {} -> {}'.format(slots, extents))
930
931 if extents:
932 command = [
933 'lvcreate',
934 '--yes',
935 '-l',
936 '{}'.format(extents),
937 '-n', name, vg.vg_name
938 ]
939 # create the lv with all the space available, this is needed because the
940 # system call is different for LVM
941 else:
942 command = [
943 'lvcreate',
944 '--yes',
945 '-l',
946 '100%FREE',
947 '-n', name, vg.vg_name
948 ]
949 process.run(command)
950
951 lv = get_first_lv(filters={'lv_name': name, 'vg_name': vg.vg_name})
952
953 if tags is None:
954 tags = {
955 "ceph.osd_id": "null",
956 "ceph.type": "null",
957 "ceph.cluster_fsid": "null",
958 "ceph.osd_fsid": "null",
959 }
960 # when creating a distinct type, the caller doesn't know what the path will
961 # be so this function will set it after creation using the mapping
962 # XXX add CEPH_VOLUME_LVM_DEBUG to enable -vvvv on lv operations
963 type_path_tag = {
964 'journal': 'ceph.journal_device',
965 'data': 'ceph.data_device',
966 'block': 'ceph.block_device',
967 'wal': 'ceph.wal_device',
968 'db': 'ceph.db_device',
969 'lockbox': 'ceph.lockbox_device', # XXX might not ever need this lockbox sorcery
970 }
971 path_tag = type_path_tag.get(tags.get('ceph.type'))
972 if path_tag:
973 tags.update({path_tag: lv.lv_path})
974
975 lv.set_tags(tags)
976
977 return lv
978
979
980 def create_lvs(volume_group, parts=None, size=None, name_prefix='ceph-lv'):
981 """
982 Create multiple Logical Volumes from a Volume Group by calculating the
983 proper extents from ``parts`` or ``size``. A custom prefix can be used
984 (defaults to ``ceph-lv``), these names are always suffixed with a uuid.
985
986 LV creation in ceph-volume will require tags, this is expected to be
987 pre-computed by callers who know Ceph metadata like OSD IDs and FSIDs. It
988 will probably not be the case when mass-creating LVs, so common/default
989 tags will be set to ``"null"``.
990
991 .. note:: LVs that are not in use can be detected by querying LVM for tags that are
992 set to ``"null"``.
993
994 :param volume_group: The volume group (vg) to use for LV creation
995 :type group: ``VolumeGroup()`` object
996 :param parts: Number of LVs to create *instead of* ``size``.
997 :type parts: int
998 :param size: Size (in gigabytes) of LVs to create, e.g. "as many 10gb LVs as possible"
999 :type size: int
1000 :param extents: The number of LVM extents to use to create the LV. Useful if looking to have
1001 accurate LV sizes (LVM rounds sizes otherwise)
1002 """
1003 if parts is None and size is None:
1004 # fallback to just one part (using 100% of the vg)
1005 parts = 1
1006 lvs = []
1007 tags = {
1008 "ceph.osd_id": "null",
1009 "ceph.type": "null",
1010 "ceph.cluster_fsid": "null",
1011 "ceph.osd_fsid": "null",
1012 }
1013 sizing = volume_group.sizing(parts=parts, size=size)
1014 for part in range(0, sizing['parts']):
1015 size = sizing['sizes']
1016 extents = sizing['extents']
1017 lvs.append(
1018 create_lv(name_prefix, uuid.uuid4(), vg=volume_group, extents=extents, tags=tags)
1019 )
1020 return lvs
1021
1022
1023 def remove_lv(lv):
1024 """
1025 Removes a logical volume given it's absolute path.
1026
1027 Will return True if the lv is successfully removed or
1028 raises a RuntimeError if the removal fails.
1029
1030 :param lv: A ``Volume`` object or the path for an LV
1031 """
1032 if isinstance(lv, Volume):
1033 path = lv.lv_path
1034 else:
1035 path = lv
1036
1037 stdout, stderr, returncode = process.call(
1038 [
1039 'lvremove',
1040 '-v', # verbose
1041 '-f', # force it
1042 path
1043 ],
1044 show_command=True,
1045 terminal_verbose=True,
1046 )
1047 if returncode != 0:
1048 raise RuntimeError("Unable to remove %s" % path)
1049 return True
1050
1051
1052 def get_lvs(fields=LV_FIELDS, filters='', tags=None):
1053 """
1054 Return a list of LVs that are available on the system and match the
1055 filters and tags passed. Argument filters takes a dictionary containing
1056 arguments required by -S option of LVM. Passing a list of LVM tags can be
1057 quite tricky to pass as a dictionary within dictionary, therefore pass
1058 dictionary of tags via tags argument and tricky part will be taken care of
1059 by the helper methods.
1060
1061 :param fields: string containing list of fields to be displayed by the
1062 lvs command
1063 :param sep: string containing separator to be used between two fields
1064 :param filters: dictionary containing LVM filters
1065 :param tags: dictionary containng LVM tags
1066 :returns: list of class Volume object representing LVs on the system
1067 """
1068 filters = make_filters_lvmcmd_ready(filters, tags)
1069 args = ['lvs'] + LV_CMD_OPTIONS + ['-S', filters, '-o', fields]
1070
1071 stdout, stderr, returncode = process.call(args, verbose_on_failure=False)
1072 lvs_report = _output_parser(stdout, fields)
1073 return [Volume(**lv_report) for lv_report in lvs_report]
1074
1075
1076 def get_first_lv(fields=LV_FIELDS, filters=None, tags=None):
1077 """
1078 Wrapper of get_lv meant to be a convenience method to avoid the phrase::
1079 lvs = get_lvs()
1080 if len(lvs) >= 1:
1081 lv = lvs[0]
1082 """
1083 lvs = get_lvs(fields=fields, filters=filters, tags=tags)
1084 return lvs[0] if len(lvs) > 0 else []
1085
1086
1087 def get_lv_by_name(name):
1088 stdout, stderr, returncode = process.call(
1089 ['lvs', '--noheadings', '-o', LV_FIELDS, '-S',
1090 'lv_name={}'.format(name)],
1091 verbose_on_failure=False
1092 )
1093 lvs = _output_parser(stdout, LV_FIELDS)
1094 return [Volume(**lv) for lv in lvs]
1095
1096
1097 def get_lvs_by_tag(lv_tag):
1098 stdout, stderr, returncode = process.call(
1099 ['lvs', '--noheadings', '--separator=";"', '-a', '-o', LV_FIELDS, '-S',
1100 'lv_tags={{{}}}'.format(lv_tag)],
1101 verbose_on_failure=False
1102 )
1103 lvs = _output_parser(stdout, LV_FIELDS)
1104 return [Volume(**lv) for lv in lvs]
1105
1106
1107 def get_device_lvs(device, name_prefix=''):
1108 stdout, stderr, returncode = process.call(
1109 ['pvs'] + LV_CMD_OPTIONS + ['-o', LV_FIELDS, device],
1110 verbose_on_failure=False
1111 )
1112 lvs = _output_parser(stdout, LV_FIELDS)
1113 return [Volume(**lv) for lv in lvs if lv['lv_name'] and
1114 lv['lv_name'].startswith(name_prefix)]