]> git.proxmox.com Git - ceph.git/blame - ceph/src/ceph-volume/ceph_volume/devices/lvm/api.py
update sources to v12.1.3
[ceph.git] / ceph / src / ceph-volume / ceph_volume / devices / lvm / api.py
CommitLineData
d2e6a577
FG
1"""
2API for CRUD lvm tag operations. Follows the Ceph LVM tag naming convention
3that prefixes tags with ``ceph.`` and uses ``=`` for assignment, and provides
4set of utilities for interacting with LVM.
5"""
6import json
7from ceph_volume import process
8from ceph_volume.exceptions import MultipleLVsError, MultipleVGsError
9
10
11def parse_tags(lv_tags):
12 """
13 Return a dictionary mapping of all the tags associated with
14 a Volume from the comma-separated tags coming from the LVM API
15
16 Input look like::
17
18 "ceph.osd_fsid=aaa-fff-bbbb,ceph.osd_id=0"
19
20 For the above example, the expected return value would be::
21
22 {
23 "ceph.osd_fsid": "aaa-fff-bbbb",
24 "ceph.osd_id": "0"
25 }
26 """
27 if not lv_tags:
28 return {}
29 tag_mapping = {}
30 tags = lv_tags.split(',')
31 for tag_assignment in tags:
32 key, value = tag_assignment.split('=', 1)
33 tag_mapping[key] = value
34
35 return tag_mapping
36
37
38def get_api_vgs():
39 """
40 Return the list of group volumes available in the system using flags to include common
41 metadata associated with them
42
43 Command and sample JSON output, should look like::
44
45 $ sudo vgs --reportformat=json
46 {
47 "report": [
48 {
49 "vg": [
50 {
51 "vg_name":"VolGroup00",
52 "pv_count":"1",
53 "lv_count":"2",
54 "snap_count":"0",
55 "vg_attr":"wz--n-",
56 "vg_size":"38.97g",
57 "vg_free":"0 "},
58 {
59 "vg_name":"osd_vg",
60 "pv_count":"3",
61 "lv_count":"1",
62 "snap_count":"0",
63 "vg_attr":"wz--n-",
64 "vg_size":"32.21g",
65 "vg_free":"9.21g"
66 }
67 ]
68 }
69 ]
70 }
71
72 """
73 stdout, stderr, returncode = process.call(
74 [
75 'sudo', 'vgs', '--reportformat=json'
76 ]
77 )
78 report = json.loads(''.join(stdout))
79 for report_item in report.get('report', []):
80 # is it possible to get more than one item in "report" ?
81 return report_item['vg']
82 return []
83
84
85def get_api_lvs():
86 """
87 Return the list of logical volumes available in the system using flags to include common
88 metadata associated with them
89
90 Command and sample JSON output, should look like::
91
92 $ sudo lvs -o lv_tags,lv_path,lv_name,vg_name --reportformat=json
93 {
94 "report": [
95 {
96 "lv": [
97 {
98 "lv_tags":"",
99 "lv_path":"/dev/VolGroup00/LogVol00",
100 "lv_name":"LogVol00",
101 "vg_name":"VolGroup00"},
102 {
103 "lv_tags":"ceph.osd_fsid=aaa-fff-0000,ceph.osd_fsid=aaa-fff-bbbb,ceph.osd_id=0",
104 "lv_path":"/dev/osd_vg/OriginLV",
105 "lv_name":"OriginLV",
106 "vg_name":"osd_vg"
107 }
108 ]
109 }
110 ]
111 }
112
113 """
114 stdout, stderr, returncode = process.call(
115 ['sudo', 'lvs', '-o', 'lv_tags,lv_path,lv_name,vg_name', '--reportformat=json'])
116 report = json.loads(''.join(stdout))
117 for report_item in report.get('report', []):
118 # is it possible to get more than one item in "report" ?
119 return report_item['lv']
120 return []
121
122
123def get_lv(lv_name=None, vg_name=None, lv_path=None, lv_tags=None):
124 """
125 Return a matching lv for the current system, requiring ``lv_name``,
126 ``vg_name``, ``lv_path`` or ``tags``. Raises an error if more than one lv
127 is found.
128
129 It is useful to use ``tags`` when trying to find a specific logical volume,
130 but it can also lead to multiple lvs being found, since a lot of metadata
131 is shared between lvs of a distinct OSD.
132 """
133 if not any([lv_name, vg_name, lv_path, lv_tags]):
134 return None
135 lvs = Volumes()
136 return lvs.get(lv_name=lv_name, vg_name=vg_name, lv_path=lv_path, lv_tags=lv_tags)
137
138
139def create_lv(name, group, size=None, **tags):
140 """
141 Create a Logical Volume in a Volume Group. Command looks like::
142
143 lvcreate -L 50G -n gfslv vg0
144
145 ``name``, ``group``, and ``size`` are required. Tags are optional and are "translated" to include
146 the prefixes for the Ceph LVM tag API.
147
148 """
149 # XXX add CEPH_VOLUME_LVM_DEBUG to enable -vvvv on lv operations
150 type_path_tag = {
151 'journal': 'ceph.journal_device',
152 'data': 'ceph.data_device',
153 'block': 'ceph.block',
154 'wal': 'ceph.wal',
155 'db': 'ceph.db',
156 'lockbox': 'ceph.lockbox_device',
157 }
158 if size:
159 process.run([
160 'sudo',
161 'lvcreate',
162 '--yes',
163 '-L',
164 '%sG' % size,
165 '-n', name, group
166 ])
167 # create the lv with all the space available, this is needed because the
168 # system call is different for LVM
169 else:
170 process.run([
171 'sudo',
172 'lvcreate',
173 '--yes',
174 '-l',
175 '100%FREE',
176 '-n', name, group
177 ])
178
179 lv = get_lv(lv_name=name, vg_name=group)
180 ceph_tags = {}
181 for k, v in tags.items():
182 ceph_tags['ceph.%s' % k] = v
183 lv.set_tags(ceph_tags)
184
185 # when creating a distinct type, the caller doesn't know what the path will
186 # be so this function will set it after creation using the mapping
187 path_tag = type_path_tag[tags['type']]
188 lv.set_tags(
189 {path_tag: lv.lv_path}
190 )
191 return lv
192
193
194def get_vg(vg_name=None, vg_tags=None):
195 """
196 Return a matching vg for the current system, requires ``vg_name`` or
197 ``tags``. Raises an error if more than one vg is found.
198
199 It is useful to use ``tags`` when trying to find a specific volume group,
200 but it can also lead to multiple vgs being found.
201 """
202 if not any([vg_name, vg_tags]):
203 return None
204 vgs = VolumeGroups()
205 return vgs.get(vg_name=vg_name, vg_tags=vg_tags)
206
207
208class VolumeGroups(list):
209 """
210 A list of all known volume groups for the current system, with the ability
211 to filter them via keyword arguments.
212 """
213
214 def __init__(self):
215 self._populate()
216
217 def _populate(self):
218 # get all the vgs in the current system
219 for vg_item in get_api_vgs():
220 self.append(VolumeGroup(**vg_item))
221
222 def _purge(self):
223 """
224 Deplete all the items in the list, used internally only so that we can
225 dynamically allocate the items when filtering without the concern of
226 messing up the contents
227 """
228 self[:] = []
229
230 def _filter(self, vg_name=None, vg_tags=None):
231 """
232 The actual method that filters using a new list. Useful so that other
233 methods that do not want to alter the contents of the list (e.g.
234 ``self.find``) can operate safely.
235
236 .. note:: ``vg_tags`` is not yet implemented
237 """
238 filtered = [i for i in self]
239 if vg_name:
240 filtered = [i for i in filtered if i.vg_name == vg_name]
241
242 # at this point, `filtered` has either all the volumes in self or is an
243 # actual filtered list if any filters were applied
244 if vg_tags:
245 tag_filtered = []
246 for k, v in vg_tags.items():
247 for volume in filtered:
248 if volume.tags.get(k) == str(v):
249 if volume not in tag_filtered:
250 tag_filtered.append(volume)
251 # return the tag_filtered volumes here, the `filtered` list is no
252 # longer useable
253 return tag_filtered
254
255 return filtered
256
257 def filter(self, vg_name=None, vg_tags=None):
258 """
259 Filter out groups on top level attributes like ``vg_name`` or by
260 ``vg_tags`` where a dict is required. For example, to find a Ceph group
261 with dmcache as the type, the filter would look like::
262
263 vg_tags={'ceph.type': 'dmcache'}
264
265 .. warning:: These tags are not documented because they are currently
266 unused, but are here to maintain API consistency
267 """
268 if not any([vg_name, vg_tags]):
269 raise TypeError('.filter() requires vg_name or vg_tags (none given)')
270 # first find the filtered volumes with the values in self
271 filtered_groups = self._filter(
272 vg_name=vg_name,
273 vg_tags=vg_tags
274 )
275 # then purge everything
276 self._purge()
277 # and add the filtered items
278 self.extend(filtered_groups)
279
280 def get(self, vg_name=None, vg_tags=None):
281 """
282 This is a bit expensive, since it will try to filter out all the
283 matching items in the list, filter them out applying anything that was
284 added and return the matching item.
285
286 This method does *not* alter the list, and it will raise an error if
287 multiple VGs are matched
288
289 It is useful to use ``tags`` when trying to find a specific volume group,
290 but it can also lead to multiple vgs being found (although unlikely)
291 """
292 if not any([vg_name, vg_tags]):
293 return None
294 vgs = self._filter(
295 vg_name=vg_name,
296 vg_tags=vg_tags
297 )
298 if not vgs:
299 return None
300 if len(vgs) > 1:
301 # this is probably never going to happen, but it is here to keep
302 # the API code consistent
303 raise MultipleVGsError(vg_name)
304 return vgs[0]
305
306
307class Volumes(list):
308 """
309 A list of all known (logical) volumes for the current system, with the ability
310 to filter them via keyword arguments.
311 """
312
313 def __init__(self):
314 self._populate()
315
316 def _populate(self):
317 # get all the lvs in the current system
318 for lv_item in get_api_lvs():
319 self.append(Volume(**lv_item))
320
321 def _purge(self):
322 """
323 Deplete all the items in the list, used internally only so that we can
324 dynamically allocate the items when filtering without the concern of
325 messing up the contents
326 """
327 self[:] = []
328
329 def _filter(self, lv_name=None, vg_name=None, lv_path=None, lv_tags=None):
330 """
331 The actual method that filters using a new list. Useful so that other
332 methods that do not want to alter the contents of the list (e.g.
333 ``self.find``) can operate safely.
334 """
335 filtered = [i for i in self]
336 if lv_name:
337 filtered = [i for i in filtered if i.lv_name == lv_name]
338
339 if vg_name:
340 filtered = [i for i in filtered if i.vg_name == vg_name]
341
342 if lv_path:
343 filtered = [i for i in filtered if i.lv_path == lv_path]
344
345 # at this point, `filtered` has either all the volumes in self or is an
346 # actual filtered list if any filters were applied
347 if lv_tags:
348 tag_filtered = []
349 for k, v in lv_tags.items():
350 for volume in filtered:
351 if volume.tags.get(k) == str(v):
352 if volume not in tag_filtered:
353 tag_filtered.append(volume)
354 # return the tag_filtered volumes here, the `filtered` list is no
355 # longer useable
356 return tag_filtered
357
358 return filtered
359
360 def filter(self, lv_name=None, vg_name=None, lv_path=None, lv_tags=None):
361 """
362 Filter out volumes on top level attributes like ``lv_name`` or by
363 ``lv_tags`` where a dict is required. For example, to find a volume
364 that has an OSD ID of 0, the filter would look like::
365
366 lv_tags={'ceph.osd_id': '0'}
367
368 """
369 if not any([lv_name, vg_name, lv_path, lv_tags]):
370 raise TypeError('.filter() requires lv_name, vg_name, lv_path, or tags (none given)')
371 # first find the filtered volumes with the values in self
372 filtered_volumes = self._filter(
373 lv_name=lv_name,
374 vg_name=vg_name,
375 lv_path=lv_path,
376 lv_tags=lv_tags
377 )
378 # then purge everything
379 self._purge()
380 # and add the filtered items
381 self.extend(filtered_volumes)
382
383 def get(self, lv_name=None, vg_name=None, lv_path=None, lv_tags=None):
384 """
385 This is a bit expensive, since it will try to filter out all the
386 matching items in the list, filter them out applying anything that was
387 added and return the matching item.
388
389 This method does *not* alter the list, and it will raise an error if
390 multiple LVs are matched
391
392 It is useful to use ``tags`` when trying to find a specific logical volume,
393 but it can also lead to multiple lvs being found, since a lot of metadata
394 is shared between lvs of a distinct OSD.
395 """
396 if not any([lv_name, vg_name, lv_path, lv_tags]):
397 return None
398 lvs = self._filter(
399 lv_name=lv_name,
400 vg_name=vg_name,
401 lv_path=lv_path,
402 lv_tags=lv_tags
403 )
404 if not lvs:
405 return None
406 if len(lvs) > 1:
407 raise MultipleLVsError(lv_name, lv_path)
408 return lvs[0]
409
410
411class VolumeGroup(object):
412 """
413 Represents an LVM group, with some top-level attributes like ``vg_name``
414 """
415
416 def __init__(self, **kw):
417 for k, v in kw.items():
418 setattr(self, k, v)
419 self.name = kw['vg_name']
420 self.tags = parse_tags(kw.get('vg_tags', ''))
421
422 def __str__(self):
423 return '<%s>' % self.name
424
425 def __repr__(self):
426 return self.__str__()
427
428
429class Volume(object):
430 """
431 Represents a Logical Volume from LVM, with some top-level attributes like
432 ``lv_name`` and parsed tags as a dictionary of key/value pairs.
433 """
434
435 def __init__(self, **kw):
436 for k, v in kw.items():
437 setattr(self, k, v)
438 self.lv_api = kw
439 self.name = kw['lv_name']
440 self.tags = parse_tags(kw['lv_tags'])
441
442 def __str__(self):
443 return '<%s>' % self.lv_api['lv_path']
444
445 def __repr__(self):
446 return self.__str__()
447
448 def set_tags(self, tags):
449 """
450 :param tags: A dictionary of tag names and values, like::
451
452 {
453 "ceph.osd_fsid": "aaa-fff-bbbb",
454 "ceph.osd_id": "0"
455 }
456
457 At the end of all modifications, the tags are refreshed to reflect
458 LVM's most current view.
459 """
460 for k, v in tags.items():
461 self.set_tag(k, v)
462 # after setting all the tags, refresh them for the current object, use the
463 # lv_* identifiers to filter because those shouldn't change
464 lv_object = get_lv(lv_name=self.lv_name, lv_path=self.lv_path)
465 self.tags = lv_object.tags
466
467 def set_tag(self, key, value):
468 """
469 Set the key/value pair as an LVM tag. Does not "refresh" the values of
470 the current object for its tags. Meant to be a "fire and forget" type
471 of modification.
472 """
473 # remove it first if it exists
474 if self.tags.get(key):
475 current_value = self.tags[key]
476 tag = "%s=%s" % (key, current_value)
477 process.call(['sudo', 'lvchange', '--deltag', tag, self.lv_api['lv_path']])
478
479 process.call(
480 [
481 'sudo', 'lvchange',
482 '--addtag', '%s=%s' % (key, value), self.lv_path
483 ]
484 )