]> git.proxmox.com Git - ceph.git/blame - ceph/src/ceph-volume/ceph_volume/api/lvm.py
update sources to 12.2.10
[ceph.git] / ceph / src / ceph-volume / ceph_volume / api / lvm.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"""
94b18763
FG
6import logging
7import os
1adf2230
AA
8import uuid
9from math import floor
10from ceph_volume import process, util
11from ceph_volume.exceptions import (
12 MultipleLVsError, MultipleVGsError,
13 MultiplePVsError, SizeAllocationError
14)
d2e6a577 15
94b18763
FG
16logger = logging.getLogger(__name__)
17
d2e6a577 18
b5b8bbf5
FG
19def _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 # spliting 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 fiels
47 report.append(
48 dict(zip(field_items, output_items))
49 )
50
51 return report
52
53
1adf2230
AA
54def _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 parts = line[0].split(';')
71 parsed = {}
72 for part in parts:
73 part = part.replace("'", '')
74 key, value = part.split('=')
75 if 'DM_VG_NAME' in key:
76 value = value.split('/dev/mapper/')[-1]
77 key = key.split('DM_')[-1]
78 parsed[key] = value
79
80 return parsed
81
82
83def sizing(device_size, parts=None, size=None):
84 """
85 Calculate proper sizing to fully utilize the volume group in the most
86 efficient way possible. To prevent situations where LVM might accept
87 a percentage that is beyond the vg's capabilities, it will refuse with
88 an error when requesting a larger-than-possible parameter, in addition
89 to rounding down calculations.
90
91 A dictionary with different sizing parameters is returned, to make it
92 easier for others to choose what they need in order to create logical
93 volumes::
94
95 >>> sizing(100, parts=2)
96 >>> {'parts': 2, 'percentages': 50, 'sizes': 50}
97
98 """
99 if parts is not None and size is not None:
100 raise ValueError(
101 "Cannot process sizing with both parts (%s) and size (%s)" % (parts, size)
102 )
103
104 if size and size > device_size:
105 raise SizeAllocationError(size, device_size)
106
107 def get_percentage(parts):
108 return int(floor(100 / float(parts)))
109
110 if parts is not None:
111 # Prevent parts being 0, falling back to 1 (100% usage)
112 parts = parts or 1
113 percentages = get_percentage(parts)
114
115 if size:
116 parts = int(device_size / size) or 1
117 percentages = get_percentage(parts)
118
119 sizes = device_size / parts if parts else int(floor(device_size))
120
121 return {
122 'parts': parts,
123 'percentages': percentages,
124 'sizes': int(sizes),
125 }
126
127
d2e6a577
FG
128def parse_tags(lv_tags):
129 """
130 Return a dictionary mapping of all the tags associated with
131 a Volume from the comma-separated tags coming from the LVM API
132
133 Input look like::
134
135 "ceph.osd_fsid=aaa-fff-bbbb,ceph.osd_id=0"
136
137 For the above example, the expected return value would be::
138
139 {
140 "ceph.osd_fsid": "aaa-fff-bbbb",
141 "ceph.osd_id": "0"
142 }
143 """
144 if not lv_tags:
145 return {}
146 tag_mapping = {}
147 tags = lv_tags.split(',')
148 for tag_assignment in tags:
b32b8144
FG
149 if not tag_assignment.startswith('ceph.'):
150 continue
d2e6a577
FG
151 key, value = tag_assignment.split('=', 1)
152 tag_mapping[key] = value
153
154 return tag_mapping
155
156
94b18763
FG
157def _vdo_parents(devices):
158 """
159 It is possible we didn't get a logical volume, or a mapper path, but
160 a device like /dev/sda2, to resolve this, we must look at all the slaves of
161 every single device in /sys/block and if any of those devices is related to
162 VDO devices, then we can add the parent
163 """
164 parent_devices = []
165 for parent in os.listdir('/sys/block'):
166 for slave in os.listdir('/sys/block/%s/slaves' % parent):
167 if slave in devices:
168 parent_devices.append('/dev/%s' % parent)
169 parent_devices.append(parent)
170 return parent_devices
171
172
173def _vdo_slaves(vdo_names):
174 """
175 find all the slaves associated with each vdo name (from realpath) by going
176 into /sys/block/<realpath>/slaves
177 """
178 devices = []
179 for vdo_name in vdo_names:
180 mapper_path = '/dev/mapper/%s' % vdo_name
181 if not os.path.exists(mapper_path):
182 continue
183 # resolve the realpath and realname of the vdo mapper
184 vdo_realpath = os.path.realpath(mapper_path)
185 vdo_realname = vdo_realpath.split('/')[-1]
186 slaves_path = '/sys/block/%s/slaves' % vdo_realname
187 if not os.path.exists(slaves_path):
188 continue
189 devices.append(vdo_realpath)
190 devices.append(mapper_path)
191 devices.append(vdo_realname)
192 for slave in os.listdir(slaves_path):
193 devices.append('/dev/%s' % slave)
194 devices.append(slave)
195 return devices
196
197
198def _is_vdo(path):
199 """
200 A VDO device can be composed from many different devices, go through each
201 one of those devices and its slaves (if any) and correlate them back to
202 /dev/mapper and their realpaths, and then check if they appear as part of
203 /sys/kvdo/<name>/statistics
204
205 From the realpath of a logical volume, determine if it is a VDO device or
206 not, by correlating it to the presence of the name in
207 /sys/kvdo/<name>/statistics and all the previously captured devices
208 """
209 if not os.path.isdir('/sys/kvdo'):
210 return False
211 realpath = os.path.realpath(path)
212 realpath_name = realpath.split('/')[-1]
213 devices = []
214 vdo_names = set()
215 # get all the vdo names
216 for dirname in os.listdir('/sys/kvdo/'):
217 if os.path.isdir('/sys/kvdo/%s/statistics' % dirname):
218 vdo_names.add(dirname)
219
220 # find all the slaves associated with each vdo name (from realpath) by
221 # going into /sys/block/<realpath>/slaves
222 devices.extend(_vdo_slaves(vdo_names))
223
224 # Find all possible parents, looking into slaves that are related to VDO
225 devices.extend(_vdo_parents(devices))
226
227 return any([
228 path in devices,
229 realpath in devices,
230 realpath_name in devices])
231
232
233def is_vdo(path):
234 """
235 Detect if a path is backed by VDO, proxying the actual call to _is_vdo so
236 that we can prevent an exception breaking OSD creation. If an exception is
237 raised, it will get captured and logged to file, while returning
238 a ``False``.
239 """
240 try:
241 if _is_vdo(path):
242 return '1'
243 return '0'
244 except Exception:
245 logger.exception('Unable to properly detect device as VDO: %s', path)
246 return '0'
247
248
1adf2230
AA
249def dmsetup_splitname(dev):
250 """
251 Run ``dmsetup splitname`` and parse the results.
252
253 .. warning:: This call does not ensure that the device is correct or that
254 it exists. ``dmsetup`` will happily take a non existing path and still
255 return a 0 exit status.
256 """
257 command = [
258 'dmsetup', 'splitname', '--noheadings',
259 "--separator=';'", '--nameprefixes', dev
260 ]
261 out, err, rc = process.call(command)
262 return _splitname_parser(out)
263
264
265def is_lv(dev, lvs=None):
266 """
267 Boolean to detect if a device is an LV or not.
268 """
269 splitname = dmsetup_splitname(dev)
270 # Allowing to optionally pass `lvs` can help reduce repetitive checks for
271 # multiple devices at once.
272 lvs = lvs if lvs is not None else Volumes()
273 if splitname.get('LV_NAME'):
274 lvs.filter(lv_name=splitname['LV_NAME'], vg_name=splitname['VG_NAME'])
275 return len(lvs) > 0
276 return False
277
278
d2e6a577
FG
279def get_api_vgs():
280 """
b5b8bbf5
FG
281 Return the list of group volumes available in the system using flags to
282 include common metadata associated with them
d2e6a577 283
94b18763 284 Command and sample delimited output should look like::
d2e6a577 285
1adf2230 286 $ vgs --noheadings --units=g --readonly --separator=';' \
b5b8bbf5
FG
287 -o vg_name,pv_count,lv_count,snap_count,vg_attr,vg_size,vg_free
288 ubuntubox-vg;1;2;0;wz--n-;299.52g;12.00m
289 osd_vg;3;1;0;wz--n-;29.21g;9.21g
d2e6a577 290
1adf2230
AA
291 To normalize sizing, the units are forced in 'g' which is equivalent to
292 gigabytes, which uses multiples of 1024 (as opposed to 1000)
d2e6a577 293 """
1adf2230 294 fields = 'vg_name,pv_count,lv_count,snap_count,vg_attr,vg_size,vg_free,vg_free_count'
d2e6a577 295 stdout, stderr, returncode = process.call(
91327a77
AA
296 ['vgs', '--noheadings', '--readonly', '--units=g', '--separator=";"', '-o', fields],
297 verbose_on_failure=False
d2e6a577 298 )
b5b8bbf5 299 return _output_parser(stdout, fields)
d2e6a577
FG
300
301
302def get_api_lvs():
303 """
304 Return the list of logical volumes available in the system using flags to include common
305 metadata associated with them
306
94b18763 307 Command and delimited output should look like::
d2e6a577 308
94b18763 309 $ lvs --noheadings --readonly --separator=';' -o lv_tags,lv_path,lv_name,vg_name
b5b8bbf5
FG
310 ;/dev/ubuntubox-vg/root;root;ubuntubox-vg
311 ;/dev/ubuntubox-vg/swap_1;swap_1;ubuntubox-vg
d2e6a577
FG
312
313 """
1adf2230 314 fields = 'lv_tags,lv_path,lv_name,vg_name,lv_uuid,lv_size'
d2e6a577 315 stdout, stderr, returncode = process.call(
91327a77
AA
316 ['lvs', '--noheadings', '--readonly', '--separator=";"', '-o', fields],
317 verbose_on_failure=False
b5b8bbf5
FG
318 )
319 return _output_parser(stdout, fields)
d2e6a577
FG
320
321
181888fb
FG
322def get_api_pvs():
323 """
324 Return the list of physical volumes configured for lvm and available in the
325 system using flags to include common metadata associated with them like the uuid
326
b32b8144
FG
327 This will only return physical volumes set up to work with LVM.
328
94b18763 329 Command and delimited output should look like::
181888fb 330
94b18763 331 $ pvs --noheadings --readonly --separator=';' -o pv_name,pv_tags,pv_uuid
181888fb
FG
332 /dev/sda1;;
333 /dev/sdv;;07A4F654-4162-4600-8EB3-88D1E42F368D
334
335 """
28e407b8 336 fields = 'pv_name,pv_tags,pv_uuid,vg_name,lv_uuid'
181888fb 337
181888fb 338 stdout, stderr, returncode = process.call(
91327a77
AA
339 ['pvs', '--no-heading', '--readonly', '--separator=";"', '-o', fields],
340 verbose_on_failure=False
181888fb
FG
341 )
342
343 return _output_parser(stdout, fields)
344
345
3efd9988
FG
346def get_lv_from_argument(argument):
347 """
348 Helper proxy function that consumes a possible logical volume passed in from the CLI
349 in the form of `vg/lv`, but with some validation so that an argument that is a full
350 path to a device can be ignored
351 """
352 if argument.startswith('/'):
353 lv = get_lv(lv_path=argument)
354 return lv
355 try:
356 vg_name, lv_name = argument.split('/')
357 except (ValueError, AttributeError):
358 return None
359 return get_lv(lv_name=lv_name, vg_name=vg_name)
360
361
181888fb 362def get_lv(lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
d2e6a577
FG
363 """
364 Return a matching lv for the current system, requiring ``lv_name``,
365 ``vg_name``, ``lv_path`` or ``tags``. Raises an error if more than one lv
366 is found.
367
368 It is useful to use ``tags`` when trying to find a specific logical volume,
369 but it can also lead to multiple lvs being found, since a lot of metadata
370 is shared between lvs of a distinct OSD.
371 """
181888fb 372 if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]):
d2e6a577
FG
373 return None
374 lvs = Volumes()
181888fb
FG
375 return lvs.get(
376 lv_name=lv_name, vg_name=vg_name, lv_path=lv_path, lv_uuid=lv_uuid,
377 lv_tags=lv_tags
378 )
379
380
381def get_pv(pv_name=None, pv_uuid=None, pv_tags=None):
382 """
383 Return a matching pv (physical volume) for the current system, requiring
384 ``pv_name``, ``pv_uuid``, or ``pv_tags``. Raises an error if more than one
385 pv is found.
386 """
387 if not any([pv_name, pv_uuid, pv_tags]):
388 return None
389 pvs = PVolumes()
390 return pvs.get(pv_name=pv_name, pv_uuid=pv_uuid, pv_tags=pv_tags)
391
392
393def create_pv(device):
394 """
395 Create a physical volume from a device, useful when devices need to be later mapped
396 to journals.
397 """
398 process.run([
181888fb
FG
399 'pvcreate',
400 '-v', # verbose
401 '-f', # force it
402 '--yes', # answer yes to any prompts
403 device
404 ])
d2e6a577
FG
405
406
1adf2230 407def create_vg(devices, name=None, name_prefix=None):
3efd9988
FG
408 """
409 Create a Volume Group. Command looks like::
410
411 vgcreate --force --yes group_name device
412
413 Once created the volume group is returned as a ``VolumeGroup`` object
1adf2230
AA
414
415 :param devices: A list of devices to create a VG. Optionally, a single
416 device (as a string) can be used.
417 :param name: Optionally set the name of the VG, defaults to 'ceph-{uuid}'
418 :param name_prefix: Optionally prefix the name of the VG, which will get combined
419 with a UUID string
3efd9988 420 """
1adf2230
AA
421 if isinstance(devices, set):
422 devices = list(devices)
423 if not isinstance(devices, list):
424 devices = [devices]
425 if name_prefix:
426 name = "%s-%s" % (name_prefix, str(uuid.uuid4()))
427 elif name is None:
428 name = "ceph-%s" % str(uuid.uuid4())
3efd9988 429 process.run([
3efd9988
FG
430 'vgcreate',
431 '--force',
432 '--yes',
1adf2230 433 name] + devices
3efd9988
FG
434 )
435
436 vg = get_vg(vg_name=name)
437 return vg
438
439
1adf2230
AA
440def extend_vg(vg, devices):
441 """
442 Extend a Volume Group. Command looks like::
443
444 vgextend --force --yes group_name [device, ...]
445
446 Once created the volume group is extended and returned as a ``VolumeGroup`` object
447
448 :param vg: A VolumeGroup object
449 :param devices: A list of devices to extend the VG. Optionally, a single
450 device (as a string) can be used.
451 """
452 if not isinstance(devices, list):
453 devices = [devices]
454 process.run([
455 'vgextend',
456 '--force',
457 '--yes',
458 vg.name] + devices
459 )
460
461 vg = get_vg(vg_name=vg.name)
462 return vg
463
464
b32b8144
FG
465def remove_vg(vg_name):
466 """
467 Removes a volume group.
468 """
94b18763 469 fail_msg = "Unable to remove vg %s" % vg_name
b32b8144
FG
470 process.run(
471 [
472 'vgremove',
473 '-v', # verbose
474 '-f', # force it
475 vg_name
476 ],
477 fail_msg=fail_msg,
478 )
479
480
481def remove_pv(pv_name):
482 """
91327a77
AA
483 Removes a physical volume using a double `-f` to prevent prompts and fully
484 remove anything related to LVM. This is tremendously destructive, but so is all other actions
485 when zapping a device.
486
487 In the case where multiple PVs are found, it will ignore that fact and
488 continue with the removal, specifically in the case of messages like::
489
490 WARNING: PV $UUID /dev/DEV-1 was already found on /dev/DEV-2
491
492 These situations can be avoided with custom filtering rules, which this API
493 cannot handle while accommodating custom user filters.
b32b8144 494 """
94b18763 495 fail_msg = "Unable to remove vg %s" % pv_name
b32b8144
FG
496 process.run(
497 [
498 'pvremove',
499 '-v', # verbose
500 '-f', # force it
91327a77 501 '-f', # force it
b32b8144
FG
502 pv_name
503 ],
504 fail_msg=fail_msg,
505 )
506
507
91327a77 508def remove_lv(lv):
3efd9988
FG
509 """
510 Removes a logical volume given it's absolute path.
511
512 Will return True if the lv is successfully removed or
513 raises a RuntimeError if the removal fails.
91327a77
AA
514
515 :param lv: A ``Volume`` object or the path for an LV
3efd9988 516 """
91327a77
AA
517 if isinstance(lv, Volume):
518 path = lv.lv_path
519 else:
520 path = lv
521
3efd9988
FG
522 stdout, stderr, returncode = process.call(
523 [
3efd9988
FG
524 'lvremove',
525 '-v', # verbose
526 '-f', # force it
527 path
528 ],
529 show_command=True,
530 terminal_verbose=True,
531 )
532 if returncode != 0:
94b18763 533 raise RuntimeError("Unable to remove %s" % path)
3efd9988
FG
534 return True
535
536
1adf2230 537def create_lv(name, group, extents=None, size=None, tags=None, uuid_name=False):
d2e6a577
FG
538 """
539 Create a Logical Volume in a Volume Group. Command looks like::
540
541 lvcreate -L 50G -n gfslv vg0
542
3efd9988
FG
543 ``name``, ``group``, are required. If ``size`` is provided it must follow
544 lvm's size notation (like 1G, or 20M). Tags are an optional dictionary and is expected to
545 conform to the convention of prefixing them with "ceph." like::
d2e6a577 546
3efd9988 547 {"ceph.block_device": "/dev/ceph/osd-1"}
1adf2230
AA
548
549 :param uuid_name: Optionally combine the ``name`` with UUID to ensure uniqueness
d2e6a577 550 """
1adf2230
AA
551 if uuid_name:
552 name = '%s-%s' % (name, uuid.uuid4())
553 if tags is None:
554 tags = {
555 "ceph.osd_id": "null",
556 "ceph.type": "null",
557 "ceph.cluster_fsid": "null",
558 "ceph.osd_fsid": "null",
559 }
560
d2e6a577
FG
561 # XXX add CEPH_VOLUME_LVM_DEBUG to enable -vvvv on lv operations
562 type_path_tag = {
563 'journal': 'ceph.journal_device',
564 'data': 'ceph.data_device',
3efd9988
FG
565 'block': 'ceph.block_device',
566 'wal': 'ceph.wal_device',
567 'db': 'ceph.db_device',
568 'lockbox': 'ceph.lockbox_device', # XXX might not ever need this lockbox sorcery
d2e6a577
FG
569 }
570 if size:
571 process.run([
d2e6a577
FG
572 'lvcreate',
573 '--yes',
574 '-L',
3efd9988 575 '%s' % size,
d2e6a577
FG
576 '-n', name, group
577 ])
1adf2230
AA
578 elif extents:
579 process.run([
580 'lvcreate',
581 '--yes',
582 '-l',
583 '%s' % extents,
584 '-n', name, group
585 ])
d2e6a577
FG
586 # create the lv with all the space available, this is needed because the
587 # system call is different for LVM
588 else:
589 process.run([
d2e6a577
FG
590 'lvcreate',
591 '--yes',
592 '-l',
593 '100%FREE',
594 '-n', name, group
595 ])
596
597 lv = get_lv(lv_name=name, vg_name=group)
3efd9988 598 lv.set_tags(tags)
d2e6a577
FG
599
600 # when creating a distinct type, the caller doesn't know what the path will
601 # be so this function will set it after creation using the mapping
3efd9988
FG
602 path_tag = type_path_tag.get(tags.get('ceph.type'))
603 if path_tag:
604 lv.set_tags(
605 {path_tag: lv.lv_path}
606 )
d2e6a577
FG
607 return lv
608
609
1adf2230
AA
610def create_lvs(volume_group, parts=None, size=None, name_prefix='ceph-lv'):
611 """
612 Create multiple Logical Volumes from a Volume Group by calculating the
613 proper extents from ``parts`` or ``size``. A custom prefix can be used
614 (defaults to ``ceph-lv``), these names are always suffixed with a uuid.
615
616 LV creation in ceph-volume will require tags, this is expected to be
617 pre-computed by callers who know Ceph metadata like OSD IDs and FSIDs. It
618 will probably not be the case when mass-creating LVs, so common/default
619 tags will be set to ``"null"``.
620
621 .. note:: LVs that are not in use can be detected by querying LVM for tags that are
622 set to ``"null"``.
623
624 :param volume_group: The volume group (vg) to use for LV creation
625 :type group: ``VolumeGroup()`` object
626 :param parts: Number of LVs to create *instead of* ``size``.
627 :type parts: int
628 :param size: Size (in gigabytes) of LVs to create, e.g. "as many 10gb LVs as possible"
629 :type size: int
630 :param extents: The number of LVM extents to use to create the LV. Useful if looking to have
631 accurate LV sizes (LVM rounds sizes otherwise)
632 """
633 if parts is None and size is None:
634 # fallback to just one part (using 100% of the vg)
635 parts = 1
636 lvs = []
637 tags = {
638 "ceph.osd_id": "null",
639 "ceph.type": "null",
640 "ceph.cluster_fsid": "null",
641 "ceph.osd_fsid": "null",
642 }
643 sizing = volume_group.sizing(parts=parts, size=size)
644 for part in range(0, sizing['parts']):
645 size = sizing['sizes']
646 extents = sizing['extents']
647 lv_name = '%s-%s' % (name_prefix, uuid.uuid4())
648 lvs.append(
649 create_lv(lv_name, volume_group.name, extents=extents, tags=tags)
650 )
651 return lvs
652
653
d2e6a577
FG
654def get_vg(vg_name=None, vg_tags=None):
655 """
656 Return a matching vg for the current system, requires ``vg_name`` or
657 ``tags``. Raises an error if more than one vg is found.
658
659 It is useful to use ``tags`` when trying to find a specific volume group,
660 but it can also lead to multiple vgs being found.
661 """
662 if not any([vg_name, vg_tags]):
663 return None
664 vgs = VolumeGroups()
665 return vgs.get(vg_name=vg_name, vg_tags=vg_tags)
666
667
668class VolumeGroups(list):
669 """
670 A list of all known volume groups for the current system, with the ability
671 to filter them via keyword arguments.
672 """
673
674 def __init__(self):
675 self._populate()
676
677 def _populate(self):
678 # get all the vgs in the current system
679 for vg_item in get_api_vgs():
680 self.append(VolumeGroup(**vg_item))
681
682 def _purge(self):
683 """
684 Deplete all the items in the list, used internally only so that we can
685 dynamically allocate the items when filtering without the concern of
686 messing up the contents
687 """
688 self[:] = []
689
690 def _filter(self, vg_name=None, vg_tags=None):
691 """
692 The actual method that filters using a new list. Useful so that other
693 methods that do not want to alter the contents of the list (e.g.
694 ``self.find``) can operate safely.
695
696 .. note:: ``vg_tags`` is not yet implemented
697 """
698 filtered = [i for i in self]
699 if vg_name:
700 filtered = [i for i in filtered if i.vg_name == vg_name]
701
702 # at this point, `filtered` has either all the volumes in self or is an
703 # actual filtered list if any filters were applied
704 if vg_tags:
705 tag_filtered = []
181888fb
FG
706 for volume in filtered:
707 matches = all(volume.tags.get(k) == str(v) for k, v in vg_tags.items())
708 if matches:
709 tag_filtered.append(volume)
d2e6a577
FG
710 return tag_filtered
711
712 return filtered
713
714 def filter(self, vg_name=None, vg_tags=None):
715 """
716 Filter out groups on top level attributes like ``vg_name`` or by
717 ``vg_tags`` where a dict is required. For example, to find a Ceph group
718 with dmcache as the type, the filter would look like::
719
720 vg_tags={'ceph.type': 'dmcache'}
721
722 .. warning:: These tags are not documented because they are currently
723 unused, but are here to maintain API consistency
724 """
725 if not any([vg_name, vg_tags]):
726 raise TypeError('.filter() requires vg_name or vg_tags (none given)')
727 # first find the filtered volumes with the values in self
728 filtered_groups = self._filter(
729 vg_name=vg_name,
730 vg_tags=vg_tags
731 )
732 # then purge everything
733 self._purge()
734 # and add the filtered items
735 self.extend(filtered_groups)
736
737 def get(self, vg_name=None, vg_tags=None):
738 """
739 This is a bit expensive, since it will try to filter out all the
740 matching items in the list, filter them out applying anything that was
741 added and return the matching item.
742
743 This method does *not* alter the list, and it will raise an error if
744 multiple VGs are matched
745
746 It is useful to use ``tags`` when trying to find a specific volume group,
747 but it can also lead to multiple vgs being found (although unlikely)
748 """
749 if not any([vg_name, vg_tags]):
750 return None
751 vgs = self._filter(
752 vg_name=vg_name,
753 vg_tags=vg_tags
754 )
755 if not vgs:
756 return None
757 if len(vgs) > 1:
758 # this is probably never going to happen, but it is here to keep
759 # the API code consistent
760 raise MultipleVGsError(vg_name)
761 return vgs[0]
762
763
764class Volumes(list):
765 """
766 A list of all known (logical) volumes for the current system, with the ability
767 to filter them via keyword arguments.
768 """
769
770 def __init__(self):
771 self._populate()
772
773 def _populate(self):
774 # get all the lvs in the current system
775 for lv_item in get_api_lvs():
776 self.append(Volume(**lv_item))
777
778 def _purge(self):
779 """
94b18763 780 Delete all the items in the list, used internally only so that we can
d2e6a577
FG
781 dynamically allocate the items when filtering without the concern of
782 messing up the contents
783 """
784 self[:] = []
785
181888fb 786 def _filter(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
d2e6a577
FG
787 """
788 The actual method that filters using a new list. Useful so that other
789 methods that do not want to alter the contents of the list (e.g.
790 ``self.find``) can operate safely.
791 """
792 filtered = [i for i in self]
793 if lv_name:
794 filtered = [i for i in filtered if i.lv_name == lv_name]
795
796 if vg_name:
797 filtered = [i for i in filtered if i.vg_name == vg_name]
798
181888fb
FG
799 if lv_uuid:
800 filtered = [i for i in filtered if i.lv_uuid == lv_uuid]
801
d2e6a577
FG
802 if lv_path:
803 filtered = [i for i in filtered if i.lv_path == lv_path]
804
805 # at this point, `filtered` has either all the volumes in self or is an
806 # actual filtered list if any filters were applied
807 if lv_tags:
808 tag_filtered = []
181888fb
FG
809 for volume in filtered:
810 # all the tags we got need to match on the volume
811 matches = all(volume.tags.get(k) == str(v) for k, v in lv_tags.items())
812 if matches:
813 tag_filtered.append(volume)
d2e6a577
FG
814 return tag_filtered
815
816 return filtered
817
181888fb 818 def filter(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
d2e6a577
FG
819 """
820 Filter out volumes on top level attributes like ``lv_name`` or by
821 ``lv_tags`` where a dict is required. For example, to find a volume
822 that has an OSD ID of 0, the filter would look like::
823
824 lv_tags={'ceph.osd_id': '0'}
825
826 """
181888fb
FG
827 if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]):
828 raise TypeError('.filter() requires lv_name, vg_name, lv_path, lv_uuid, or tags (none given)')
d2e6a577
FG
829 # first find the filtered volumes with the values in self
830 filtered_volumes = self._filter(
831 lv_name=lv_name,
832 vg_name=vg_name,
833 lv_path=lv_path,
181888fb 834 lv_uuid=lv_uuid,
d2e6a577
FG
835 lv_tags=lv_tags
836 )
837 # then purge everything
838 self._purge()
839 # and add the filtered items
840 self.extend(filtered_volumes)
841
181888fb 842 def get(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
d2e6a577
FG
843 """
844 This is a bit expensive, since it will try to filter out all the
845 matching items in the list, filter them out applying anything that was
846 added and return the matching item.
847
848 This method does *not* alter the list, and it will raise an error if
849 multiple LVs are matched
850
851 It is useful to use ``tags`` when trying to find a specific logical volume,
852 but it can also lead to multiple lvs being found, since a lot of metadata
853 is shared between lvs of a distinct OSD.
854 """
181888fb 855 if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]):
d2e6a577
FG
856 return None
857 lvs = self._filter(
858 lv_name=lv_name,
859 vg_name=vg_name,
860 lv_path=lv_path,
181888fb 861 lv_uuid=lv_uuid,
d2e6a577
FG
862 lv_tags=lv_tags
863 )
864 if not lvs:
865 return None
866 if len(lvs) > 1:
867 raise MultipleLVsError(lv_name, lv_path)
868 return lvs[0]
869
870
181888fb
FG
871class PVolumes(list):
872 """
873 A list of all known (physical) volumes for the current system, with the ability
874 to filter them via keyword arguments.
875 """
876
877 def __init__(self):
878 self._populate()
879
880 def _populate(self):
881 # get all the pvs in the current system
882 for pv_item in get_api_pvs():
883 self.append(PVolume(**pv_item))
884
885 def _purge(self):
886 """
887 Deplete all the items in the list, used internally only so that we can
888 dynamically allocate the items when filtering without the concern of
889 messing up the contents
890 """
891 self[:] = []
892
893 def _filter(self, pv_name=None, pv_uuid=None, pv_tags=None):
894 """
895 The actual method that filters using a new list. Useful so that other
896 methods that do not want to alter the contents of the list (e.g.
897 ``self.find``) can operate safely.
898 """
899 filtered = [i for i in self]
900 if pv_name:
901 filtered = [i for i in filtered if i.pv_name == pv_name]
902
903 if pv_uuid:
904 filtered = [i for i in filtered if i.pv_uuid == pv_uuid]
905
906 # at this point, `filtered` has either all the physical volumes in self
907 # or is an actual filtered list if any filters were applied
908 if pv_tags:
909 tag_filtered = []
910 for pvolume in filtered:
911 matches = all(pvolume.tags.get(k) == str(v) for k, v in pv_tags.items())
912 if matches:
913 tag_filtered.append(pvolume)
914 # return the tag_filtered pvolumes here, the `filtered` list is no
915 # longer useable
916 return tag_filtered
917
918 return filtered
919
920 def filter(self, pv_name=None, pv_uuid=None, pv_tags=None):
921 """
922 Filter out volumes on top level attributes like ``pv_name`` or by
923 ``pv_tags`` where a dict is required. For example, to find a physical volume
924 that has an OSD ID of 0, the filter would look like::
925
926 pv_tags={'ceph.osd_id': '0'}
927
928 """
929 if not any([pv_name, pv_uuid, pv_tags]):
930 raise TypeError('.filter() requires pv_name, pv_uuid, or pv_tags (none given)')
931 # first find the filtered volumes with the values in self
932 filtered_volumes = self._filter(
933 pv_name=pv_name,
934 pv_uuid=pv_uuid,
935 pv_tags=pv_tags
936 )
937 # then purge everything
938 self._purge()
939 # and add the filtered items
940 self.extend(filtered_volumes)
941
942 def get(self, pv_name=None, pv_uuid=None, pv_tags=None):
943 """
944 This is a bit expensive, since it will try to filter out all the
945 matching items in the list, filter them out applying anything that was
946 added and return the matching item.
947
948 This method does *not* alter the list, and it will raise an error if
949 multiple pvs are matched
950
951 It is useful to use ``tags`` when trying to find a specific logical volume,
952 but it can also lead to multiple pvs being found, since a lot of metadata
953 is shared between pvs of a distinct OSD.
954 """
955 if not any([pv_name, pv_uuid, pv_tags]):
956 return None
957 pvs = self._filter(
958 pv_name=pv_name,
959 pv_uuid=pv_uuid,
960 pv_tags=pv_tags
961 )
962 if not pvs:
963 return None
1adf2230 964 if len(pvs) > 1 and pv_tags:
181888fb
FG
965 raise MultiplePVsError(pv_name)
966 return pvs[0]
967
968
d2e6a577
FG
969class VolumeGroup(object):
970 """
971 Represents an LVM group, with some top-level attributes like ``vg_name``
972 """
973
974 def __init__(self, **kw):
975 for k, v in kw.items():
976 setattr(self, k, v)
977 self.name = kw['vg_name']
978 self.tags = parse_tags(kw.get('vg_tags', ''))
979
980 def __str__(self):
981 return '<%s>' % self.name
982
983 def __repr__(self):
984 return self.__str__()
985
1adf2230
AA
986 def _parse_size(self, size):
987 error_msg = "Unable to convert vg size to integer: '%s'" % str(size)
988 try:
989 integer, _ = size.split('g')
990 except ValueError:
991 logger.exception(error_msg)
992 raise RuntimeError(error_msg)
993
994 return util.str_to_int(integer)
995
996 @property
997 def free(self):
998 """
999 Parse the available size in gigabytes from the ``vg_free`` attribute, that
1000 will be a string with a character ('g') to indicate gigabytes in size.
1001 Returns a rounded down integer to ease internal operations::
1002
1003 >>> data_vg.vg_free
1004 '0.01g'
1005 >>> data_vg.size
1006 0
1007 """
1008 return self._parse_size(self.vg_free)
1009
1010 @property
1011 def size(self):
1012 """
1013 Parse the size in gigabytes from the ``vg_size`` attribute, that
1014 will be a string with a character ('g') to indicate gigabytes in size.
1015 Returns a rounded down integer to ease internal operations::
1016
1017 >>> data_vg.vg_size
1018 '1024.9g'
1019 >>> data_vg.size
1020 1024
1021 """
1022 return self._parse_size(self.vg_size)
1023
1024 def sizing(self, parts=None, size=None):
1025 """
1026 Calculate proper sizing to fully utilize the volume group in the most
1027 efficient way possible. To prevent situations where LVM might accept
1028 a percentage that is beyond the vg's capabilities, it will refuse with
1029 an error when requesting a larger-than-possible parameter, in addition
1030 to rounding down calculations.
1031
1032 A dictionary with different sizing parameters is returned, to make it
1033 easier for others to choose what they need in order to create logical
1034 volumes::
1035
1036 >>> data_vg.free
1037 1024
1038 >>> data_vg.sizing(parts=4)
1039 {'parts': 4, 'sizes': 256, 'percentages': 25}
1040 >>> data_vg.sizing(size=512)
1041 {'parts': 2, 'sizes': 512, 'percentages': 50}
1042
1043
1044 :param parts: Number of parts to create LVs from
1045 :param size: Size in gigabytes to divide the VG into
1046
1047 :raises SizeAllocationError: When requested size cannot be allocated with
1048 :raises ValueError: If both ``parts`` and ``size`` are given
1049 """
1050 if parts is not None and size is not None:
1051 raise ValueError(
1052 "Cannot process sizing with both parts (%s) and size (%s)" % (parts, size)
1053 )
1054
1055 # if size is given we need to map that to extents so that we avoid
1056 # issues when trying to get this right with a size in gigabytes find
1057 # the percentage first, cheating, because these values are thrown out
1058 vg_free_count = util.str_to_int(self.vg_free_count)
1059
1060 if size:
1061 extents = int(size * vg_free_count / self.free)
1062 disk_sizing = sizing(self.free, size=size, parts=parts)
1063 else:
1064 if parts is not None:
1065 # Prevent parts being 0, falling back to 1 (100% usage)
1066 parts = parts or 1
1067 size = int(self.free / parts)
1068 extents = size * vg_free_count / self.free
1069 disk_sizing = sizing(self.free, parts=parts)
1070
1071 extent_sizing = sizing(vg_free_count, size=extents)
1072
1073 disk_sizing['extents'] = int(extents)
1074 disk_sizing['percentages'] = extent_sizing['percentages']
1075 return disk_sizing
1076
d2e6a577
FG
1077
1078class Volume(object):
1079 """
1080 Represents a Logical Volume from LVM, with some top-level attributes like
1081 ``lv_name`` and parsed tags as a dictionary of key/value pairs.
1082 """
1083
1084 def __init__(self, **kw):
1085 for k, v in kw.items():
1086 setattr(self, k, v)
1087 self.lv_api = kw
1088 self.name = kw['lv_name']
1089 self.tags = parse_tags(kw['lv_tags'])
3a9019d9 1090 self.encrypted = self.tags.get('ceph.encrypted', '0') == '1'
91327a77 1091 self.used_by_ceph = 'ceph.osd_id' in self.tags
d2e6a577
FG
1092
1093 def __str__(self):
1094 return '<%s>' % self.lv_api['lv_path']
1095
1096 def __repr__(self):
1097 return self.__str__()
1098
3efd9988
FG
1099 def as_dict(self):
1100 obj = {}
1101 obj.update(self.lv_api)
1102 obj['tags'] = self.tags
1103 obj['name'] = self.name
1104 obj['type'] = self.tags['ceph.type']
1105 obj['path'] = self.lv_path
1106 return obj
1107
91327a77
AA
1108 def report(self):
1109 if not self.used_by_ceph:
1110 return {
1111 'name': self.lv_name,
1112 'comment': 'not used by ceph'
1113 }
1114 else:
1115 type_ = self.tags['ceph.type']
1116 report = {
1117 'name': self.lv_name,
1118 'osd_id': self.tags['ceph.osd_id'],
1119 'cluster_name': self.tags['ceph.cluster_name'],
1120 'type': type_,
1121 'osd_fsid': self.tags['ceph.osd_fsid'],
1122 'cluster_fsid': self.tags['ceph.cluster_fsid'],
1123 }
1124 type_uuid = '{}_uuid'.format(type_)
1125 report[type_uuid] = self.tags['ceph.{}'.format(type_uuid)]
1126 return report
1127
3efd9988
FG
1128 def clear_tags(self):
1129 """
1130 Removes all tags from the Logical Volume.
1131 """
1132 for k, v in self.tags.items():
1133 tag = "%s=%s" % (k, v)
b32b8144 1134 process.run(['lvchange', '--deltag', tag, self.lv_path])
3efd9988 1135
d2e6a577
FG
1136 def set_tags(self, tags):
1137 """
1138 :param tags: A dictionary of tag names and values, like::
1139
1140 {
1141 "ceph.osd_fsid": "aaa-fff-bbbb",
1142 "ceph.osd_id": "0"
1143 }
1144
1145 At the end of all modifications, the tags are refreshed to reflect
1146 LVM's most current view.
1147 """
1148 for k, v in tags.items():
1149 self.set_tag(k, v)
1150 # after setting all the tags, refresh them for the current object, use the
1151 # lv_* identifiers to filter because those shouldn't change
1152 lv_object = get_lv(lv_name=self.lv_name, lv_path=self.lv_path)
1153 self.tags = lv_object.tags
1154
1155 def set_tag(self, key, value):
1156 """
1157 Set the key/value pair as an LVM tag. Does not "refresh" the values of
1158 the current object for its tags. Meant to be a "fire and forget" type
1159 of modification.
1160 """
1161 # remove it first if it exists
1162 if self.tags.get(key):
1163 current_value = self.tags[key]
1164 tag = "%s=%s" % (key, current_value)
b32b8144 1165 process.call(['lvchange', '--deltag', tag, self.lv_api['lv_path']])
d2e6a577
FG
1166
1167 process.call(
1168 [
b32b8144 1169 'lvchange',
d2e6a577
FG
1170 '--addtag', '%s=%s' % (key, value), self.lv_path
1171 ]
1172 )
181888fb
FG
1173
1174
1175class PVolume(object):
1176 """
1177 Represents a Physical Volume from LVM, with some top-level attributes like
1178 ``pv_name`` and parsed tags as a dictionary of key/value pairs.
1179 """
1180
1181 def __init__(self, **kw):
1182 for k, v in kw.items():
1183 setattr(self, k, v)
1184 self.pv_api = kw
1185 self.name = kw['pv_name']
1186 self.tags = parse_tags(kw['pv_tags'])
1187
1188 def __str__(self):
1189 return '<%s>' % self.pv_api['pv_name']
1190
1191 def __repr__(self):
1192 return self.__str__()
1193
1194 def set_tags(self, tags):
1195 """
1196 :param tags: A dictionary of tag names and values, like::
1197
1198 {
1199 "ceph.osd_fsid": "aaa-fff-bbbb",
1200 "ceph.osd_id": "0"
1201 }
1202
1203 At the end of all modifications, the tags are refreshed to reflect
1204 LVM's most current view.
1205 """
1206 for k, v in tags.items():
1207 self.set_tag(k, v)
1208 # after setting all the tags, refresh them for the current object, use the
1209 # pv_* identifiers to filter because those shouldn't change
1210 pv_object = get_pv(pv_name=self.pv_name, pv_uuid=self.pv_uuid)
1211 self.tags = pv_object.tags
1212
1213 def set_tag(self, key, value):
1214 """
1215 Set the key/value pair as an LVM tag. Does not "refresh" the values of
1216 the current object for its tags. Meant to be a "fire and forget" type
1217 of modification.
1218
1219 **warning**: Altering tags on a PV has to be done ensuring that the
1220 device is actually the one intended. ``pv_name`` is *not* a persistent
1221 value, only ``pv_uuid`` is. Using ``pv_uuid`` is the best way to make
1222 sure the device getting changed is the one needed.
1223 """
1224 # remove it first if it exists
1225 if self.tags.get(key):
1226 current_value = self.tags[key]
1227 tag = "%s=%s" % (key, current_value)
b32b8144 1228 process.call(['pvchange', '--deltag', tag, self.pv_name])
181888fb
FG
1229
1230 process.call(
1231 [
b32b8144 1232 'pvchange',
181888fb
FG
1233 '--addtag', '%s=%s' % (key, value), self.pv_name
1234 ]
1235 )