]>
git.proxmox.com Git - ceph.git/blob - ceph/src/ceph-volume/ceph_volume/api/lvm.py
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.
8 from ceph_volume
import process
9 from ceph_volume
.exceptions
import MultipleLVsError
, MultipleVGsError
, MultiplePVsError
11 logger
= logging
.getLogger(__name__
)
14 def _output_parser(output
, fields
):
16 Newer versions of LVM allow ``--reportformat=json``, but older versions,
17 like the one included in Xenial do not. LVM has the ability to filter and
18 format its output so we assume the output will be in a format this parser
19 can handle (using ',' as a delimiter)
21 :param fields: A string, possibly using ',' to group many items, as it
22 would be used on the CLI
23 :param output: The CLI output from the LVM call
25 field_items
= fields
.split(',')
28 # clear the leading/trailing whitespace
31 # remove the extra '"' in each field
32 line
= line
.replace('"', '')
34 # prevent moving forward with empty contents
38 # spliting on ';' because that is what the lvm call uses as
40 output_items
= [i
.strip() for i
in line
.split(';')]
41 # map the output to the fiels
43 dict(zip(field_items
, output_items
))
49 def parse_tags(lv_tags
):
51 Return a dictionary mapping of all the tags associated with
52 a Volume from the comma-separated tags coming from the LVM API
56 "ceph.osd_fsid=aaa-fff-bbbb,ceph.osd_id=0"
58 For the above example, the expected return value would be::
61 "ceph.osd_fsid": "aaa-fff-bbbb",
68 tags
= lv_tags
.split(',')
69 for tag_assignment
in tags
:
70 if not tag_assignment
.startswith('ceph.'):
72 key
, value
= tag_assignment
.split('=', 1)
73 tag_mapping
[key
] = value
78 def _vdo_parents(devices
):
80 It is possible we didn't get a logical volume, or a mapper path, but
81 a device like /dev/sda2, to resolve this, we must look at all the slaves of
82 every single device in /sys/block and if any of those devices is related to
83 VDO devices, then we can add the parent
86 for parent
in os
.listdir('/sys/block'):
87 for slave
in os
.listdir('/sys/block/%s/slaves' % parent
):
89 parent_devices
.append('/dev/%s' % parent
)
90 parent_devices
.append(parent
)
94 def _vdo_slaves(vdo_names
):
96 find all the slaves associated with each vdo name (from realpath) by going
97 into /sys/block/<realpath>/slaves
100 for vdo_name
in vdo_names
:
101 mapper_path
= '/dev/mapper/%s' % vdo_name
102 if not os
.path
.exists(mapper_path
):
104 # resolve the realpath and realname of the vdo mapper
105 vdo_realpath
= os
.path
.realpath(mapper_path
)
106 vdo_realname
= vdo_realpath
.split('/')[-1]
107 slaves_path
= '/sys/block/%s/slaves' % vdo_realname
108 if not os
.path
.exists(slaves_path
):
110 devices
.append(vdo_realpath
)
111 devices
.append(mapper_path
)
112 devices
.append(vdo_realname
)
113 for slave
in os
.listdir(slaves_path
):
114 devices
.append('/dev/%s' % slave
)
115 devices
.append(slave
)
121 A VDO device can be composed from many different devices, go through each
122 one of those devices and its slaves (if any) and correlate them back to
123 /dev/mapper and their realpaths, and then check if they appear as part of
124 /sys/kvdo/<name>/statistics
126 From the realpath of a logical volume, determine if it is a VDO device or
127 not, by correlating it to the presence of the name in
128 /sys/kvdo/<name>/statistics and all the previously captured devices
130 if not os
.path
.isdir('/sys/kvdo'):
132 realpath
= os
.path
.realpath(path
)
133 realpath_name
= realpath
.split('/')[-1]
136 # get all the vdo names
137 for dirname
in os
.listdir('/sys/kvdo/'):
138 if os
.path
.isdir('/sys/kvdo/%s/statistics' % dirname
):
139 vdo_names
.add(dirname
)
141 # find all the slaves associated with each vdo name (from realpath) by
142 # going into /sys/block/<realpath>/slaves
143 devices
.extend(_vdo_slaves(vdo_names
))
145 # Find all possible parents, looking into slaves that are related to VDO
146 devices
.extend(_vdo_parents(devices
))
151 realpath_name
in devices
])
156 Detect if a path is backed by VDO, proxying the actual call to _is_vdo so
157 that we can prevent an exception breaking OSD creation. If an exception is
158 raised, it will get captured and logged to file, while returning
166 logger
.exception('Unable to properly detect device as VDO: %s', path
)
172 Return the list of group volumes available in the system using flags to
173 include common metadata associated with them
175 Command and sample delimited output should look like::
177 $ vgs --noheadings --readonly --separator=';' \
178 -o vg_name,pv_count,lv_count,snap_count,vg_attr,vg_size,vg_free
179 ubuntubox-vg;1;2;0;wz--n-;299.52g;12.00m
180 osd_vg;3;1;0;wz--n-;29.21g;9.21g
183 fields
= 'vg_name,pv_count,lv_count,snap_count,vg_attr,vg_size,vg_free'
184 stdout
, stderr
, returncode
= process
.call(
185 ['vgs', '--noheadings', '--readonly', '--separator=";"', '-o', fields
]
187 return _output_parser(stdout
, fields
)
192 Return the list of logical volumes available in the system using flags to include common
193 metadata associated with them
195 Command and delimited output should look like::
197 $ lvs --noheadings --readonly --separator=';' -o lv_tags,lv_path,lv_name,vg_name
198 ;/dev/ubuntubox-vg/root;root;ubuntubox-vg
199 ;/dev/ubuntubox-vg/swap_1;swap_1;ubuntubox-vg
202 fields
= 'lv_tags,lv_path,lv_name,vg_name,lv_uuid'
203 stdout
, stderr
, returncode
= process
.call(
204 ['lvs', '--noheadings', '--readonly', '--separator=";"', '-o', fields
]
206 return _output_parser(stdout
, fields
)
211 Return the list of physical volumes configured for lvm and available in the
212 system using flags to include common metadata associated with them like the uuid
214 This will only return physical volumes set up to work with LVM.
216 Command and delimited output should look like::
218 $ pvs --noheadings --readonly --separator=';' -o pv_name,pv_tags,pv_uuid
220 /dev/sdv;;07A4F654-4162-4600-8EB3-88D1E42F368D
223 fields
= 'pv_name,pv_tags,pv_uuid,vg_name,lv_uuid'
225 stdout
, stderr
, returncode
= process
.call(
226 ['pvs', '--no-heading', '--readonly', '--separator=";"', '-o', fields
]
229 return _output_parser(stdout
, fields
)
232 def get_lv_from_argument(argument
):
234 Helper proxy function that consumes a possible logical volume passed in from the CLI
235 in the form of `vg/lv`, but with some validation so that an argument that is a full
236 path to a device can be ignored
238 if argument
.startswith('/'):
239 lv
= get_lv(lv_path
=argument
)
242 vg_name
, lv_name
= argument
.split('/')
243 except (ValueError, AttributeError):
245 return get_lv(lv_name
=lv_name
, vg_name
=vg_name
)
248 def get_lv(lv_name
=None, vg_name
=None, lv_path
=None, lv_uuid
=None, lv_tags
=None):
250 Return a matching lv for the current system, requiring ``lv_name``,
251 ``vg_name``, ``lv_path`` or ``tags``. Raises an error if more than one lv
254 It is useful to use ``tags`` when trying to find a specific logical volume,
255 but it can also lead to multiple lvs being found, since a lot of metadata
256 is shared between lvs of a distinct OSD.
258 if not any([lv_name
, vg_name
, lv_path
, lv_uuid
, lv_tags
]):
262 lv_name
=lv_name
, vg_name
=vg_name
, lv_path
=lv_path
, lv_uuid
=lv_uuid
,
267 def get_pv(pv_name
=None, pv_uuid
=None, pv_tags
=None):
269 Return a matching pv (physical volume) for the current system, requiring
270 ``pv_name``, ``pv_uuid``, or ``pv_tags``. Raises an error if more than one
273 if not any([pv_name
, pv_uuid
, pv_tags
]):
276 return pvs
.get(pv_name
=pv_name
, pv_uuid
=pv_uuid
, pv_tags
=pv_tags
)
279 def create_pv(device
):
281 Create a physical volume from a device, useful when devices need to be later mapped
288 '--yes', # answer yes to any prompts
293 def create_vg(name
, *devices
):
295 Create a Volume Group. Command looks like::
297 vgcreate --force --yes group_name device
299 Once created the volume group is returned as a ``VolumeGroup`` object
305 name
] + list(devices
)
308 vg
= get_vg(vg_name
=name
)
312 def remove_vg(vg_name
):
314 Removes a volume group.
316 fail_msg
= "Unable to remove vg %s" % vg_name
328 def remove_pv(pv_name
):
330 Removes a physical volume.
332 fail_msg
= "Unable to remove vg %s" % pv_name
346 Removes a logical volume given it's absolute path.
348 Will return True if the lv is successfully removed or
349 raises a RuntimeError if the removal fails.
351 stdout
, stderr
, returncode
= process
.call(
359 terminal_verbose
=True,
362 raise RuntimeError("Unable to remove %s" % path
)
366 def create_lv(name
, group
, size
=None, tags
=None):
368 Create a Logical Volume in a Volume Group. Command looks like::
370 lvcreate -L 50G -n gfslv vg0
372 ``name``, ``group``, are required. If ``size`` is provided it must follow
373 lvm's size notation (like 1G, or 20M). Tags are an optional dictionary and is expected to
374 conform to the convention of prefixing them with "ceph." like::
376 {"ceph.block_device": "/dev/ceph/osd-1"}
378 # XXX add CEPH_VOLUME_LVM_DEBUG to enable -vvvv on lv operations
380 'journal': 'ceph.journal_device',
381 'data': 'ceph.data_device',
382 'block': 'ceph.block_device',
383 'wal': 'ceph.wal_device',
384 'db': 'ceph.db_device',
385 'lockbox': 'ceph.lockbox_device', # XXX might not ever need this lockbox sorcery
395 # create the lv with all the space available, this is needed because the
396 # system call is different for LVM
406 lv
= get_lv(lv_name
=name
, vg_name
=group
)
409 # when creating a distinct type, the caller doesn't know what the path will
410 # be so this function will set it after creation using the mapping
411 path_tag
= type_path_tag
.get(tags
.get('ceph.type'))
414 {path_tag
: lv
.lv_path
}
419 def get_vg(vg_name
=None, vg_tags
=None):
421 Return a matching vg for the current system, requires ``vg_name`` or
422 ``tags``. Raises an error if more than one vg is found.
424 It is useful to use ``tags`` when trying to find a specific volume group,
425 but it can also lead to multiple vgs being found.
427 if not any([vg_name
, vg_tags
]):
430 return vgs
.get(vg_name
=vg_name
, vg_tags
=vg_tags
)
433 class VolumeGroups(list):
435 A list of all known volume groups for the current system, with the ability
436 to filter them via keyword arguments.
443 # get all the vgs in the current system
444 for vg_item
in get_api_vgs():
445 self
.append(VolumeGroup(**vg_item
))
449 Deplete all the items in the list, used internally only so that we can
450 dynamically allocate the items when filtering without the concern of
451 messing up the contents
455 def _filter(self
, vg_name
=None, vg_tags
=None):
457 The actual method that filters using a new list. Useful so that other
458 methods that do not want to alter the contents of the list (e.g.
459 ``self.find``) can operate safely.
461 .. note:: ``vg_tags`` is not yet implemented
463 filtered
= [i
for i
in self
]
465 filtered
= [i
for i
in filtered
if i
.vg_name
== vg_name
]
467 # at this point, `filtered` has either all the volumes in self or is an
468 # actual filtered list if any filters were applied
471 for volume
in filtered
:
472 matches
= all(volume
.tags
.get(k
) == str(v
) for k
, v
in vg_tags
.items())
474 tag_filtered
.append(volume
)
479 def filter(self
, vg_name
=None, vg_tags
=None):
481 Filter out groups on top level attributes like ``vg_name`` or by
482 ``vg_tags`` where a dict is required. For example, to find a Ceph group
483 with dmcache as the type, the filter would look like::
485 vg_tags={'ceph.type': 'dmcache'}
487 .. warning:: These tags are not documented because they are currently
488 unused, but are here to maintain API consistency
490 if not any([vg_name
, vg_tags
]):
491 raise TypeError('.filter() requires vg_name or vg_tags (none given)')
492 # first find the filtered volumes with the values in self
493 filtered_groups
= self
._filter
(
497 # then purge everything
499 # and add the filtered items
500 self
.extend(filtered_groups
)
502 def get(self
, vg_name
=None, vg_tags
=None):
504 This is a bit expensive, since it will try to filter out all the
505 matching items in the list, filter them out applying anything that was
506 added and return the matching item.
508 This method does *not* alter the list, and it will raise an error if
509 multiple VGs are matched
511 It is useful to use ``tags`` when trying to find a specific volume group,
512 but it can also lead to multiple vgs being found (although unlikely)
514 if not any([vg_name
, vg_tags
]):
523 # this is probably never going to happen, but it is here to keep
524 # the API code consistent
525 raise MultipleVGsError(vg_name
)
531 A list of all known (logical) volumes for the current system, with the ability
532 to filter them via keyword arguments.
539 # get all the lvs in the current system
540 for lv_item
in get_api_lvs():
541 self
.append(Volume(**lv_item
))
545 Delete all the items in the list, used internally only so that we can
546 dynamically allocate the items when filtering without the concern of
547 messing up the contents
551 def _filter(self
, lv_name
=None, vg_name
=None, lv_path
=None, lv_uuid
=None, lv_tags
=None):
553 The actual method that filters using a new list. Useful so that other
554 methods that do not want to alter the contents of the list (e.g.
555 ``self.find``) can operate safely.
557 filtered
= [i
for i
in self
]
559 filtered
= [i
for i
in filtered
if i
.lv_name
== lv_name
]
562 filtered
= [i
for i
in filtered
if i
.vg_name
== vg_name
]
565 filtered
= [i
for i
in filtered
if i
.lv_uuid
== lv_uuid
]
568 filtered
= [i
for i
in filtered
if i
.lv_path
== lv_path
]
570 # at this point, `filtered` has either all the volumes in self or is an
571 # actual filtered list if any filters were applied
574 for volume
in filtered
:
575 # all the tags we got need to match on the volume
576 matches
= all(volume
.tags
.get(k
) == str(v
) for k
, v
in lv_tags
.items())
578 tag_filtered
.append(volume
)
583 def filter(self
, lv_name
=None, vg_name
=None, lv_path
=None, lv_uuid
=None, lv_tags
=None):
585 Filter out volumes on top level attributes like ``lv_name`` or by
586 ``lv_tags`` where a dict is required. For example, to find a volume
587 that has an OSD ID of 0, the filter would look like::
589 lv_tags={'ceph.osd_id': '0'}
592 if not any([lv_name
, vg_name
, lv_path
, lv_uuid
, lv_tags
]):
593 raise TypeError('.filter() requires lv_name, vg_name, lv_path, lv_uuid, or tags (none given)')
594 # first find the filtered volumes with the values in self
595 filtered_volumes
= self
._filter
(
602 # then purge everything
604 # and add the filtered items
605 self
.extend(filtered_volumes
)
607 def get(self
, lv_name
=None, vg_name
=None, lv_path
=None, lv_uuid
=None, lv_tags
=None):
609 This is a bit expensive, since it will try to filter out all the
610 matching items in the list, filter them out applying anything that was
611 added and return the matching item.
613 This method does *not* alter the list, and it will raise an error if
614 multiple LVs are matched
616 It is useful to use ``tags`` when trying to find a specific logical volume,
617 but it can also lead to multiple lvs being found, since a lot of metadata
618 is shared between lvs of a distinct OSD.
620 if not any([lv_name
, vg_name
, lv_path
, lv_uuid
, lv_tags
]):
632 raise MultipleLVsError(lv_name
, lv_path
)
636 class PVolumes(list):
638 A list of all known (physical) volumes for the current system, with the ability
639 to filter them via keyword arguments.
646 # get all the pvs in the current system
647 for pv_item
in get_api_pvs():
648 self
.append(PVolume(**pv_item
))
652 Deplete all the items in the list, used internally only so that we can
653 dynamically allocate the items when filtering without the concern of
654 messing up the contents
658 def _filter(self
, pv_name
=None, pv_uuid
=None, pv_tags
=None):
660 The actual method that filters using a new list. Useful so that other
661 methods that do not want to alter the contents of the list (e.g.
662 ``self.find``) can operate safely.
664 filtered
= [i
for i
in self
]
666 filtered
= [i
for i
in filtered
if i
.pv_name
== pv_name
]
669 filtered
= [i
for i
in filtered
if i
.pv_uuid
== pv_uuid
]
671 # at this point, `filtered` has either all the physical volumes in self
672 # or is an actual filtered list if any filters were applied
675 for pvolume
in filtered
:
676 matches
= all(pvolume
.tags
.get(k
) == str(v
) for k
, v
in pv_tags
.items())
678 tag_filtered
.append(pvolume
)
679 # return the tag_filtered pvolumes here, the `filtered` list is no
685 def filter(self
, pv_name
=None, pv_uuid
=None, pv_tags
=None):
687 Filter out volumes on top level attributes like ``pv_name`` or by
688 ``pv_tags`` where a dict is required. For example, to find a physical volume
689 that has an OSD ID of 0, the filter would look like::
691 pv_tags={'ceph.osd_id': '0'}
694 if not any([pv_name
, pv_uuid
, pv_tags
]):
695 raise TypeError('.filter() requires pv_name, pv_uuid, or pv_tags (none given)')
696 # first find the filtered volumes with the values in self
697 filtered_volumes
= self
._filter
(
702 # then purge everything
704 # and add the filtered items
705 self
.extend(filtered_volumes
)
707 def get(self
, pv_name
=None, pv_uuid
=None, pv_tags
=None):
709 This is a bit expensive, since it will try to filter out all the
710 matching items in the list, filter them out applying anything that was
711 added and return the matching item.
713 This method does *not* alter the list, and it will raise an error if
714 multiple pvs are matched
716 It is useful to use ``tags`` when trying to find a specific logical volume,
717 but it can also lead to multiple pvs being found, since a lot of metadata
718 is shared between pvs of a distinct OSD.
720 if not any([pv_name
, pv_uuid
, pv_tags
]):
730 raise MultiplePVsError(pv_name
)
734 class VolumeGroup(object):
736 Represents an LVM group, with some top-level attributes like ``vg_name``
739 def __init__(self
, **kw
):
740 for k
, v
in kw
.items():
742 self
.name
= kw
['vg_name']
743 self
.tags
= parse_tags(kw
.get('vg_tags', ''))
746 return '<%s>' % self
.name
749 return self
.__str
__()
752 class Volume(object):
754 Represents a Logical Volume from LVM, with some top-level attributes like
755 ``lv_name`` and parsed tags as a dictionary of key/value pairs.
758 def __init__(self
, **kw
):
759 for k
, v
in kw
.items():
762 self
.name
= kw
['lv_name']
763 self
.tags
= parse_tags(kw
['lv_tags'])
764 self
.encrypted
= self
.tags
.get('ceph.encrypted', '0') == '1'
767 return '<%s>' % self
.lv_api
['lv_path']
770 return self
.__str
__()
774 obj
.update(self
.lv_api
)
775 obj
['tags'] = self
.tags
776 obj
['name'] = self
.name
777 obj
['type'] = self
.tags
['ceph.type']
778 obj
['path'] = self
.lv_path
781 def clear_tags(self
):
783 Removes all tags from the Logical Volume.
785 for k
, v
in self
.tags
.items():
786 tag
= "%s=%s" % (k
, v
)
787 process
.run(['lvchange', '--deltag', tag
, self
.lv_path
])
789 def set_tags(self
, tags
):
791 :param tags: A dictionary of tag names and values, like::
794 "ceph.osd_fsid": "aaa-fff-bbbb",
798 At the end of all modifications, the tags are refreshed to reflect
799 LVM's most current view.
801 for k
, v
in tags
.items():
803 # after setting all the tags, refresh them for the current object, use the
804 # lv_* identifiers to filter because those shouldn't change
805 lv_object
= get_lv(lv_name
=self
.lv_name
, lv_path
=self
.lv_path
)
806 self
.tags
= lv_object
.tags
808 def set_tag(self
, key
, value
):
810 Set the key/value pair as an LVM tag. Does not "refresh" the values of
811 the current object for its tags. Meant to be a "fire and forget" type
814 # remove it first if it exists
815 if self
.tags
.get(key
):
816 current_value
= self
.tags
[key
]
817 tag
= "%s=%s" % (key
, current_value
)
818 process
.call(['lvchange', '--deltag', tag
, self
.lv_api
['lv_path']])
823 '--addtag', '%s=%s' % (key
, value
), self
.lv_path
828 class PVolume(object):
830 Represents a Physical Volume from LVM, with some top-level attributes like
831 ``pv_name`` and parsed tags as a dictionary of key/value pairs.
834 def __init__(self
, **kw
):
835 for k
, v
in kw
.items():
838 self
.name
= kw
['pv_name']
839 self
.tags
= parse_tags(kw
['pv_tags'])
842 return '<%s>' % self
.pv_api
['pv_name']
845 return self
.__str
__()
847 def set_tags(self
, tags
):
849 :param tags: A dictionary of tag names and values, like::
852 "ceph.osd_fsid": "aaa-fff-bbbb",
856 At the end of all modifications, the tags are refreshed to reflect
857 LVM's most current view.
859 for k
, v
in tags
.items():
861 # after setting all the tags, refresh them for the current object, use the
862 # pv_* identifiers to filter because those shouldn't change
863 pv_object
= get_pv(pv_name
=self
.pv_name
, pv_uuid
=self
.pv_uuid
)
864 self
.tags
= pv_object
.tags
866 def set_tag(self
, key
, value
):
868 Set the key/value pair as an LVM tag. Does not "refresh" the values of
869 the current object for its tags. Meant to be a "fire and forget" type
872 **warning**: Altering tags on a PV has to be done ensuring that the
873 device is actually the one intended. ``pv_name`` is *not* a persistent
874 value, only ``pv_uuid`` is. Using ``pv_uuid`` is the best way to make
875 sure the device getting changed is the one needed.
877 # remove it first if it exists
878 if self
.tags
.get(key
):
879 current_value
= self
.tags
[key
]
880 tag
= "%s=%s" % (key
, current_value
)
881 process
.call(['pvchange', '--deltag', tag
, self
.pv_name
])
886 '--addtag', '%s=%s' % (key
, value
), self
.pv_name