]>
git.proxmox.com Git - ceph.git/blob - ceph/src/ceph-volume/ceph_volume/devices/lvm/api.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
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 key
, value
= tag_assignment
.split('=', 1)
67 tag_mapping
[key
] = value
74 Return the list of group volumes available in the system using flags to
75 include common metadata associated with them
77 Command and sample delimeted output, should look like::
79 $ sudo vgs --noheadings --separator=';' \
80 -o vg_name,pv_count,lv_count,snap_count,vg_attr,vg_size,vg_free
81 ubuntubox-vg;1;2;0;wz--n-;299.52g;12.00m
82 osd_vg;3;1;0;wz--n-;29.21g;9.21g
85 fields
= 'vg_name,pv_count,lv_count,snap_count,vg_attr,vg_size,vg_free'
86 stdout
, stderr
, returncode
= process
.call(
87 ['sudo', 'vgs', '--noheadings', '--separator=";"', '-o', fields
]
89 return _output_parser(stdout
, fields
)
94 Return the list of logical volumes available in the system using flags to include common
95 metadata associated with them
97 Command and delimeted output, should look like::
99 $ sudo lvs --noheadings --separator=';' -o lv_tags,lv_path,lv_name,vg_name
100 ;/dev/ubuntubox-vg/root;root;ubuntubox-vg
101 ;/dev/ubuntubox-vg/swap_1;swap_1;ubuntubox-vg
104 fields
= 'lv_tags,lv_path,lv_name,vg_name'
105 stdout
, stderr
, returncode
= process
.call(
106 ['sudo', 'lvs', '--noheadings', '--separator=";"', '-o', fields
]
108 return _output_parser(stdout
, fields
)
111 def get_lv(lv_name
=None, vg_name
=None, lv_path
=None, lv_tags
=None):
113 Return a matching lv for the current system, requiring ``lv_name``,
114 ``vg_name``, ``lv_path`` or ``tags``. Raises an error if more than one lv
117 It is useful to use ``tags`` when trying to find a specific logical volume,
118 but it can also lead to multiple lvs being found, since a lot of metadata
119 is shared between lvs of a distinct OSD.
121 if not any([lv_name
, vg_name
, lv_path
, lv_tags
]):
124 return lvs
.get(lv_name
=lv_name
, vg_name
=vg_name
, lv_path
=lv_path
, lv_tags
=lv_tags
)
127 def create_lv(name
, group
, size
=None, **tags
):
129 Create a Logical Volume in a Volume Group. Command looks like::
131 lvcreate -L 50G -n gfslv vg0
133 ``name``, ``group``, and ``size`` are required. Tags are optional and are "translated" to include
134 the prefixes for the Ceph LVM tag API.
137 # XXX add CEPH_VOLUME_LVM_DEBUG to enable -vvvv on lv operations
139 'journal': 'ceph.journal_device',
140 'data': 'ceph.data_device',
141 'block': 'ceph.block',
144 'lockbox': 'ceph.lockbox_device',
155 # create the lv with all the space available, this is needed because the
156 # system call is different for LVM
167 lv
= get_lv(lv_name
=name
, vg_name
=group
)
169 for k
, v
in tags
.items():
170 ceph_tags
['ceph.%s' % k
] = v
171 lv
.set_tags(ceph_tags
)
173 # when creating a distinct type, the caller doesn't know what the path will
174 # be so this function will set it after creation using the mapping
175 path_tag
= type_path_tag
[tags
['type']]
177 {path_tag
: lv
.lv_path
}
182 def get_vg(vg_name
=None, vg_tags
=None):
184 Return a matching vg for the current system, requires ``vg_name`` or
185 ``tags``. Raises an error if more than one vg is found.
187 It is useful to use ``tags`` when trying to find a specific volume group,
188 but it can also lead to multiple vgs being found.
190 if not any([vg_name
, vg_tags
]):
193 return vgs
.get(vg_name
=vg_name
, vg_tags
=vg_tags
)
196 class VolumeGroups(list):
198 A list of all known volume groups for the current system, with the ability
199 to filter them via keyword arguments.
206 # get all the vgs in the current system
207 for vg_item
in get_api_vgs():
208 self
.append(VolumeGroup(**vg_item
))
212 Deplete all the items in the list, used internally only so that we can
213 dynamically allocate the items when filtering without the concern of
214 messing up the contents
218 def _filter(self
, vg_name
=None, vg_tags
=None):
220 The actual method that filters using a new list. Useful so that other
221 methods that do not want to alter the contents of the list (e.g.
222 ``self.find``) can operate safely.
224 .. note:: ``vg_tags`` is not yet implemented
226 filtered
= [i
for i
in self
]
228 filtered
= [i
for i
in filtered
if i
.vg_name
== vg_name
]
230 # at this point, `filtered` has either all the volumes in self or is an
231 # actual filtered list if any filters were applied
234 for k
, v
in vg_tags
.items():
235 for volume
in filtered
:
236 if volume
.tags
.get(k
) == str(v
):
237 if volume
not in tag_filtered
:
238 tag_filtered
.append(volume
)
239 # return the tag_filtered volumes here, the `filtered` list is no
245 def filter(self
, vg_name
=None, vg_tags
=None):
247 Filter out groups on top level attributes like ``vg_name`` or by
248 ``vg_tags`` where a dict is required. For example, to find a Ceph group
249 with dmcache as the type, the filter would look like::
251 vg_tags={'ceph.type': 'dmcache'}
253 .. warning:: These tags are not documented because they are currently
254 unused, but are here to maintain API consistency
256 if not any([vg_name
, vg_tags
]):
257 raise TypeError('.filter() requires vg_name or vg_tags (none given)')
258 # first find the filtered volumes with the values in self
259 filtered_groups
= self
._filter
(
263 # then purge everything
265 # and add the filtered items
266 self
.extend(filtered_groups
)
268 def get(self
, vg_name
=None, vg_tags
=None):
270 This is a bit expensive, since it will try to filter out all the
271 matching items in the list, filter them out applying anything that was
272 added and return the matching item.
274 This method does *not* alter the list, and it will raise an error if
275 multiple VGs are matched
277 It is useful to use ``tags`` when trying to find a specific volume group,
278 but it can also lead to multiple vgs being found (although unlikely)
280 if not any([vg_name
, vg_tags
]):
289 # this is probably never going to happen, but it is here to keep
290 # the API code consistent
291 raise MultipleVGsError(vg_name
)
297 A list of all known (logical) volumes for the current system, with the ability
298 to filter them via keyword arguments.
305 # get all the lvs in the current system
306 for lv_item
in get_api_lvs():
307 self
.append(Volume(**lv_item
))
311 Deplete all the items in the list, used internally only so that we can
312 dynamically allocate the items when filtering without the concern of
313 messing up the contents
317 def _filter(self
, lv_name
=None, vg_name
=None, lv_path
=None, lv_tags
=None):
319 The actual method that filters using a new list. Useful so that other
320 methods that do not want to alter the contents of the list (e.g.
321 ``self.find``) can operate safely.
323 filtered
= [i
for i
in self
]
325 filtered
= [i
for i
in filtered
if i
.lv_name
== lv_name
]
328 filtered
= [i
for i
in filtered
if i
.vg_name
== vg_name
]
331 filtered
= [i
for i
in filtered
if i
.lv_path
== lv_path
]
333 # at this point, `filtered` has either all the volumes in self or is an
334 # actual filtered list if any filters were applied
337 for k
, v
in lv_tags
.items():
338 for volume
in filtered
:
339 if volume
.tags
.get(k
) == str(v
):
340 if volume
not in tag_filtered
:
341 tag_filtered
.append(volume
)
342 # return the tag_filtered volumes here, the `filtered` list is no
348 def filter(self
, lv_name
=None, vg_name
=None, lv_path
=None, lv_tags
=None):
350 Filter out volumes on top level attributes like ``lv_name`` or by
351 ``lv_tags`` where a dict is required. For example, to find a volume
352 that has an OSD ID of 0, the filter would look like::
354 lv_tags={'ceph.osd_id': '0'}
357 if not any([lv_name
, vg_name
, lv_path
, lv_tags
]):
358 raise TypeError('.filter() requires lv_name, vg_name, lv_path, or tags (none given)')
359 # first find the filtered volumes with the values in self
360 filtered_volumes
= self
._filter
(
366 # then purge everything
368 # and add the filtered items
369 self
.extend(filtered_volumes
)
371 def get(self
, lv_name
=None, vg_name
=None, lv_path
=None, lv_tags
=None):
373 This is a bit expensive, since it will try to filter out all the
374 matching items in the list, filter them out applying anything that was
375 added and return the matching item.
377 This method does *not* alter the list, and it will raise an error if
378 multiple LVs are matched
380 It is useful to use ``tags`` when trying to find a specific logical volume,
381 but it can also lead to multiple lvs being found, since a lot of metadata
382 is shared between lvs of a distinct OSD.
384 if not any([lv_name
, vg_name
, lv_path
, lv_tags
]):
395 raise MultipleLVsError(lv_name
, lv_path
)
399 class VolumeGroup(object):
401 Represents an LVM group, with some top-level attributes like ``vg_name``
404 def __init__(self
, **kw
):
405 for k
, v
in kw
.items():
407 self
.name
= kw
['vg_name']
408 self
.tags
= parse_tags(kw
.get('vg_tags', ''))
411 return '<%s>' % self
.name
414 return self
.__str
__()
417 class Volume(object):
419 Represents a Logical Volume from LVM, with some top-level attributes like
420 ``lv_name`` and parsed tags as a dictionary of key/value pairs.
423 def __init__(self
, **kw
):
424 for k
, v
in kw
.items():
427 self
.name
= kw
['lv_name']
428 self
.tags
= parse_tags(kw
['lv_tags'])
431 return '<%s>' % self
.lv_api
['lv_path']
434 return self
.__str
__()
436 def set_tags(self
, tags
):
438 :param tags: A dictionary of tag names and values, like::
441 "ceph.osd_fsid": "aaa-fff-bbbb",
445 At the end of all modifications, the tags are refreshed to reflect
446 LVM's most current view.
448 for k
, v
in tags
.items():
450 # after setting all the tags, refresh them for the current object, use the
451 # lv_* identifiers to filter because those shouldn't change
452 lv_object
= get_lv(lv_name
=self
.lv_name
, lv_path
=self
.lv_path
)
453 self
.tags
= lv_object
.tags
455 def set_tag(self
, key
, value
):
457 Set the key/value pair as an LVM tag. Does not "refresh" the values of
458 the current object for its tags. Meant to be a "fire and forget" type
461 # remove it first if it exists
462 if self
.tags
.get(key
):
463 current_value
= self
.tags
[key
]
464 tag
= "%s=%s" % (key
, current_value
)
465 process
.call(['sudo', 'lvchange', '--deltag', tag
, self
.lv_api
['lv_path']])
470 '--addtag', '%s=%s' % (key
, value
), self
.lv_path