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