]>
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.
6 from ceph_volume
import process
7 from ceph_volume
.exceptions
import MultipleLVsError
, MultipleVGsError
, MultiplePVsError
10 def _output_parser(output
, fields
):
12 Newer versions of LVM allow ``--reportformat=json``, but older versions,
13 like the one included in Xenial do not. LVM has the ability to filter and
14 format its output so we assume the output will be in a format this parser
15 can handle (using ',' as a delimiter)
17 :param fields: A string, possibly using ',' to group many items, as it
18 would be used on the CLI
19 :param output: The CLI output from the LVM call
21 field_items
= fields
.split(',')
24 # clear the leading/trailing whitespace
27 # remove the extra '"' in each field
28 line
= line
.replace('"', '')
30 # prevent moving forward with empty contents
34 # spliting on ';' because that is what the lvm call uses as
36 output_items
= [i
.strip() for i
in line
.split(';')]
37 # map the output to the fiels
39 dict(zip(field_items
, output_items
))
45 def parse_tags(lv_tags
):
47 Return a dictionary mapping of all the tags associated with
48 a Volume from the comma-separated tags coming from the LVM API
52 "ceph.osd_fsid=aaa-fff-bbbb,ceph.osd_id=0"
54 For the above example, the expected return value would be::
57 "ceph.osd_fsid": "aaa-fff-bbbb",
64 tags
= lv_tags
.split(',')
65 for tag_assignment
in tags
:
66 if not tag_assignment
.startswith('ceph.'):
68 key
, value
= tag_assignment
.split('=', 1)
69 tag_mapping
[key
] = value
76 Return the list of group volumes available in the system using flags to
77 include common metadata associated with them
79 Command and sample delimeted output, should look like::
81 $ vgs --noheadings --separator=';' \
82 -o vg_name,pv_count,lv_count,snap_count,vg_attr,vg_size,vg_free
83 ubuntubox-vg;1;2;0;wz--n-;299.52g;12.00m
84 osd_vg;3;1;0;wz--n-;29.21g;9.21g
87 fields
= 'vg_name,pv_count,lv_count,snap_count,vg_attr,vg_size,vg_free'
88 stdout
, stderr
, returncode
= process
.call(
89 ['vgs', '--noheadings', '--separator=";"', '-o', fields
]
91 return _output_parser(stdout
, fields
)
96 Return the list of logical volumes available in the system using flags to include common
97 metadata associated with them
99 Command and delimeted output, should look like::
101 $ lvs --noheadings --separator=';' -o lv_tags,lv_path,lv_name,vg_name
102 ;/dev/ubuntubox-vg/root;root;ubuntubox-vg
103 ;/dev/ubuntubox-vg/swap_1;swap_1;ubuntubox-vg
106 fields
= 'lv_tags,lv_path,lv_name,vg_name,lv_uuid'
107 stdout
, stderr
, returncode
= process
.call(
108 ['lvs', '--noheadings', '--separator=";"', '-o', fields
]
110 return _output_parser(stdout
, fields
)
115 Return the list of physical volumes configured for lvm and available in the
116 system using flags to include common metadata associated with them like the uuid
118 This will only return physical volumes set up to work with LVM.
120 Command and delimeted output, should look like::
122 $ pvs --noheadings --separator=';' -o pv_name,pv_tags,pv_uuid
124 /dev/sdv;;07A4F654-4162-4600-8EB3-88D1E42F368D
127 fields
= 'pv_name,pv_tags,pv_uuid,vg_name'
129 stdout
, stderr
, returncode
= process
.call(
130 ['pvs', '--no-heading', '--separator=";"', '-o', fields
]
133 return _output_parser(stdout
, fields
)
136 def get_lv_from_argument(argument
):
138 Helper proxy function that consumes a possible logical volume passed in from the CLI
139 in the form of `vg/lv`, but with some validation so that an argument that is a full
140 path to a device can be ignored
142 if argument
.startswith('/'):
143 lv
= get_lv(lv_path
=argument
)
146 vg_name
, lv_name
= argument
.split('/')
147 except (ValueError, AttributeError):
149 return get_lv(lv_name
=lv_name
, vg_name
=vg_name
)
152 def get_lv(lv_name
=None, vg_name
=None, lv_path
=None, lv_uuid
=None, lv_tags
=None):
154 Return a matching lv for the current system, requiring ``lv_name``,
155 ``vg_name``, ``lv_path`` or ``tags``. Raises an error if more than one lv
158 It is useful to use ``tags`` when trying to find a specific logical volume,
159 but it can also lead to multiple lvs being found, since a lot of metadata
160 is shared between lvs of a distinct OSD.
162 if not any([lv_name
, vg_name
, lv_path
, lv_uuid
, lv_tags
]):
166 lv_name
=lv_name
, vg_name
=vg_name
, lv_path
=lv_path
, lv_uuid
=lv_uuid
,
171 def get_pv(pv_name
=None, pv_uuid
=None, pv_tags
=None):
173 Return a matching pv (physical volume) for the current system, requiring
174 ``pv_name``, ``pv_uuid``, or ``pv_tags``. Raises an error if more than one
177 if not any([pv_name
, pv_uuid
, pv_tags
]):
180 return pvs
.get(pv_name
=pv_name
, pv_uuid
=pv_uuid
, pv_tags
=pv_tags
)
183 def create_pv(device
):
185 Create a physical volume from a device, useful when devices need to be later mapped
192 '--yes', # answer yes to any prompts
197 def create_vg(name
, *devices
):
199 Create a Volume Group. Command looks like::
201 vgcreate --force --yes group_name device
203 Once created the volume group is returned as a ``VolumeGroup`` object
209 name
] + list(devices
)
212 vg
= get_vg(vg_name
=name
)
216 def remove_vg(vg_name
):
218 Removes a volume group.
220 fail_msg
= "Unable to remove vg %s".format(vg_name
)
232 def remove_pv(pv_name
):
234 Removes a physical volume.
236 fail_msg
= "Unable to remove vg %s".format(pv_name
)
250 Removes a logical volume given it's absolute path.
252 Will return True if the lv is successfully removed or
253 raises a RuntimeError if the removal fails.
255 stdout
, stderr
, returncode
= process
.call(
263 terminal_verbose
=True,
266 raise RuntimeError("Unable to remove %s".format(path
))
270 def create_lv(name
, group
, size
=None, tags
=None):
272 Create a Logical Volume in a Volume Group. Command looks like::
274 lvcreate -L 50G -n gfslv vg0
276 ``name``, ``group``, are required. If ``size`` is provided it must follow
277 lvm's size notation (like 1G, or 20M). Tags are an optional dictionary and is expected to
278 conform to the convention of prefixing them with "ceph." like::
280 {"ceph.block_device": "/dev/ceph/osd-1"}
282 # XXX add CEPH_VOLUME_LVM_DEBUG to enable -vvvv on lv operations
284 'journal': 'ceph.journal_device',
285 'data': 'ceph.data_device',
286 'block': 'ceph.block_device',
287 'wal': 'ceph.wal_device',
288 'db': 'ceph.db_device',
289 'lockbox': 'ceph.lockbox_device', # XXX might not ever need this lockbox sorcery
299 # create the lv with all the space available, this is needed because the
300 # system call is different for LVM
310 lv
= get_lv(lv_name
=name
, vg_name
=group
)
313 # when creating a distinct type, the caller doesn't know what the path will
314 # be so this function will set it after creation using the mapping
315 path_tag
= type_path_tag
.get(tags
.get('ceph.type'))
318 {path_tag
: lv
.lv_path
}
323 def get_vg(vg_name
=None, vg_tags
=None):
325 Return a matching vg for the current system, requires ``vg_name`` or
326 ``tags``. Raises an error if more than one vg is found.
328 It is useful to use ``tags`` when trying to find a specific volume group,
329 but it can also lead to multiple vgs being found.
331 if not any([vg_name
, vg_tags
]):
334 return vgs
.get(vg_name
=vg_name
, vg_tags
=vg_tags
)
337 class VolumeGroups(list):
339 A list of all known volume groups for the current system, with the ability
340 to filter them via keyword arguments.
347 # get all the vgs in the current system
348 for vg_item
in get_api_vgs():
349 self
.append(VolumeGroup(**vg_item
))
353 Deplete all the items in the list, used internally only so that we can
354 dynamically allocate the items when filtering without the concern of
355 messing up the contents
359 def _filter(self
, vg_name
=None, vg_tags
=None):
361 The actual method that filters using a new list. Useful so that other
362 methods that do not want to alter the contents of the list (e.g.
363 ``self.find``) can operate safely.
365 .. note:: ``vg_tags`` is not yet implemented
367 filtered
= [i
for i
in self
]
369 filtered
= [i
for i
in filtered
if i
.vg_name
== vg_name
]
371 # at this point, `filtered` has either all the volumes in self or is an
372 # actual filtered list if any filters were applied
375 for volume
in filtered
:
376 matches
= all(volume
.tags
.get(k
) == str(v
) for k
, v
in vg_tags
.items())
378 tag_filtered
.append(volume
)
383 def filter(self
, vg_name
=None, vg_tags
=None):
385 Filter out groups on top level attributes like ``vg_name`` or by
386 ``vg_tags`` where a dict is required. For example, to find a Ceph group
387 with dmcache as the type, the filter would look like::
389 vg_tags={'ceph.type': 'dmcache'}
391 .. warning:: These tags are not documented because they are currently
392 unused, but are here to maintain API consistency
394 if not any([vg_name
, vg_tags
]):
395 raise TypeError('.filter() requires vg_name or vg_tags (none given)')
396 # first find the filtered volumes with the values in self
397 filtered_groups
= self
._filter
(
401 # then purge everything
403 # and add the filtered items
404 self
.extend(filtered_groups
)
406 def get(self
, vg_name
=None, vg_tags
=None):
408 This is a bit expensive, since it will try to filter out all the
409 matching items in the list, filter them out applying anything that was
410 added and return the matching item.
412 This method does *not* alter the list, and it will raise an error if
413 multiple VGs are matched
415 It is useful to use ``tags`` when trying to find a specific volume group,
416 but it can also lead to multiple vgs being found (although unlikely)
418 if not any([vg_name
, vg_tags
]):
427 # this is probably never going to happen, but it is here to keep
428 # the API code consistent
429 raise MultipleVGsError(vg_name
)
435 A list of all known (logical) volumes for the current system, with the ability
436 to filter them via keyword arguments.
443 # get all the lvs in the current system
444 for lv_item
in get_api_lvs():
445 self
.append(Volume(**lv_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
, lv_name
=None, vg_name
=None, lv_path
=None, lv_uuid
=None, lv_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 filtered
= [i
for i
in self
]
463 filtered
= [i
for i
in filtered
if i
.lv_name
== lv_name
]
466 filtered
= [i
for i
in filtered
if i
.vg_name
== vg_name
]
469 filtered
= [i
for i
in filtered
if i
.lv_uuid
== lv_uuid
]
472 filtered
= [i
for i
in filtered
if i
.lv_path
== lv_path
]
474 # at this point, `filtered` has either all the volumes in self or is an
475 # actual filtered list if any filters were applied
478 for volume
in filtered
:
479 # all the tags we got need to match on the volume
480 matches
= all(volume
.tags
.get(k
) == str(v
) for k
, v
in lv_tags
.items())
482 tag_filtered
.append(volume
)
487 def filter(self
, lv_name
=None, vg_name
=None, lv_path
=None, lv_uuid
=None, lv_tags
=None):
489 Filter out volumes on top level attributes like ``lv_name`` or by
490 ``lv_tags`` where a dict is required. For example, to find a volume
491 that has an OSD ID of 0, the filter would look like::
493 lv_tags={'ceph.osd_id': '0'}
496 if not any([lv_name
, vg_name
, lv_path
, lv_uuid
, lv_tags
]):
497 raise TypeError('.filter() requires lv_name, vg_name, lv_path, lv_uuid, or tags (none given)')
498 # first find the filtered volumes with the values in self
499 filtered_volumes
= self
._filter
(
506 # then purge everything
508 # and add the filtered items
509 self
.extend(filtered_volumes
)
511 def get(self
, lv_name
=None, vg_name
=None, lv_path
=None, lv_uuid
=None, lv_tags
=None):
513 This is a bit expensive, since it will try to filter out all the
514 matching items in the list, filter them out applying anything that was
515 added and return the matching item.
517 This method does *not* alter the list, and it will raise an error if
518 multiple LVs are matched
520 It is useful to use ``tags`` when trying to find a specific logical volume,
521 but it can also lead to multiple lvs being found, since a lot of metadata
522 is shared between lvs of a distinct OSD.
524 if not any([lv_name
, vg_name
, lv_path
, lv_uuid
, lv_tags
]):
536 raise MultipleLVsError(lv_name
, lv_path
)
540 class PVolumes(list):
542 A list of all known (physical) volumes for the current system, with the ability
543 to filter them via keyword arguments.
550 # get all the pvs in the current system
551 for pv_item
in get_api_pvs():
552 self
.append(PVolume(**pv_item
))
556 Deplete all the items in the list, used internally only so that we can
557 dynamically allocate the items when filtering without the concern of
558 messing up the contents
562 def _filter(self
, pv_name
=None, pv_uuid
=None, pv_tags
=None):
564 The actual method that filters using a new list. Useful so that other
565 methods that do not want to alter the contents of the list (e.g.
566 ``self.find``) can operate safely.
568 filtered
= [i
for i
in self
]
570 filtered
= [i
for i
in filtered
if i
.pv_name
== pv_name
]
573 filtered
= [i
for i
in filtered
if i
.pv_uuid
== pv_uuid
]
575 # at this point, `filtered` has either all the physical volumes in self
576 # or is an actual filtered list if any filters were applied
579 for pvolume
in filtered
:
580 matches
= all(pvolume
.tags
.get(k
) == str(v
) for k
, v
in pv_tags
.items())
582 tag_filtered
.append(pvolume
)
583 # return the tag_filtered pvolumes here, the `filtered` list is no
589 def filter(self
, pv_name
=None, pv_uuid
=None, pv_tags
=None):
591 Filter out volumes on top level attributes like ``pv_name`` or by
592 ``pv_tags`` where a dict is required. For example, to find a physical volume
593 that has an OSD ID of 0, the filter would look like::
595 pv_tags={'ceph.osd_id': '0'}
598 if not any([pv_name
, pv_uuid
, pv_tags
]):
599 raise TypeError('.filter() requires pv_name, pv_uuid, or pv_tags (none given)')
600 # first find the filtered volumes with the values in self
601 filtered_volumes
= self
._filter
(
606 # then purge everything
608 # and add the filtered items
609 self
.extend(filtered_volumes
)
611 def get(self
, pv_name
=None, pv_uuid
=None, pv_tags
=None):
613 This is a bit expensive, since it will try to filter out all the
614 matching items in the list, filter them out applying anything that was
615 added and return the matching item.
617 This method does *not* alter the list, and it will raise an error if
618 multiple pvs are matched
620 It is useful to use ``tags`` when trying to find a specific logical volume,
621 but it can also lead to multiple pvs being found, since a lot of metadata
622 is shared between pvs of a distinct OSD.
624 if not any([pv_name
, pv_uuid
, pv_tags
]):
634 raise MultiplePVsError(pv_name
)
638 class VolumeGroup(object):
640 Represents an LVM group, with some top-level attributes like ``vg_name``
643 def __init__(self
, **kw
):
644 for k
, v
in kw
.items():
646 self
.name
= kw
['vg_name']
647 self
.tags
= parse_tags(kw
.get('vg_tags', ''))
650 return '<%s>' % self
.name
653 return self
.__str
__()
656 class Volume(object):
658 Represents a Logical Volume from LVM, with some top-level attributes like
659 ``lv_name`` and parsed tags as a dictionary of key/value pairs.
662 def __init__(self
, **kw
):
663 for k
, v
in kw
.items():
666 self
.name
= kw
['lv_name']
667 self
.tags
= parse_tags(kw
['lv_tags'])
670 return '<%s>' % self
.lv_api
['lv_path']
673 return self
.__str
__()
677 obj
.update(self
.lv_api
)
678 obj
['tags'] = self
.tags
679 obj
['name'] = self
.name
680 obj
['type'] = self
.tags
['ceph.type']
681 obj
['path'] = self
.lv_path
684 def clear_tags(self
):
686 Removes all tags from the Logical Volume.
688 for k
, v
in self
.tags
.items():
689 tag
= "%s=%s" % (k
, v
)
690 process
.run(['lvchange', '--deltag', tag
, self
.lv_path
])
692 def set_tags(self
, tags
):
694 :param tags: A dictionary of tag names and values, like::
697 "ceph.osd_fsid": "aaa-fff-bbbb",
701 At the end of all modifications, the tags are refreshed to reflect
702 LVM's most current view.
704 for k
, v
in tags
.items():
706 # after setting all the tags, refresh them for the current object, use the
707 # lv_* identifiers to filter because those shouldn't change
708 lv_object
= get_lv(lv_name
=self
.lv_name
, lv_path
=self
.lv_path
)
709 self
.tags
= lv_object
.tags
711 def set_tag(self
, key
, value
):
713 Set the key/value pair as an LVM tag. Does not "refresh" the values of
714 the current object for its tags. Meant to be a "fire and forget" type
717 # remove it first if it exists
718 if self
.tags
.get(key
):
719 current_value
= self
.tags
[key
]
720 tag
= "%s=%s" % (key
, current_value
)
721 process
.call(['lvchange', '--deltag', tag
, self
.lv_api
['lv_path']])
726 '--addtag', '%s=%s' % (key
, value
), self
.lv_path
731 class PVolume(object):
733 Represents a Physical Volume from LVM, with some top-level attributes like
734 ``pv_name`` and parsed tags as a dictionary of key/value pairs.
737 def __init__(self
, **kw
):
738 for k
, v
in kw
.items():
741 self
.name
= kw
['pv_name']
742 self
.tags
= parse_tags(kw
['pv_tags'])
745 return '<%s>' % self
.pv_api
['pv_name']
748 return self
.__str
__()
750 def set_tags(self
, tags
):
752 :param tags: A dictionary of tag names and values, like::
755 "ceph.osd_fsid": "aaa-fff-bbbb",
759 At the end of all modifications, the tags are refreshed to reflect
760 LVM's most current view.
762 for k
, v
in tags
.items():
764 # after setting all the tags, refresh them for the current object, use the
765 # pv_* identifiers to filter because those shouldn't change
766 pv_object
= get_pv(pv_name
=self
.pv_name
, pv_uuid
=self
.pv_uuid
)
767 self
.tags
= pv_object
.tags
769 def set_tag(self
, key
, value
):
771 Set the key/value pair as an LVM tag. Does not "refresh" the values of
772 the current object for its tags. Meant to be a "fire and forget" type
775 **warning**: Altering tags on a PV has to be done ensuring that the
776 device is actually the one intended. ``pv_name`` is *not* a persistent
777 value, only ``pv_uuid`` is. Using ``pv_uuid`` is the best way to make
778 sure the device getting changed is the one needed.
780 # remove it first if it exists
781 if self
.tags
.get(key
):
782 current_value
= self
.tags
[key
]
783 tag
= "%s=%s" % (key
, current_value
)
784 process
.call(['pvchange', '--deltag', tag
, self
.pv_name
])
789 '--addtag', '%s=%s' % (key
, value
), self
.pv_name