]> git.proxmox.com Git - ceph.git/blob - ceph/src/ceph-volume/ceph_volume/api/lvm.py
import 15.2.0 Octopus source
[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 # splitting 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 fields
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 parsed = {}
71 try:
72 parts = line[0].split(';')
73 except IndexError:
74 logger.exception('Unable to parse mapper device: %s', line)
75 return parsed
76
77 for part in parts:
78 part = part.replace("'", '')
79 key, value = part.split('=')
80 if 'DM_VG_NAME' in key:
81 value = value.split('/dev/mapper/')[-1]
82 key = key.split('DM_')[-1]
83 parsed[key] = value
84
85 return parsed
86
87
88 def sizing(device_size, parts=None, size=None):
89 """
90 Calculate proper sizing to fully utilize the volume group in the most
91 efficient way possible. To prevent situations where LVM might accept
92 a percentage that is beyond the vg's capabilities, it will refuse with
93 an error when requesting a larger-than-possible parameter, in addition
94 to rounding down calculations.
95
96 A dictionary with different sizing parameters is returned, to make it
97 easier for others to choose what they need in order to create logical
98 volumes::
99
100 >>> sizing(100, parts=2)
101 >>> {'parts': 2, 'percentages': 50, 'sizes': 50}
102
103 """
104 if parts is not None and size is not None:
105 raise ValueError(
106 "Cannot process sizing with both parts (%s) and size (%s)" % (parts, size)
107 )
108
109 if size and size > device_size:
110 raise SizeAllocationError(size, device_size)
111
112 def get_percentage(parts):
113 return int(floor(100 / float(parts)))
114
115 if parts is not None:
116 # Prevent parts being 0, falling back to 1 (100% usage)
117 parts = parts or 1
118 percentages = get_percentage(parts)
119
120 if size:
121 parts = int(device_size / size) or 1
122 percentages = get_percentage(parts)
123
124 sizes = device_size / parts if parts else int(floor(device_size))
125
126 return {
127 'parts': parts,
128 'percentages': percentages,
129 'sizes': int(sizes/1024/1024/1024),
130 }
131
132
133 def parse_tags(lv_tags):
134 """
135 Return a dictionary mapping of all the tags associated with
136 a Volume from the comma-separated tags coming from the LVM API
137
138 Input look like::
139
140 "ceph.osd_fsid=aaa-fff-bbbb,ceph.osd_id=0"
141
142 For the above example, the expected return value would be::
143
144 {
145 "ceph.osd_fsid": "aaa-fff-bbbb",
146 "ceph.osd_id": "0"
147 }
148 """
149 if not lv_tags:
150 return {}
151 tag_mapping = {}
152 tags = lv_tags.split(',')
153 for tag_assignment in tags:
154 if not tag_assignment.startswith('ceph.'):
155 continue
156 key, value = tag_assignment.split('=', 1)
157 tag_mapping[key] = value
158
159 return tag_mapping
160
161
162 def _vdo_parents(devices):
163 """
164 It is possible we didn't get a logical volume, or a mapper path, but
165 a device like /dev/sda2, to resolve this, we must look at all the slaves of
166 every single device in /sys/block and if any of those devices is related to
167 VDO devices, then we can add the parent
168 """
169 parent_devices = []
170 for parent in os.listdir('/sys/block'):
171 for slave in os.listdir('/sys/block/%s/slaves' % parent):
172 if slave in devices:
173 parent_devices.append('/dev/%s' % parent)
174 parent_devices.append(parent)
175 return parent_devices
176
177
178 def _vdo_slaves(vdo_names):
179 """
180 find all the slaves associated with each vdo name (from realpath) by going
181 into /sys/block/<realpath>/slaves
182 """
183 devices = []
184 for vdo_name in vdo_names:
185 mapper_path = '/dev/mapper/%s' % vdo_name
186 if not os.path.exists(mapper_path):
187 continue
188 # resolve the realpath and realname of the vdo mapper
189 vdo_realpath = os.path.realpath(mapper_path)
190 vdo_realname = vdo_realpath.split('/')[-1]
191 slaves_path = '/sys/block/%s/slaves' % vdo_realname
192 if not os.path.exists(slaves_path):
193 continue
194 devices.append(vdo_realpath)
195 devices.append(mapper_path)
196 devices.append(vdo_realname)
197 for slave in os.listdir(slaves_path):
198 devices.append('/dev/%s' % slave)
199 devices.append(slave)
200 return devices
201
202
203 def _is_vdo(path):
204 """
205 A VDO device can be composed from many different devices, go through each
206 one of those devices and its slaves (if any) and correlate them back to
207 /dev/mapper and their realpaths, and then check if they appear as part of
208 /sys/kvdo/<name>/statistics
209
210 From the realpath of a logical volume, determine if it is a VDO device or
211 not, by correlating it to the presence of the name in
212 /sys/kvdo/<name>/statistics and all the previously captured devices
213 """
214 if not os.path.isdir('/sys/kvdo'):
215 return False
216 realpath = os.path.realpath(path)
217 realpath_name = realpath.split('/')[-1]
218 devices = []
219 vdo_names = set()
220 # get all the vdo names
221 for dirname in os.listdir('/sys/kvdo/'):
222 if os.path.isdir('/sys/kvdo/%s/statistics' % dirname):
223 vdo_names.add(dirname)
224
225 # find all the slaves associated with each vdo name (from realpath) by
226 # going into /sys/block/<realpath>/slaves
227 devices.extend(_vdo_slaves(vdo_names))
228
229 # Find all possible parents, looking into slaves that are related to VDO
230 devices.extend(_vdo_parents(devices))
231
232 return any([
233 path in devices,
234 realpath in devices,
235 realpath_name in devices])
236
237
238 def is_vdo(path):
239 """
240 Detect if a path is backed by VDO, proxying the actual call to _is_vdo so
241 that we can prevent an exception breaking OSD creation. If an exception is
242 raised, it will get captured and logged to file, while returning
243 a ``False``.
244 """
245 try:
246 if _is_vdo(path):
247 return '1'
248 return '0'
249 except Exception:
250 logger.exception('Unable to properly detect device as VDO: %s', path)
251 return '0'
252
253
254 def dmsetup_splitname(dev):
255 """
256 Run ``dmsetup splitname`` and parse the results.
257
258 .. warning:: This call does not ensure that the device is correct or that
259 it exists. ``dmsetup`` will happily take a non existing path and still
260 return a 0 exit status.
261 """
262 command = [
263 'dmsetup', 'splitname', '--noheadings',
264 "--separator=';'", '--nameprefixes', dev
265 ]
266 out, err, rc = process.call(command)
267 return _splitname_parser(out)
268
269
270 def is_ceph_device(lv):
271 try:
272 lv.tags['ceph.osd_id']
273 except (KeyError, AttributeError):
274 logger.warning('device is not part of ceph: %s', lv)
275 return False
276
277 if lv.tags['ceph.osd_id'] == 'null':
278 return False
279 else:
280 return True
281
282
283 ####################################
284 #
285 # Code for LVM Physical Volumes
286 #
287 ################################
288
289 PV_FIELDS = 'pv_name,pv_tags,pv_uuid,vg_name,lv_uuid'
290
291 def get_api_pvs():
292 """
293 Return the list of physical volumes configured for lvm and available in the
294 system using flags to include common metadata associated with them like the uuid
295
296 This will only return physical volumes set up to work with LVM.
297
298 Command and delimited output should look like::
299
300 $ pvs --noheadings --readonly --separator=';' -o pv_name,pv_tags,pv_uuid
301 /dev/sda1;;
302 /dev/sdv;;07A4F654-4162-4600-8EB3-88D1E42F368D
303
304 """
305 stdout, stderr, returncode = process.call(
306 ['pvs', '--no-heading', '--readonly', '--separator=";"', '-o',
307 PV_FIELDS],
308 verbose_on_failure=False
309 )
310
311 return _output_parser(stdout, PV_FIELDS)
312
313
314 class PVolume(object):
315 """
316 Represents a Physical Volume from LVM, with some top-level attributes like
317 ``pv_name`` and parsed tags as a dictionary of key/value pairs.
318 """
319
320 def __init__(self, **kw):
321 for k, v in kw.items():
322 setattr(self, k, v)
323 self.pv_api = kw
324 self.name = kw['pv_name']
325 self.tags = parse_tags(kw['pv_tags'])
326
327 def __str__(self):
328 return '<%s>' % self.pv_api['pv_name']
329
330 def __repr__(self):
331 return self.__str__()
332
333 def set_tags(self, tags):
334 """
335 :param tags: A dictionary of tag names and values, like::
336
337 {
338 "ceph.osd_fsid": "aaa-fff-bbbb",
339 "ceph.osd_id": "0"
340 }
341
342 At the end of all modifications, the tags are refreshed to reflect
343 LVM's most current view.
344 """
345 for k, v in tags.items():
346 self.set_tag(k, v)
347 # after setting all the tags, refresh them for the current object, use the
348 # pv_* identifiers to filter because those shouldn't change
349 pv_object = get_pv(pv_name=self.pv_name, pv_uuid=self.pv_uuid)
350 self.tags = pv_object.tags
351
352 def set_tag(self, key, value):
353 """
354 Set the key/value pair as an LVM tag. Does not "refresh" the values of
355 the current object for its tags. Meant to be a "fire and forget" type
356 of modification.
357
358 **warning**: Altering tags on a PV has to be done ensuring that the
359 device is actually the one intended. ``pv_name`` is *not* a persistent
360 value, only ``pv_uuid`` is. Using ``pv_uuid`` is the best way to make
361 sure the device getting changed is the one needed.
362 """
363 # remove it first if it exists
364 if self.tags.get(key):
365 current_value = self.tags[key]
366 tag = "%s=%s" % (key, current_value)
367 process.call(['pvchange', '--deltag', tag, self.pv_name])
368
369 process.call(
370 [
371 'pvchange',
372 '--addtag', '%s=%s' % (key, value), self.pv_name
373 ]
374 )
375
376
377 class PVolumes(list):
378 """
379 A list of all known (physical) volumes for the current system, with the ability
380 to filter them via keyword arguments.
381 """
382
383 def __init__(self, populate=True):
384 if populate:
385 self._populate()
386
387 def _populate(self):
388 # get all the pvs in the current system
389 for pv_item in get_api_pvs():
390 self.append(PVolume(**pv_item))
391
392 def _purge(self):
393 """
394 Deplete all the items in the list, used internally only so that we can
395 dynamically allocate the items when filtering without the concern of
396 messing up the contents
397 """
398 self[:] = []
399
400 def _filter(self, pv_name=None, pv_uuid=None, pv_tags=None):
401 """
402 The actual method that filters using a new list. Useful so that other
403 methods that do not want to alter the contents of the list (e.g.
404 ``self.find``) can operate safely.
405 """
406 filtered = [i for i in self]
407 if pv_name:
408 filtered = [i for i in filtered if i.pv_name == pv_name]
409
410 if pv_uuid:
411 filtered = [i for i in filtered if i.pv_uuid == pv_uuid]
412
413 # at this point, `filtered` has either all the physical volumes in self
414 # or is an actual filtered list if any filters were applied
415 if pv_tags:
416 tag_filtered = []
417 for pvolume in filtered:
418 matches = all(pvolume.tags.get(k) == str(v) for k, v in pv_tags.items())
419 if matches:
420 tag_filtered.append(pvolume)
421 # return the tag_filtered pvolumes here, the `filtered` list is no
422 # longer usable
423 return tag_filtered
424
425 return filtered
426
427 def filter(self, pv_name=None, pv_uuid=None, pv_tags=None):
428 """
429 Filter out volumes on top level attributes like ``pv_name`` or by
430 ``pv_tags`` where a dict is required. For example, to find a physical
431 volume that has an OSD ID of 0, the filter would look like::
432
433 pv_tags={'ceph.osd_id': '0'}
434
435 """
436 if not any([pv_name, pv_uuid, pv_tags]):
437 raise TypeError('.filter() requires pv_name, pv_uuid, or pv_tags'
438 '(none given)')
439
440 filtered_pvs = PVolumes(populate=False)
441 filtered_pvs.extend(self._filter(pv_name, pv_uuid, pv_tags))
442 return filtered_pvs
443
444 def get(self, pv_name=None, pv_uuid=None, pv_tags=None):
445 """
446 This is a bit expensive, since it will try to filter out all the
447 matching items in the list, filter them out applying anything that was
448 added and return the matching item.
449
450 This method does *not* alter the list, and it will raise an error if
451 multiple pvs are matched
452
453 It is useful to use ``tags`` when trying to find a specific logical volume,
454 but it can also lead to multiple pvs being found, since a lot of metadata
455 is shared between pvs of a distinct OSD.
456 """
457 if not any([pv_name, pv_uuid, pv_tags]):
458 return None
459 pvs = self._filter(
460 pv_name=pv_name,
461 pv_uuid=pv_uuid,
462 pv_tags=pv_tags
463 )
464 if not pvs:
465 return None
466 if len(pvs) > 1 and pv_tags:
467 raise MultiplePVsError(pv_name)
468 return pvs[0]
469
470
471 def create_pv(device):
472 """
473 Create a physical volume from a device, useful when devices need to be later mapped
474 to journals.
475 """
476 process.run([
477 'pvcreate',
478 '-v', # verbose
479 '-f', # force it
480 '--yes', # answer yes to any prompts
481 device
482 ])
483
484
485 def remove_pv(pv_name):
486 """
487 Removes a physical volume using a double `-f` to prevent prompts and fully
488 remove anything related to LVM. This is tremendously destructive, but so is all other actions
489 when zapping a device.
490
491 In the case where multiple PVs are found, it will ignore that fact and
492 continue with the removal, specifically in the case of messages like::
493
494 WARNING: PV $UUID /dev/DEV-1 was already found on /dev/DEV-2
495
496 These situations can be avoided with custom filtering rules, which this API
497 cannot handle while accommodating custom user filters.
498 """
499 fail_msg = "Unable to remove vg %s" % pv_name
500 process.run(
501 [
502 'pvremove',
503 '-v', # verbose
504 '-f', # force it
505 '-f', # force it
506 pv_name
507 ],
508 fail_msg=fail_msg,
509 )
510
511
512 def get_pv(pv_name=None, pv_uuid=None, pv_tags=None, pvs=None):
513 """
514 Return a matching pv (physical volume) for the current system, requiring
515 ``pv_name``, ``pv_uuid``, or ``pv_tags``. Raises an error if more than one
516 pv is found.
517 """
518 if not any([pv_name, pv_uuid, pv_tags]):
519 return None
520 if pvs is None or len(pvs) == 0:
521 pvs = PVolumes()
522
523 return pvs.get(pv_name=pv_name, pv_uuid=pv_uuid, pv_tags=pv_tags)
524
525
526 ################################
527 #
528 # Code for LVM Volume Groups
529 #
530 #############################
531
532 VG_FIELDS = 'vg_name,pv_count,lv_count,vg_attr,vg_extent_count,vg_free_count,vg_extent_size'
533 VG_CMD_OPTIONS = ['--noheadings', '--readonly', '--units=b', '--nosuffix', '--separator=";"']
534
535
536 def get_api_vgs():
537 """
538 Return the list of group volumes available in the system using flags to
539 include common metadata associated with them
540
541 Command and sample delimited output should look like::
542
543 $ vgs --noheadings --units=b --readonly --separator=';' \
544 -o vg_name,pv_count,lv_count,vg_attr,vg_free_count,vg_extent_size
545 ubuntubox-vg;1;2;wz--n-;12;
546
547 To normalize sizing, the units are forced in 'g' which is equivalent to
548 gigabytes, which uses multiples of 1024 (as opposed to 1000)
549 """
550 stdout, stderr, returncode = process.call(
551 ['vgs'] + VG_CMD_OPTIONS + ['-o', VG_FIELDS],
552 verbose_on_failure=False
553 )
554 return _output_parser(stdout, VG_FIELDS)
555
556
557 class VolumeGroup(object):
558 """
559 Represents an LVM group, with some top-level attributes like ``vg_name``
560 """
561
562 def __init__(self, **kw):
563 for k, v in kw.items():
564 setattr(self, k, v)
565 self.name = kw['vg_name']
566 if not self.name:
567 raise ValueError('VolumeGroup must have a non-empty name')
568 self.tags = parse_tags(kw.get('vg_tags', ''))
569
570 def __str__(self):
571 return '<%s>' % self.name
572
573 def __repr__(self):
574 return self.__str__()
575
576 @property
577 def free(self):
578 """
579 Return free space in VG in bytes
580 """
581 return int(self.vg_extent_size) * int(self.vg_free_count)
582
583 @property
584 def size(self):
585 """
586 Returns VG size in bytes
587 """
588 return int(self.vg_extent_size) * int(self.vg_extent_count)
589
590 def sizing(self, parts=None, size=None):
591 """
592 Calculate proper sizing to fully utilize the volume group in the most
593 efficient way possible. To prevent situations where LVM might accept
594 a percentage that is beyond the vg's capabilities, it will refuse with
595 an error when requesting a larger-than-possible parameter, in addition
596 to rounding down calculations.
597
598 A dictionary with different sizing parameters is returned, to make it
599 easier for others to choose what they need in order to create logical
600 volumes::
601
602 >>> data_vg.free
603 1024
604 >>> data_vg.sizing(parts=4)
605 {'parts': 4, 'sizes': 256, 'percentages': 25}
606 >>> data_vg.sizing(size=512)
607 {'parts': 2, 'sizes': 512, 'percentages': 50}
608
609
610 :param parts: Number of parts to create LVs from
611 :param size: Size in gigabytes to divide the VG into
612
613 :raises SizeAllocationError: When requested size cannot be allocated with
614 :raises ValueError: If both ``parts`` and ``size`` are given
615 """
616 if parts is not None and size is not None:
617 raise ValueError(
618 "Cannot process sizing with both parts (%s) and size (%s)" % (parts, size)
619 )
620
621 # if size is given we need to map that to extents so that we avoid
622 # issues when trying to get this right with a size in gigabytes find
623 # the percentage first, cheating, because these values are thrown out
624 vg_free_count = util.str_to_int(self.vg_free_count)
625
626 if size:
627 size = size * 1024 * 1024 * 1024
628 extents = int(size / int(self.vg_extent_size))
629 disk_sizing = sizing(self.free, size=size, parts=parts)
630 else:
631 if parts is not None:
632 # Prevent parts being 0, falling back to 1 (100% usage)
633 parts = parts or 1
634 size = int(self.free / parts)
635 extents = size * vg_free_count / self.free
636 disk_sizing = sizing(self.free, parts=parts)
637
638 extent_sizing = sizing(vg_free_count, size=extents)
639
640 disk_sizing['extents'] = int(extents)
641 disk_sizing['percentages'] = extent_sizing['percentages']
642 return disk_sizing
643
644 def bytes_to_extents(self, size):
645 '''
646 Return a how many extents we can fit into a size in bytes.
647 '''
648 return int(size / int(self.vg_extent_size))
649
650 def slots_to_extents(self, slots):
651 '''
652 Return how many extents fit the VG slot times
653 '''
654 return int(int(self.vg_free_count) / slots)
655
656
657 class VolumeGroups(list):
658 """
659 A list of all known volume groups for the current system, with the ability
660 to filter them via keyword arguments.
661 """
662
663 def __init__(self, populate=True):
664 if populate:
665 self._populate()
666
667 def _populate(self):
668 # get all the vgs in the current system
669 for vg_item in get_api_vgs():
670 self.append(VolumeGroup(**vg_item))
671
672 def _purge(self):
673 """
674 Deplete all the items in the list, used internally only so that we can
675 dynamically allocate the items when filtering without the concern of
676 messing up the contents
677 """
678 self[:] = []
679
680 def _filter(self, vg_name=None, vg_tags=None):
681 """
682 The actual method that filters using a new list. Useful so that other
683 methods that do not want to alter the contents of the list (e.g.
684 ``self.find``) can operate safely.
685
686 .. note:: ``vg_tags`` is not yet implemented
687 """
688 filtered = [i for i in self]
689 if vg_name:
690 filtered = [i for i in filtered if i.vg_name == vg_name]
691
692 # at this point, `filtered` has either all the volumes in self or is an
693 # actual filtered list if any filters were applied
694 if vg_tags:
695 tag_filtered = []
696 for volume in filtered:
697 matches = all(volume.tags.get(k) == str(v) for k, v in vg_tags.items())
698 if matches:
699 tag_filtered.append(volume)
700 return tag_filtered
701
702 return filtered
703
704 def filter(self, vg_name=None, vg_tags=None):
705 """
706 Filter out groups on top level attributes like ``vg_name`` or by
707 ``vg_tags`` where a dict is required. For example, to find a Ceph group
708 with dmcache as the type, the filter would look like::
709
710 vg_tags={'ceph.type': 'dmcache'}
711
712 .. warning:: These tags are not documented because they are currently
713 unused, but are here to maintain API consistency
714 """
715 if not any([vg_name, vg_tags]):
716 raise TypeError('.filter() requires vg_name or vg_tags (none given)')
717
718 filtered_vgs = VolumeGroups(populate=False)
719 filtered_vgs.extend(self._filter(vg_name, vg_tags))
720 return filtered_vgs
721
722 def get(self, vg_name=None, vg_tags=None):
723 """
724 This is a bit expensive, since it will try to filter out all the
725 matching items in the list, filter them out applying anything that was
726 added and return the matching item.
727
728 This method does *not* alter the list, and it will raise an error if
729 multiple VGs are matched
730
731 It is useful to use ``tags`` when trying to find a specific volume group,
732 but it can also lead to multiple vgs being found (although unlikely)
733 """
734 if not any([vg_name, vg_tags]):
735 return None
736 vgs = self._filter(
737 vg_name=vg_name,
738 vg_tags=vg_tags
739 )
740 if not vgs:
741 return None
742 if len(vgs) > 1:
743 # this is probably never going to happen, but it is here to keep
744 # the API code consistent
745 raise MultipleVGsError(vg_name)
746 return vgs[0]
747
748
749 def create_vg(devices, name=None, name_prefix=None):
750 """
751 Create a Volume Group. Command looks like::
752
753 vgcreate --force --yes group_name device
754
755 Once created the volume group is returned as a ``VolumeGroup`` object
756
757 :param devices: A list of devices to create a VG. Optionally, a single
758 device (as a string) can be used.
759 :param name: Optionally set the name of the VG, defaults to 'ceph-{uuid}'
760 :param name_prefix: Optionally prefix the name of the VG, which will get combined
761 with a UUID string
762 """
763 if isinstance(devices, set):
764 devices = list(devices)
765 if not isinstance(devices, list):
766 devices = [devices]
767 if name_prefix:
768 name = "%s-%s" % (name_prefix, str(uuid.uuid4()))
769 elif name is None:
770 name = "ceph-%s" % str(uuid.uuid4())
771 process.run([
772 'vgcreate',
773 '--force',
774 '--yes',
775 name] + devices
776 )
777
778 vg = get_vg(vg_name=name)
779 return vg
780
781
782 def extend_vg(vg, devices):
783 """
784 Extend a Volume Group. Command looks like::
785
786 vgextend --force --yes group_name [device, ...]
787
788 Once created the volume group is extended and returned as a ``VolumeGroup`` object
789
790 :param vg: A VolumeGroup object
791 :param devices: A list of devices to extend the VG. Optionally, a single
792 device (as a string) can be used.
793 """
794 if not isinstance(devices, list):
795 devices = [devices]
796 process.run([
797 'vgextend',
798 '--force',
799 '--yes',
800 vg.name] + devices
801 )
802
803 vg = get_vg(vg_name=vg.name)
804 return vg
805
806
807 def reduce_vg(vg, devices):
808 """
809 Reduce a Volume Group. Command looks like::
810
811 vgreduce --force --yes group_name [device, ...]
812
813 :param vg: A VolumeGroup object
814 :param devices: A list of devices to remove from the VG. Optionally, a
815 single device (as a string) can be used.
816 """
817 if not isinstance(devices, list):
818 devices = [devices]
819 process.run([
820 'vgreduce',
821 '--force',
822 '--yes',
823 vg.name] + devices
824 )
825
826 vg = get_vg(vg_name=vg.name)
827 return vg
828
829
830 def remove_vg(vg_name):
831 """
832 Removes a volume group.
833 """
834 if not vg_name:
835 logger.warning('Skipping removal of invalid VG name: "%s"', vg_name)
836 return
837 fail_msg = "Unable to remove vg %s" % vg_name
838 process.run(
839 [
840 'vgremove',
841 '-v', # verbose
842 '-f', # force it
843 vg_name
844 ],
845 fail_msg=fail_msg,
846 )
847
848
849 def get_vg(vg_name=None, vg_tags=None, vgs=None):
850 """
851 Return a matching vg for the current system, requires ``vg_name`` or
852 ``tags``. Raises an error if more than one vg is found.
853
854 It is useful to use ``tags`` when trying to find a specific volume group,
855 but it can also lead to multiple vgs being found.
856 """
857 if not any([vg_name, vg_tags]):
858 return None
859 if vgs is None or len(vgs) == 0:
860 vgs = VolumeGroups()
861
862 return vgs.get(vg_name=vg_name, vg_tags=vg_tags)
863
864
865 def get_device_vgs(device, name_prefix=''):
866 stdout, stderr, returncode = process.call(
867 ['pvs'] + VG_CMD_OPTIONS + ['-o', VG_FIELDS, device],
868 verbose_on_failure=False
869 )
870 vgs = _output_parser(stdout, VG_FIELDS)
871 return [VolumeGroup(**vg) for vg in vgs if vg['vg_name'] and vg['vg_name'].startswith(name_prefix)]
872
873
874 #################################
875 #
876 # Code for LVM Logical Volumes
877 #
878 ###############################
879
880 LV_FIELDS = 'lv_tags,lv_path,lv_name,vg_name,lv_uuid,lv_size'
881 LV_CMD_OPTIONS = ['--noheadings', '--readonly', '--separator=";"', '-a']
882
883 def get_api_lvs():
884 """
885 Return the list of logical volumes available in the system using flags to include common
886 metadata associated with them
887
888 Command and delimited output should look like::
889
890 $ lvs --noheadings --readonly --separator=';' -a -o lv_tags,lv_path,lv_name,vg_name
891 ;/dev/ubuntubox-vg/root;root;ubuntubox-vg
892 ;/dev/ubuntubox-vg/swap_1;swap_1;ubuntubox-vg
893
894 """
895 stdout, stderr, returncode = process.call(
896 ['lvs'] + LV_CMD_OPTIONS + ['-o', LV_FIELDS],
897 verbose_on_failure=False
898 )
899 return _output_parser(stdout, LV_FIELDS)
900
901
902 class Volume(object):
903 """
904 Represents a Logical Volume from LVM, with some top-level attributes like
905 ``lv_name`` and parsed tags as a dictionary of key/value pairs.
906 """
907
908 def __init__(self, **kw):
909 for k, v in kw.items():
910 setattr(self, k, v)
911 self.lv_api = kw
912 self.name = kw['lv_name']
913 if not self.name:
914 raise ValueError('Volume must have a non-empty name')
915 self.tags = parse_tags(kw['lv_tags'])
916 self.encrypted = self.tags.get('ceph.encrypted', '0') == '1'
917 self.used_by_ceph = 'ceph.osd_id' in self.tags
918
919 def __str__(self):
920 return '<%s>' % self.lv_api['lv_path']
921
922 def __repr__(self):
923 return self.__str__()
924
925 def as_dict(self):
926 obj = {}
927 obj.update(self.lv_api)
928 obj['tags'] = self.tags
929 obj['name'] = self.name
930 obj['type'] = self.tags['ceph.type']
931 obj['path'] = self.lv_path
932 return obj
933
934 def report(self):
935 if not self.used_by_ceph:
936 return {
937 'name': self.lv_name,
938 'comment': 'not used by ceph'
939 }
940 else:
941 type_ = self.tags['ceph.type']
942 report = {
943 'name': self.lv_name,
944 'osd_id': self.tags['ceph.osd_id'],
945 'cluster_name': self.tags['ceph.cluster_name'],
946 'type': type_,
947 'osd_fsid': self.tags['ceph.osd_fsid'],
948 'cluster_fsid': self.tags['ceph.cluster_fsid'],
949 }
950 type_uuid = '{}_uuid'.format(type_)
951 report[type_uuid] = self.tags['ceph.{}'.format(type_uuid)]
952 return report
953
954 def clear_tags(self):
955 """
956 Removes all tags from the Logical Volume.
957 """
958 for k in list(self.tags):
959 self.clear_tag(k)
960
961
962 def set_tags(self, tags):
963 """
964 :param tags: A dictionary of tag names and values, like::
965
966 {
967 "ceph.osd_fsid": "aaa-fff-bbbb",
968 "ceph.osd_id": "0"
969 }
970
971 At the end of all modifications, the tags are refreshed to reflect
972 LVM's most current view.
973 """
974 for k, v in tags.items():
975 self.set_tag(k, v)
976
977
978 def clear_tag(self, key):
979 if self.tags.get(key):
980 current_value = self.tags[key]
981 tag = "%s=%s" % (key, current_value)
982 process.call(['lvchange', '--deltag', tag, self.lv_path])
983 del self.tags[key]
984
985
986 def set_tag(self, key, value):
987 """
988 Set the key/value pair as an LVM tag.
989 """
990 # remove it first if it exists
991 self.clear_tag(key)
992
993 process.call(
994 [
995 'lvchange',
996 '--addtag', '%s=%s' % (key, value), self.lv_path
997 ]
998 )
999 self.tags[key] = value
1000
1001 def deactivate(self):
1002 """
1003 Deactivate the LV by calling lvchange -an
1004 """
1005 process.call(['lvchange', '-an', self.lv_path])
1006
1007
1008 class Volumes(list):
1009 """
1010 A list of all known (logical) volumes for the current system, with the ability
1011 to filter them via keyword arguments.
1012 """
1013
1014 def __init__(self):
1015 self._populate()
1016
1017 def _populate(self):
1018 # get all the lvs in the current system
1019 for lv_item in get_api_lvs():
1020 self.append(Volume(**lv_item))
1021
1022 def _purge(self):
1023 """
1024 Delete all the items in the list, used internally only so that we can
1025 dynamically allocate the items when filtering without the concern of
1026 messing up the contents
1027 """
1028 self[:] = []
1029
1030 def _filter(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
1031 """
1032 The actual method that filters using a new list. Useful so that other
1033 methods that do not want to alter the contents of the list (e.g.
1034 ``self.find``) can operate safely.
1035 """
1036 filtered = [i for i in self]
1037 if lv_name:
1038 filtered = [i for i in filtered if i.lv_name == lv_name]
1039
1040 if vg_name:
1041 filtered = [i for i in filtered if i.vg_name == vg_name]
1042
1043 if lv_uuid:
1044 filtered = [i for i in filtered if i.lv_uuid == lv_uuid]
1045
1046 if lv_path:
1047 filtered = [i for i in filtered if i.lv_path == lv_path]
1048
1049 # at this point, `filtered` has either all the volumes in self or is an
1050 # actual filtered list if any filters were applied
1051 if lv_tags:
1052 tag_filtered = []
1053 for volume in filtered:
1054 # all the tags we got need to match on the volume
1055 matches = all(volume.tags.get(k) == str(v) for k, v in lv_tags.items())
1056 if matches:
1057 tag_filtered.append(volume)
1058 return tag_filtered
1059
1060 return filtered
1061
1062 def filter(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
1063 """
1064 Filter out volumes on top level attributes like ``lv_name`` or by
1065 ``lv_tags`` where a dict is required. For example, to find a volume
1066 that has an OSD ID of 0, the filter would look like::
1067
1068 lv_tags={'ceph.osd_id': '0'}
1069
1070 """
1071 if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]):
1072 raise TypeError('.filter() requires lv_name, vg_name, lv_path, lv_uuid, or tags (none given)')
1073 # first find the filtered volumes with the values in self
1074 filtered_volumes = self._filter(
1075 lv_name=lv_name,
1076 vg_name=vg_name,
1077 lv_path=lv_path,
1078 lv_uuid=lv_uuid,
1079 lv_tags=lv_tags
1080 )
1081 # then purge everything
1082 self._purge()
1083 # and add the filtered items
1084 self.extend(filtered_volumes)
1085
1086 def get(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
1087 """
1088 This is a bit expensive, since it will try to filter out all the
1089 matching items in the list, filter them out applying anything that was
1090 added and return the matching item.
1091
1092 This method does *not* alter the list, and it will raise an error if
1093 multiple LVs are matched
1094
1095 It is useful to use ``tags`` when trying to find a specific logical volume,
1096 but it can also lead to multiple lvs being found, since a lot of metadata
1097 is shared between lvs of a distinct OSD.
1098 """
1099 if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]):
1100 return None
1101 lvs = self._filter(
1102 lv_name=lv_name,
1103 vg_name=vg_name,
1104 lv_path=lv_path,
1105 lv_uuid=lv_uuid,
1106 lv_tags=lv_tags
1107 )
1108 if not lvs:
1109 return None
1110 if len(lvs) > 1:
1111 raise MultipleLVsError(lv_name, lv_path)
1112 return lvs[0]
1113
1114
1115 def create_lv(name_prefix,
1116 uuid,
1117 vg=None,
1118 device=None,
1119 slots=None,
1120 extents=None,
1121 size=None,
1122 tags=None):
1123 """
1124 Create a Logical Volume in a Volume Group. Command looks like::
1125
1126 lvcreate -L 50G -n gfslv vg0
1127
1128 ``name_prefix`` is required. If ``size`` is provided its expected to be a
1129 byte count. Tags are an optional dictionary and is expected to
1130 conform to the convention of prefixing them with "ceph." like::
1131
1132 {"ceph.block_device": "/dev/ceph/osd-1"}
1133
1134 :param name_prefix: name prefix for the LV, typically somehting like ceph-osd-block
1135 :param uuid: UUID to ensure uniqueness; is combined with name_prefix to
1136 form the LV name
1137 :param vg: optional, pass an existing VG to create LV
1138 :param device: optional, device to use. Either device of vg must be passed
1139 :param slots: optional, number of slots to divide vg up, LV will occupy one
1140 one slot if enough space is available
1141 :param extends: optional, how many lvm extends to use, supersedes slots
1142 :param size: optional, target LV size in bytes, supersedes extents,
1143 resulting LV might be smaller depending on extent
1144 size of the underlying VG
1145 :param tags: optional, a dict of lvm tags to set on the LV
1146 """
1147 name = '{}-{}'.format(name_prefix, uuid)
1148 if not vg:
1149 if not device:
1150 raise RuntimeError("Must either specify vg or device, none given")
1151 # check if a vgs starting with ceph already exists
1152 vgs = get_device_vgs(device, 'ceph')
1153 if vgs:
1154 vg = vgs[0]
1155 else:
1156 # create on if not
1157 vg = create_vg(device, name_prefix='ceph')
1158 assert(vg)
1159
1160 if size:
1161 extents = vg.bytes_to_extents(size)
1162 logger.debug('size was passed: {} -> {}'.format(size, extents))
1163 elif slots and not extents:
1164 extents = vg.slots_to_extents(slots)
1165 logger.debug('slots was passed: {} -> {}'.format(slots, extents))
1166
1167 if extents:
1168 command = [
1169 'lvcreate',
1170 '--yes',
1171 '-l',
1172 '{}'.format(extents),
1173 '-n', name, vg.vg_name
1174 ]
1175 # create the lv with all the space available, this is needed because the
1176 # system call is different for LVM
1177 else:
1178 command = [
1179 'lvcreate',
1180 '--yes',
1181 '-l',
1182 '100%FREE',
1183 '-n', name, vg.vg_name
1184 ]
1185 process.run(command)
1186
1187 lv = get_lv(lv_name=name, vg_name=vg.vg_name)
1188
1189 if tags is None:
1190 tags = {
1191 "ceph.osd_id": "null",
1192 "ceph.type": "null",
1193 "ceph.cluster_fsid": "null",
1194 "ceph.osd_fsid": "null",
1195 }
1196 # when creating a distinct type, the caller doesn't know what the path will
1197 # be so this function will set it after creation using the mapping
1198 # XXX add CEPH_VOLUME_LVM_DEBUG to enable -vvvv on lv operations
1199 type_path_tag = {
1200 'journal': 'ceph.journal_device',
1201 'data': 'ceph.data_device',
1202 'block': 'ceph.block_device',
1203 'wal': 'ceph.wal_device',
1204 'db': 'ceph.db_device',
1205 'lockbox': 'ceph.lockbox_device', # XXX might not ever need this lockbox sorcery
1206 }
1207 path_tag = type_path_tag.get(tags.get('ceph.type'))
1208 if path_tag:
1209 tags.update({path_tag: lv.lv_path})
1210
1211 lv.set_tags(tags)
1212
1213 return lv
1214
1215
1216 def remove_lv(lv):
1217 """
1218 Removes a logical volume given it's absolute path.
1219
1220 Will return True if the lv is successfully removed or
1221 raises a RuntimeError if the removal fails.
1222
1223 :param lv: A ``Volume`` object or the path for an LV
1224 """
1225 if isinstance(lv, Volume):
1226 path = lv.lv_path
1227 else:
1228 path = lv
1229
1230 stdout, stderr, returncode = process.call(
1231 [
1232 'lvremove',
1233 '-v', # verbose
1234 '-f', # force it
1235 path
1236 ],
1237 show_command=True,
1238 terminal_verbose=True,
1239 )
1240 if returncode != 0:
1241 raise RuntimeError("Unable to remove %s" % path)
1242 return True
1243
1244
1245 def is_lv(dev, lvs=None):
1246 """
1247 Boolean to detect if a device is an LV or not.
1248 """
1249 splitname = dmsetup_splitname(dev)
1250 # Allowing to optionally pass `lvs` can help reduce repetitive checks for
1251 # multiple devices at once.
1252 if lvs is None or len(lvs) == 0:
1253 lvs = Volumes()
1254
1255 if splitname.get('LV_NAME'):
1256 lvs.filter(lv_name=splitname['LV_NAME'], vg_name=splitname['VG_NAME'])
1257 return len(lvs) > 0
1258 return False
1259
1260 def get_lv_by_name(name):
1261 stdout, stderr, returncode = process.call(
1262 ['lvs', '--noheadings', '-o', LV_FIELDS, '-S',
1263 'lv_name={}'.format(name)],
1264 verbose_on_failure=False
1265 )
1266 lvs = _output_parser(stdout, LV_FIELDS)
1267 return [Volume(**lv) for lv in lvs]
1268
1269 def get_lvs_by_tag(lv_tag):
1270 stdout, stderr, returncode = process.call(
1271 ['lvs', '--noheadings', '--separator=";"', '-a', '-o', LV_FIELDS, '-S',
1272 'lv_tags={{{}}}'.format(lv_tag)],
1273 verbose_on_failure=False
1274 )
1275 lvs = _output_parser(stdout, LV_FIELDS)
1276 return [Volume(**lv) for lv in lvs]
1277
1278 def get_lv(lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None, lvs=None):
1279 """
1280 Return a matching lv for the current system, requiring ``lv_name``,
1281 ``vg_name``, ``lv_path`` or ``tags``. Raises an error if more than one lv
1282 is found.
1283
1284 It is useful to use ``tags`` when trying to find a specific logical volume,
1285 but it can also lead to multiple lvs being found, since a lot of metadata
1286 is shared between lvs of a distinct OSD.
1287 """
1288 if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]):
1289 return None
1290 if lvs is None:
1291 lvs = Volumes()
1292 return lvs.get(
1293 lv_name=lv_name, vg_name=vg_name, lv_path=lv_path, lv_uuid=lv_uuid,
1294 lv_tags=lv_tags
1295 )
1296
1297
1298 def get_lv_from_argument(argument):
1299 """
1300 Helper proxy function that consumes a possible logical volume passed in from the CLI
1301 in the form of `vg/lv`, but with some validation so that an argument that is a full
1302 path to a device can be ignored
1303 """
1304 if argument.startswith('/'):
1305 lv = get_lv(lv_path=argument)
1306 return lv
1307 try:
1308 vg_name, lv_name = argument.split('/')
1309 except (ValueError, AttributeError):
1310 return None
1311 return get_lv(lv_name=lv_name, vg_name=vg_name)
1312
1313
1314 def create_lvs(volume_group, parts=None, size=None, name_prefix='ceph-lv'):
1315 """
1316 Create multiple Logical Volumes from a Volume Group by calculating the
1317 proper extents from ``parts`` or ``size``. A custom prefix can be used
1318 (defaults to ``ceph-lv``), these names are always suffixed with a uuid.
1319
1320 LV creation in ceph-volume will require tags, this is expected to be
1321 pre-computed by callers who know Ceph metadata like OSD IDs and FSIDs. It
1322 will probably not be the case when mass-creating LVs, so common/default
1323 tags will be set to ``"null"``.
1324
1325 .. note:: LVs that are not in use can be detected by querying LVM for tags that are
1326 set to ``"null"``.
1327
1328 :param volume_group: The volume group (vg) to use for LV creation
1329 :type group: ``VolumeGroup()`` object
1330 :param parts: Number of LVs to create *instead of* ``size``.
1331 :type parts: int
1332 :param size: Size (in gigabytes) of LVs to create, e.g. "as many 10gb LVs as possible"
1333 :type size: int
1334 :param extents: The number of LVM extents to use to create the LV. Useful if looking to have
1335 accurate LV sizes (LVM rounds sizes otherwise)
1336 """
1337 if parts is None and size is None:
1338 # fallback to just one part (using 100% of the vg)
1339 parts = 1
1340 lvs = []
1341 tags = {
1342 "ceph.osd_id": "null",
1343 "ceph.type": "null",
1344 "ceph.cluster_fsid": "null",
1345 "ceph.osd_fsid": "null",
1346 }
1347 sizing = volume_group.sizing(parts=parts, size=size)
1348 for part in range(0, sizing['parts']):
1349 size = sizing['sizes']
1350 extents = sizing['extents']
1351 lvs.append(
1352 create_lv(name_prefix, uuid.uuid4(), vg=volume_group, extents=extents, tags=tags)
1353 )
1354 return lvs
1355
1356
1357 def get_device_lvs(device, name_prefix=''):
1358 stdout, stderr, returncode = process.call(
1359 ['pvs'] + LV_CMD_OPTIONS + ['-o', LV_FIELDS, device],
1360 verbose_on_failure=False
1361 )
1362 lvs = _output_parser(stdout, LV_FIELDS)
1363 return [Volume(**lv) for lv in lvs if lv['lv_name'] and
1364 lv['lv_name'].startswith(name_prefix)]
1365
1366
1367 #############################################################
1368 #
1369 # New methods to get PVs, LVs, and VGs.
1370 # Later, these can be easily merged with get_api_* methods
1371 #
1372 ###########################################################
1373
1374 def convert_filters_to_str(filters):
1375 """
1376 Convert filter args from dictionary to following format -
1377 filters={filter_name=filter_val,...}
1378 """
1379 if not filters:
1380 return filters
1381
1382 filter_arg = ''
1383 for k, v in filters.items():
1384 filter_arg += k + '=' + v + ','
1385 # get rid of extra comma at the end
1386 filter_arg = filter_arg[:len(filter_arg) - 1]
1387
1388 return filter_arg
1389
1390 def convert_tags_to_str(tags):
1391 """
1392 Convert tags from dictionary to following format -
1393 tags={tag_name=tag_val,...}
1394 """
1395 if not tags:
1396 return tags
1397
1398 tag_arg = 'tags={'
1399 for k, v in tags.items():
1400 tag_arg += k + '=' + v + ','
1401 # get rid of extra comma at the end
1402 tag_arg = tag_arg[:len(tag_arg) - 1] + '}'
1403
1404 return tag_arg
1405
1406 def make_filters_lvmcmd_ready(filters, tags):
1407 """
1408 Convert filters (including tags) from dictionary to following format -
1409 filter_name=filter_val...,tags={tag_name=tag_val,...}
1410
1411 The command will look as follows =
1412 lvs -S filter_name=filter_val...,tags={tag_name=tag_val,...}
1413 """
1414 filters = convert_filters_to_str(filters)
1415 tags = convert_tags_to_str(tags)
1416
1417 if filters and tags:
1418 return filters + ',' + tags
1419 if filters and not tags:
1420 return filters
1421 if not filters and tags:
1422 return tags
1423 else:
1424 return ''
1425
1426 def get_pvs(fields=PV_FIELDS, filters='', tags=None):
1427 """
1428 Return a list of PVs that are available on the system and match the
1429 filters and tags passed. Argument filters takes a dictionary containing
1430 arguments required by -S option of LVM. Passing a list of LVM tags can be
1431 quite tricky to pass as a dictionary within dictionary, therefore pass
1432 dictionary of tags via tags argument and tricky part will be taken care of
1433 by the helper methods.
1434
1435 :param fields: string containing list of fields to be displayed by the
1436 pvs command
1437 :param sep: string containing separator to be used between two fields
1438 :param filters: dictionary containing LVM filters
1439 :param tags: dictionary containng LVM tags
1440 :returns: list of class PVolume object representing pvs on the system
1441 """
1442 filters = make_filters_lvmcmd_ready(filters, tags)
1443 args = ['pvs', '--no-heading', '--readonly', '--separator=";"', '-S',
1444 filters, '-o', fields]
1445
1446 stdout, stderr, returncode = process.call(args, verbose_on_failure=False)
1447 pvs_report = _output_parser(stdout, fields)
1448 return [PVolume(**pv_report) for pv_report in pvs_report]
1449
1450 def get_first_pv(fields=PV_FIELDS, filters=None, tags=None):
1451 """
1452 Wrapper of get_pv meant to be a convenience method to avoid the phrase::
1453 pvs = get_pvs()
1454 if len(pvs) >= 1:
1455 pv = pvs[0]
1456 """
1457 pvs = get_pvs(fields=fields, filters=filters, tags=tags)
1458 return pvs[0] if len(pvs) > 0 else []
1459
1460 def get_vgs(fields=VG_FIELDS, filters='', tags=None):
1461 """
1462 Return a list of VGs that are available on the system and match the
1463 filters and tags passed. Argument filters takes a dictionary containing
1464 arguments required by -S option of LVM. Passing a list of LVM tags can be
1465 quite tricky to pass as a dictionary within dictionary, therefore pass
1466 dictionary of tags via tags argument and tricky part will be taken care of
1467 by the helper methods.
1468
1469 :param fields: string containing list of fields to be displayed by the
1470 vgs command
1471 :param sep: string containing separator to be used between two fields
1472 :param filters: dictionary containing LVM filters
1473 :param tags: dictionary containng LVM tags
1474 :returns: list of class VolumeGroup object representing vgs on the system
1475 """
1476 filters = make_filters_lvmcmd_ready(filters, tags)
1477 args = ['vgs'] + VG_CMD_OPTIONS + ['-S', filters, '-o', fields]
1478
1479 stdout, stderr, returncode = process.call(args, verbose_on_failure=False)
1480 vgs_report =_output_parser(stdout, fields)
1481 return [VolumeGroup(**vg_report) for vg_report in vgs_report]
1482
1483 def get_first_vg(fields=VG_FIELDS, filters=None, tags=None):
1484 """
1485 Wrapper of get_vg meant to be a convenience method to avoid the phrase::
1486 vgs = get_vgs()
1487 if len(vgs) >= 1:
1488 vg = vgs[0]
1489 """
1490 vgs = get_vgs(fields=fields, filters=filters, tags=tags)
1491 return vgs[0] if len(vgs) > 0 else []
1492
1493 def get_lvs(fields=LV_FIELDS, filters='', tags=None):
1494 """
1495 Return a list of LVs that are available on the system and match the
1496 filters and tags passed. Argument filters takes a dictionary containing
1497 arguments required by -S option of LVM. Passing a list of LVM tags can be
1498 quite tricky to pass as a dictionary within dictionary, therefore pass
1499 dictionary of tags via tags argument and tricky part will be taken care of
1500 by the helper methods.
1501
1502 :param fields: string containing list of fields to be displayed by the
1503 lvs command
1504 :param sep: string containing separator to be used between two fields
1505 :param filters: dictionary containing LVM filters
1506 :param tags: dictionary containng LVM tags
1507 :returns: list of class Volume object representing LVs on the system
1508 """
1509 filters = make_filters_lvmcmd_ready(filters, tags)
1510 args = ['lvs'] + LV_CMD_OPTIONS + ['-S', filters, '-o', fields]
1511
1512 stdout, stderr, returncode = process.call(args, verbose_on_failure=False)
1513 lvs_report = _output_parser(stdout, fields)
1514 return [Volume(**lv_report) for lv_report in lvs_report]
1515
1516 def get_first_lv(fields=LV_FIELDS, filters=None, tags=None):
1517 """
1518 Wrapper of get_lv meant to be a convenience method to avoid the phrase::
1519 lvs = get_lvs()
1520 if len(lvs) >= 1:
1521 lv = lvs[0]
1522 """
1523 lvs = get_lvs(fields=fields, filters=filters, tags=tags)
1524 return lvs[0] if len(lvs) > 0 else []