]> git.proxmox.com Git - ceph.git/blob - ceph/src/ceph-volume/ceph_volume/api/lvm.py
update sources to 12.2.7
[ceph.git] / ceph / src / ceph-volume / ceph_volume / api / lvm.py
1 """
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.
5 """
6 import logging
7 import os
8 from ceph_volume import process
9 from ceph_volume.exceptions import MultipleLVsError, MultipleVGsError, MultiplePVsError
10
11 logger = logging.getLogger(__name__)
12
13
14 def _output_parser(output, fields):
15 """
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)
20
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
24 """
25 field_items = fields.split(',')
26 report = []
27 for line in output:
28 # clear the leading/trailing whitespace
29 line = line.strip()
30
31 # remove the extra '"' in each field
32 line = line.replace('"', '')
33
34 # prevent moving forward with empty contents
35 if not line:
36 continue
37
38 # spliting on ';' because that is what the lvm call uses as
39 # '--separator'
40 output_items = [i.strip() for i in line.split(';')]
41 # map the output to the fiels
42 report.append(
43 dict(zip(field_items, output_items))
44 )
45
46 return report
47
48
49 def parse_tags(lv_tags):
50 """
51 Return a dictionary mapping of all the tags associated with
52 a Volume from the comma-separated tags coming from the LVM API
53
54 Input look like::
55
56 "ceph.osd_fsid=aaa-fff-bbbb,ceph.osd_id=0"
57
58 For the above example, the expected return value would be::
59
60 {
61 "ceph.osd_fsid": "aaa-fff-bbbb",
62 "ceph.osd_id": "0"
63 }
64 """
65 if not lv_tags:
66 return {}
67 tag_mapping = {}
68 tags = lv_tags.split(',')
69 for tag_assignment in tags:
70 if not tag_assignment.startswith('ceph.'):
71 continue
72 key, value = tag_assignment.split('=', 1)
73 tag_mapping[key] = value
74
75 return tag_mapping
76
77
78 def _vdo_parents(devices):
79 """
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
84 """
85 parent_devices = []
86 for parent in os.listdir('/sys/block'):
87 for slave in os.listdir('/sys/block/%s/slaves' % parent):
88 if slave in devices:
89 parent_devices.append('/dev/%s' % parent)
90 parent_devices.append(parent)
91 return parent_devices
92
93
94 def _vdo_slaves(vdo_names):
95 """
96 find all the slaves associated with each vdo name (from realpath) by going
97 into /sys/block/<realpath>/slaves
98 """
99 devices = []
100 for vdo_name in vdo_names:
101 mapper_path = '/dev/mapper/%s' % vdo_name
102 if not os.path.exists(mapper_path):
103 continue
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):
109 continue
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)
116 return devices
117
118
119 def _is_vdo(path):
120 """
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
125
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
129 """
130 if not os.path.isdir('/sys/kvdo'):
131 return False
132 realpath = os.path.realpath(path)
133 realpath_name = realpath.split('/')[-1]
134 devices = []
135 vdo_names = set()
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)
140
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))
144
145 # Find all possible parents, looking into slaves that are related to VDO
146 devices.extend(_vdo_parents(devices))
147
148 return any([
149 path in devices,
150 realpath in devices,
151 realpath_name in devices])
152
153
154 def is_vdo(path):
155 """
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
159 a ``False``.
160 """
161 try:
162 if _is_vdo(path):
163 return '1'
164 return '0'
165 except Exception:
166 logger.exception('Unable to properly detect device as VDO: %s', path)
167 return '0'
168
169
170 def get_api_vgs():
171 """
172 Return the list of group volumes available in the system using flags to
173 include common metadata associated with them
174
175 Command and sample delimited output should look like::
176
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
181
182 """
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]
186 )
187 return _output_parser(stdout, fields)
188
189
190 def get_api_lvs():
191 """
192 Return the list of logical volumes available in the system using flags to include common
193 metadata associated with them
194
195 Command and delimited output should look like::
196
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
200
201 """
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]
205 )
206 return _output_parser(stdout, fields)
207
208
209 def get_api_pvs():
210 """
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
213
214 This will only return physical volumes set up to work with LVM.
215
216 Command and delimited output should look like::
217
218 $ pvs --noheadings --readonly --separator=';' -o pv_name,pv_tags,pv_uuid
219 /dev/sda1;;
220 /dev/sdv;;07A4F654-4162-4600-8EB3-88D1E42F368D
221
222 """
223 fields = 'pv_name,pv_tags,pv_uuid,vg_name,lv_uuid'
224
225 stdout, stderr, returncode = process.call(
226 ['pvs', '--no-heading', '--readonly', '--separator=";"', '-o', fields]
227 )
228
229 return _output_parser(stdout, fields)
230
231
232 def get_lv_from_argument(argument):
233 """
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
237 """
238 if argument.startswith('/'):
239 lv = get_lv(lv_path=argument)
240 return lv
241 try:
242 vg_name, lv_name = argument.split('/')
243 except (ValueError, AttributeError):
244 return None
245 return get_lv(lv_name=lv_name, vg_name=vg_name)
246
247
248 def get_lv(lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
249 """
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
252 is found.
253
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.
257 """
258 if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]):
259 return None
260 lvs = Volumes()
261 return lvs.get(
262 lv_name=lv_name, vg_name=vg_name, lv_path=lv_path, lv_uuid=lv_uuid,
263 lv_tags=lv_tags
264 )
265
266
267 def get_pv(pv_name=None, pv_uuid=None, pv_tags=None):
268 """
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
271 pv is found.
272 """
273 if not any([pv_name, pv_uuid, pv_tags]):
274 return None
275 pvs = PVolumes()
276 return pvs.get(pv_name=pv_name, pv_uuid=pv_uuid, pv_tags=pv_tags)
277
278
279 def create_pv(device):
280 """
281 Create a physical volume from a device, useful when devices need to be later mapped
282 to journals.
283 """
284 process.run([
285 'pvcreate',
286 '-v', # verbose
287 '-f', # force it
288 '--yes', # answer yes to any prompts
289 device
290 ])
291
292
293 def create_vg(name, *devices):
294 """
295 Create a Volume Group. Command looks like::
296
297 vgcreate --force --yes group_name device
298
299 Once created the volume group is returned as a ``VolumeGroup`` object
300 """
301 process.run([
302 'vgcreate',
303 '--force',
304 '--yes',
305 name] + list(devices)
306 )
307
308 vg = get_vg(vg_name=name)
309 return vg
310
311
312 def remove_vg(vg_name):
313 """
314 Removes a volume group.
315 """
316 fail_msg = "Unable to remove vg %s" % vg_name
317 process.run(
318 [
319 'vgremove',
320 '-v', # verbose
321 '-f', # force it
322 vg_name
323 ],
324 fail_msg=fail_msg,
325 )
326
327
328 def remove_pv(pv_name):
329 """
330 Removes a physical volume.
331 """
332 fail_msg = "Unable to remove vg %s" % pv_name
333 process.run(
334 [
335 'pvremove',
336 '-v', # verbose
337 '-f', # force it
338 pv_name
339 ],
340 fail_msg=fail_msg,
341 )
342
343
344 def remove_lv(path):
345 """
346 Removes a logical volume given it's absolute path.
347
348 Will return True if the lv is successfully removed or
349 raises a RuntimeError if the removal fails.
350 """
351 stdout, stderr, returncode = process.call(
352 [
353 'lvremove',
354 '-v', # verbose
355 '-f', # force it
356 path
357 ],
358 show_command=True,
359 terminal_verbose=True,
360 )
361 if returncode != 0:
362 raise RuntimeError("Unable to remove %s" % path)
363 return True
364
365
366 def create_lv(name, group, size=None, tags=None):
367 """
368 Create a Logical Volume in a Volume Group. Command looks like::
369
370 lvcreate -L 50G -n gfslv vg0
371
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::
375
376 {"ceph.block_device": "/dev/ceph/osd-1"}
377 """
378 # XXX add CEPH_VOLUME_LVM_DEBUG to enable -vvvv on lv operations
379 type_path_tag = {
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
386 }
387 if size:
388 process.run([
389 'lvcreate',
390 '--yes',
391 '-L',
392 '%s' % size,
393 '-n', name, group
394 ])
395 # create the lv with all the space available, this is needed because the
396 # system call is different for LVM
397 else:
398 process.run([
399 'lvcreate',
400 '--yes',
401 '-l',
402 '100%FREE',
403 '-n', name, group
404 ])
405
406 lv = get_lv(lv_name=name, vg_name=group)
407 lv.set_tags(tags)
408
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'))
412 if path_tag:
413 lv.set_tags(
414 {path_tag: lv.lv_path}
415 )
416 return lv
417
418
419 def get_vg(vg_name=None, vg_tags=None):
420 """
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.
423
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.
426 """
427 if not any([vg_name, vg_tags]):
428 return None
429 vgs = VolumeGroups()
430 return vgs.get(vg_name=vg_name, vg_tags=vg_tags)
431
432
433 class VolumeGroups(list):
434 """
435 A list of all known volume groups for the current system, with the ability
436 to filter them via keyword arguments.
437 """
438
439 def __init__(self):
440 self._populate()
441
442 def _populate(self):
443 # get all the vgs in the current system
444 for vg_item in get_api_vgs():
445 self.append(VolumeGroup(**vg_item))
446
447 def _purge(self):
448 """
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
452 """
453 self[:] = []
454
455 def _filter(self, vg_name=None, vg_tags=None):
456 """
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.
460
461 .. note:: ``vg_tags`` is not yet implemented
462 """
463 filtered = [i for i in self]
464 if vg_name:
465 filtered = [i for i in filtered if i.vg_name == vg_name]
466
467 # at this point, `filtered` has either all the volumes in self or is an
468 # actual filtered list if any filters were applied
469 if vg_tags:
470 tag_filtered = []
471 for volume in filtered:
472 matches = all(volume.tags.get(k) == str(v) for k, v in vg_tags.items())
473 if matches:
474 tag_filtered.append(volume)
475 return tag_filtered
476
477 return filtered
478
479 def filter(self, vg_name=None, vg_tags=None):
480 """
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::
484
485 vg_tags={'ceph.type': 'dmcache'}
486
487 .. warning:: These tags are not documented because they are currently
488 unused, but are here to maintain API consistency
489 """
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(
494 vg_name=vg_name,
495 vg_tags=vg_tags
496 )
497 # then purge everything
498 self._purge()
499 # and add the filtered items
500 self.extend(filtered_groups)
501
502 def get(self, vg_name=None, vg_tags=None):
503 """
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.
507
508 This method does *not* alter the list, and it will raise an error if
509 multiple VGs are matched
510
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)
513 """
514 if not any([vg_name, vg_tags]):
515 return None
516 vgs = self._filter(
517 vg_name=vg_name,
518 vg_tags=vg_tags
519 )
520 if not vgs:
521 return None
522 if len(vgs) > 1:
523 # this is probably never going to happen, but it is here to keep
524 # the API code consistent
525 raise MultipleVGsError(vg_name)
526 return vgs[0]
527
528
529 class Volumes(list):
530 """
531 A list of all known (logical) volumes for the current system, with the ability
532 to filter them via keyword arguments.
533 """
534
535 def __init__(self):
536 self._populate()
537
538 def _populate(self):
539 # get all the lvs in the current system
540 for lv_item in get_api_lvs():
541 self.append(Volume(**lv_item))
542
543 def _purge(self):
544 """
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
548 """
549 self[:] = []
550
551 def _filter(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
552 """
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.
556 """
557 filtered = [i for i in self]
558 if lv_name:
559 filtered = [i for i in filtered if i.lv_name == lv_name]
560
561 if vg_name:
562 filtered = [i for i in filtered if i.vg_name == vg_name]
563
564 if lv_uuid:
565 filtered = [i for i in filtered if i.lv_uuid == lv_uuid]
566
567 if lv_path:
568 filtered = [i for i in filtered if i.lv_path == lv_path]
569
570 # at this point, `filtered` has either all the volumes in self or is an
571 # actual filtered list if any filters were applied
572 if lv_tags:
573 tag_filtered = []
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())
577 if matches:
578 tag_filtered.append(volume)
579 return tag_filtered
580
581 return filtered
582
583 def filter(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
584 """
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::
588
589 lv_tags={'ceph.osd_id': '0'}
590
591 """
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(
596 lv_name=lv_name,
597 vg_name=vg_name,
598 lv_path=lv_path,
599 lv_uuid=lv_uuid,
600 lv_tags=lv_tags
601 )
602 # then purge everything
603 self._purge()
604 # and add the filtered items
605 self.extend(filtered_volumes)
606
607 def get(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
608 """
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.
612
613 This method does *not* alter the list, and it will raise an error if
614 multiple LVs are matched
615
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.
619 """
620 if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]):
621 return None
622 lvs = self._filter(
623 lv_name=lv_name,
624 vg_name=vg_name,
625 lv_path=lv_path,
626 lv_uuid=lv_uuid,
627 lv_tags=lv_tags
628 )
629 if not lvs:
630 return None
631 if len(lvs) > 1:
632 raise MultipleLVsError(lv_name, lv_path)
633 return lvs[0]
634
635
636 class PVolumes(list):
637 """
638 A list of all known (physical) volumes for the current system, with the ability
639 to filter them via keyword arguments.
640 """
641
642 def __init__(self):
643 self._populate()
644
645 def _populate(self):
646 # get all the pvs in the current system
647 for pv_item in get_api_pvs():
648 self.append(PVolume(**pv_item))
649
650 def _purge(self):
651 """
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
655 """
656 self[:] = []
657
658 def _filter(self, pv_name=None, pv_uuid=None, pv_tags=None):
659 """
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.
663 """
664 filtered = [i for i in self]
665 if pv_name:
666 filtered = [i for i in filtered if i.pv_name == pv_name]
667
668 if pv_uuid:
669 filtered = [i for i in filtered if i.pv_uuid == pv_uuid]
670
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
673 if pv_tags:
674 tag_filtered = []
675 for pvolume in filtered:
676 matches = all(pvolume.tags.get(k) == str(v) for k, v in pv_tags.items())
677 if matches:
678 tag_filtered.append(pvolume)
679 # return the tag_filtered pvolumes here, the `filtered` list is no
680 # longer useable
681 return tag_filtered
682
683 return filtered
684
685 def filter(self, pv_name=None, pv_uuid=None, pv_tags=None):
686 """
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::
690
691 pv_tags={'ceph.osd_id': '0'}
692
693 """
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(
698 pv_name=pv_name,
699 pv_uuid=pv_uuid,
700 pv_tags=pv_tags
701 )
702 # then purge everything
703 self._purge()
704 # and add the filtered items
705 self.extend(filtered_volumes)
706
707 def get(self, pv_name=None, pv_uuid=None, pv_tags=None):
708 """
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.
712
713 This method does *not* alter the list, and it will raise an error if
714 multiple pvs are matched
715
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.
719 """
720 if not any([pv_name, pv_uuid, pv_tags]):
721 return None
722 pvs = self._filter(
723 pv_name=pv_name,
724 pv_uuid=pv_uuid,
725 pv_tags=pv_tags
726 )
727 if not pvs:
728 return None
729 if len(pvs) > 1:
730 raise MultiplePVsError(pv_name)
731 return pvs[0]
732
733
734 class VolumeGroup(object):
735 """
736 Represents an LVM group, with some top-level attributes like ``vg_name``
737 """
738
739 def __init__(self, **kw):
740 for k, v in kw.items():
741 setattr(self, k, v)
742 self.name = kw['vg_name']
743 self.tags = parse_tags(kw.get('vg_tags', ''))
744
745 def __str__(self):
746 return '<%s>' % self.name
747
748 def __repr__(self):
749 return self.__str__()
750
751
752 class Volume(object):
753 """
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.
756 """
757
758 def __init__(self, **kw):
759 for k, v in kw.items():
760 setattr(self, k, v)
761 self.lv_api = kw
762 self.name = kw['lv_name']
763 self.tags = parse_tags(kw['lv_tags'])
764 self.encrypted = self.tags.get('ceph.encrypted', '0') == '1'
765
766 def __str__(self):
767 return '<%s>' % self.lv_api['lv_path']
768
769 def __repr__(self):
770 return self.__str__()
771
772 def as_dict(self):
773 obj = {}
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
779 return obj
780
781 def clear_tags(self):
782 """
783 Removes all tags from the Logical Volume.
784 """
785 for k, v in self.tags.items():
786 tag = "%s=%s" % (k, v)
787 process.run(['lvchange', '--deltag', tag, self.lv_path])
788
789 def set_tags(self, tags):
790 """
791 :param tags: A dictionary of tag names and values, like::
792
793 {
794 "ceph.osd_fsid": "aaa-fff-bbbb",
795 "ceph.osd_id": "0"
796 }
797
798 At the end of all modifications, the tags are refreshed to reflect
799 LVM's most current view.
800 """
801 for k, v in tags.items():
802 self.set_tag(k, v)
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
807
808 def set_tag(self, key, value):
809 """
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
812 of modification.
813 """
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']])
819
820 process.call(
821 [
822 'lvchange',
823 '--addtag', '%s=%s' % (key, value), self.lv_path
824 ]
825 )
826
827
828 class PVolume(object):
829 """
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.
832 """
833
834 def __init__(self, **kw):
835 for k, v in kw.items():
836 setattr(self, k, v)
837 self.pv_api = kw
838 self.name = kw['pv_name']
839 self.tags = parse_tags(kw['pv_tags'])
840
841 def __str__(self):
842 return '<%s>' % self.pv_api['pv_name']
843
844 def __repr__(self):
845 return self.__str__()
846
847 def set_tags(self, tags):
848 """
849 :param tags: A dictionary of tag names and values, like::
850
851 {
852 "ceph.osd_fsid": "aaa-fff-bbbb",
853 "ceph.osd_id": "0"
854 }
855
856 At the end of all modifications, the tags are refreshed to reflect
857 LVM's most current view.
858 """
859 for k, v in tags.items():
860 self.set_tag(k, v)
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
865
866 def set_tag(self, key, value):
867 """
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
870 of modification.
871
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.
876 """
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])
882
883 process.call(
884 [
885 'pvchange',
886 '--addtag', '%s=%s' % (key, value), self.pv_name
887 ]
888 )