Fix some bugs when testing opensds ansible
[stor4nfv.git] / src / 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 from ceph_volume import process
7 from ceph_volume.exceptions import MultipleLVsError, MultipleVGsError, MultiplePVsError
8
9
10 def _output_parser(output, fields):
11     """
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)
16
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
20     """
21     field_items = fields.split(',')
22     report = []
23     for line in output:
24         # clear the leading/trailing whitespace
25         line = line.strip()
26
27         # remove the extra '"' in each field
28         line = line.replace('"', '')
29
30         # prevent moving forward with empty contents
31         if not line:
32             continue
33
34         # spliting on ';' because that is what the lvm call uses as
35         # '--separator'
36         output_items = [i.strip() for i in line.split(';')]
37         # map the output to the fiels
38         report.append(
39             dict(zip(field_items, output_items))
40         )
41
42     return report
43
44
45 def parse_tags(lv_tags):
46     """
47     Return a dictionary mapping of all the tags associated with
48     a Volume from the comma-separated tags coming from the LVM API
49
50     Input look like::
51
52        "ceph.osd_fsid=aaa-fff-bbbb,ceph.osd_id=0"
53
54     For the above example, the expected return value would be::
55
56         {
57             "ceph.osd_fsid": "aaa-fff-bbbb",
58             "ceph.osd_id": "0"
59         }
60     """
61     if not lv_tags:
62         return {}
63     tag_mapping = {}
64     tags = lv_tags.split(',')
65     for tag_assignment in tags:
66         if not tag_assignment.startswith('ceph.'):
67             continue
68         key, value = tag_assignment.split('=', 1)
69         tag_mapping[key] = value
70
71     return tag_mapping
72
73
74 def get_api_vgs():
75     """
76     Return the list of group volumes available in the system using flags to
77     include common metadata associated with them
78
79     Command and sample delimeted output, should look like::
80
81         $ vgs --noheadings --separator=';' \
82           -o vg_name,pv_count,lv_count,snap_count,vg_attr,vg_size,vg_free
83           ubuntubox-vg;1;2;0;wz--n-;299.52g;12.00m
84           osd_vg;3;1;0;wz--n-;29.21g;9.21g
85
86     """
87     fields = 'vg_name,pv_count,lv_count,snap_count,vg_attr,vg_size,vg_free'
88     stdout, stderr, returncode = process.call(
89         ['vgs', '--noheadings', '--separator=";"', '-o', fields]
90     )
91     return _output_parser(stdout, fields)
92
93
94 def get_api_lvs():
95     """
96     Return the list of logical volumes available in the system using flags to include common
97     metadata associated with them
98
99     Command and delimeted output, should look like::
100
101         $ lvs --noheadings --separator=';' -o lv_tags,lv_path,lv_name,vg_name
102           ;/dev/ubuntubox-vg/root;root;ubuntubox-vg
103           ;/dev/ubuntubox-vg/swap_1;swap_1;ubuntubox-vg
104
105     """
106     fields = 'lv_tags,lv_path,lv_name,vg_name,lv_uuid'
107     stdout, stderr, returncode = process.call(
108         ['lvs', '--noheadings', '--separator=";"', '-o', fields]
109     )
110     return _output_parser(stdout, fields)
111
112
113 def get_api_pvs():
114     """
115     Return the list of physical volumes configured for lvm and available in the
116     system using flags to include common metadata associated with them like the uuid
117
118     Command and delimeted output, should look like::
119
120         $ pvs --noheadings --separator=';' -o pv_name,pv_tags,pv_uuid
121           /dev/sda1;;
122           /dev/sdv;;07A4F654-4162-4600-8EB3-88D1E42F368D
123
124     """
125     fields = 'pv_name,pv_tags,pv_uuid'
126
127     # note the use of `pvs -a` which will return every physical volume including
128     # ones that have not been initialized as "pv" by LVM
129     stdout, stderr, returncode = process.call(
130         ['pvs', '-a', '--no-heading', '--separator=";"', '-o', fields]
131     )
132
133     return _output_parser(stdout, fields)
134
135
136 def get_lv_from_argument(argument):
137     """
138     Helper proxy function that consumes a possible logical volume passed in from the CLI
139     in the form of `vg/lv`, but with some validation so that an argument that is a full
140     path to a device can be ignored
141     """
142     if argument.startswith('/'):
143         lv = get_lv(lv_path=argument)
144         return lv
145     try:
146         vg_name, lv_name = argument.split('/')
147     except (ValueError, AttributeError):
148         return None
149     return get_lv(lv_name=lv_name, vg_name=vg_name)
150
151
152 def get_lv(lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
153     """
154     Return a matching lv for the current system, requiring ``lv_name``,
155     ``vg_name``, ``lv_path`` or ``tags``. Raises an error if more than one lv
156     is found.
157
158     It is useful to use ``tags`` when trying to find a specific logical volume,
159     but it can also lead to multiple lvs being found, since a lot of metadata
160     is shared between lvs of a distinct OSD.
161     """
162     if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]):
163         return None
164     lvs = Volumes()
165     return lvs.get(
166         lv_name=lv_name, vg_name=vg_name, lv_path=lv_path, lv_uuid=lv_uuid,
167         lv_tags=lv_tags
168     )
169
170
171 def get_pv(pv_name=None, pv_uuid=None, pv_tags=None):
172     """
173     Return a matching pv (physical volume) for the current system, requiring
174     ``pv_name``, ``pv_uuid``, or ``pv_tags``. Raises an error if more than one
175     pv is found.
176     """
177     if not any([pv_name, pv_uuid, pv_tags]):
178         return None
179     pvs = PVolumes()
180     return pvs.get(pv_name=pv_name, pv_uuid=pv_uuid, pv_tags=pv_tags)
181
182
183 def create_pv(device):
184     """
185     Create a physical volume from a device, useful when devices need to be later mapped
186     to journals.
187     """
188     process.run([
189         'pvcreate',
190         '-v',  # verbose
191         '-f',  # force it
192         '--yes', # answer yes to any prompts
193         device
194     ])
195
196
197 def create_vg(name, *devices):
198     """
199     Create a Volume Group. Command looks like::
200
201         vgcreate --force --yes group_name device
202
203     Once created the volume group is returned as a ``VolumeGroup`` object
204     """
205     process.run([
206         'vgcreate',
207         '--force',
208         '--yes',
209         name] + list(devices)
210     )
211
212     vg = get_vg(vg_name=name)
213     return vg
214
215
216 def remove_lv(path):
217     """
218     Removes a logical volume given it's absolute path.
219
220     Will return True if the lv is successfully removed or
221     raises a RuntimeError if the removal fails.
222     """
223     stdout, stderr, returncode = process.call(
224         [
225             'lvremove',
226             '-v',  # verbose
227             '-f',  # force it
228             path
229         ],
230         show_command=True,
231         terminal_verbose=True,
232     )
233     if returncode != 0:
234         raise RuntimeError("Unable to remove %s".format(path))
235     return True
236
237
238 def create_lv(name, group, size=None, tags=None):
239     """
240     Create a Logical Volume in a Volume Group. Command looks like::
241
242         lvcreate -L 50G -n gfslv vg0
243
244     ``name``, ``group``, are required. If ``size`` is provided it must follow
245     lvm's size notation (like 1G, or 20M). Tags are an optional dictionary and is expected to
246     conform to the convention of prefixing them with "ceph." like::
247
248         {"ceph.block_device": "/dev/ceph/osd-1"}
249     """
250     # XXX add CEPH_VOLUME_LVM_DEBUG to enable -vvvv on lv operations
251     type_path_tag = {
252         'journal': 'ceph.journal_device',
253         'data': 'ceph.data_device',
254         'block': 'ceph.block_device',
255         'wal': 'ceph.wal_device',
256         'db': 'ceph.db_device',
257         'lockbox': 'ceph.lockbox_device',  # XXX might not ever need this lockbox sorcery
258     }
259     if size:
260         process.run([
261             'lvcreate',
262             '--yes',
263             '-L',
264             '%s' % size,
265             '-n', name, group
266         ])
267     # create the lv with all the space available, this is needed because the
268     # system call is different for LVM
269     else:
270         process.run([
271             'lvcreate',
272             '--yes',
273             '-l',
274             '100%FREE',
275             '-n', name, group
276         ])
277
278     lv = get_lv(lv_name=name, vg_name=group)
279     lv.set_tags(tags)
280
281     # when creating a distinct type, the caller doesn't know what the path will
282     # be so this function will set it after creation using the mapping
283     path_tag = type_path_tag.get(tags.get('ceph.type'))
284     if path_tag:
285         lv.set_tags(
286             {path_tag: lv.lv_path}
287         )
288     return lv
289
290
291 def get_vg(vg_name=None, vg_tags=None):
292     """
293     Return a matching vg for the current system, requires ``vg_name`` or
294     ``tags``. Raises an error if more than one vg is found.
295
296     It is useful to use ``tags`` when trying to find a specific volume group,
297     but it can also lead to multiple vgs being found.
298     """
299     if not any([vg_name, vg_tags]):
300         return None
301     vgs = VolumeGroups()
302     return vgs.get(vg_name=vg_name, vg_tags=vg_tags)
303
304
305 class VolumeGroups(list):
306     """
307     A list of all known volume groups for the current system, with the ability
308     to filter them via keyword arguments.
309     """
310
311     def __init__(self):
312         self._populate()
313
314     def _populate(self):
315         # get all the vgs in the current system
316         for vg_item in get_api_vgs():
317             self.append(VolumeGroup(**vg_item))
318
319     def _purge(self):
320         """
321         Deplete all the items in the list, used internally only so that we can
322         dynamically allocate the items when filtering without the concern of
323         messing up the contents
324         """
325         self[:] = []
326
327     def _filter(self, vg_name=None, vg_tags=None):
328         """
329         The actual method that filters using a new list. Useful so that other
330         methods that do not want to alter the contents of the list (e.g.
331         ``self.find``) can operate safely.
332
333         .. note:: ``vg_tags`` is not yet implemented
334         """
335         filtered = [i for i in self]
336         if vg_name:
337             filtered = [i for i in filtered if i.vg_name == vg_name]
338
339         # at this point, `filtered` has either all the volumes in self or is an
340         # actual filtered list if any filters were applied
341         if vg_tags:
342             tag_filtered = []
343             for volume in filtered:
344                 matches = all(volume.tags.get(k) == str(v) for k, v in vg_tags.items())
345                 if matches:
346                     tag_filtered.append(volume)
347             return tag_filtered
348
349         return filtered
350
351     def filter(self, vg_name=None, vg_tags=None):
352         """
353         Filter out groups on top level attributes like ``vg_name`` or by
354         ``vg_tags`` where a dict is required. For example, to find a Ceph group
355         with dmcache as the type, the filter would look like::
356
357             vg_tags={'ceph.type': 'dmcache'}
358
359         .. warning:: These tags are not documented because they are currently
360                      unused, but are here to maintain API consistency
361         """
362         if not any([vg_name, vg_tags]):
363             raise TypeError('.filter() requires vg_name or vg_tags (none given)')
364         # first find the filtered volumes with the values in self
365         filtered_groups = self._filter(
366             vg_name=vg_name,
367             vg_tags=vg_tags
368         )
369         # then purge everything
370         self._purge()
371         # and add the filtered items
372         self.extend(filtered_groups)
373
374     def get(self, vg_name=None, vg_tags=None):
375         """
376         This is a bit expensive, since it will try to filter out all the
377         matching items in the list, filter them out applying anything that was
378         added and return the matching item.
379
380         This method does *not* alter the list, and it will raise an error if
381         multiple VGs are matched
382
383         It is useful to use ``tags`` when trying to find a specific volume group,
384         but it can also lead to multiple vgs being found (although unlikely)
385         """
386         if not any([vg_name, vg_tags]):
387             return None
388         vgs = self._filter(
389             vg_name=vg_name,
390             vg_tags=vg_tags
391         )
392         if not vgs:
393             return None
394         if len(vgs) > 1:
395             # this is probably never going to happen, but it is here to keep
396             # the API code consistent
397             raise MultipleVGsError(vg_name)
398         return vgs[0]
399
400
401 class Volumes(list):
402     """
403     A list of all known (logical) volumes for the current system, with the ability
404     to filter them via keyword arguments.
405     """
406
407     def __init__(self):
408         self._populate()
409
410     def _populate(self):
411         # get all the lvs in the current system
412         for lv_item in get_api_lvs():
413             self.append(Volume(**lv_item))
414
415     def _purge(self):
416         """
417         Deplete all the items in the list, used internally only so that we can
418         dynamically allocate the items when filtering without the concern of
419         messing up the contents
420         """
421         self[:] = []
422
423     def _filter(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
424         """
425         The actual method that filters using a new list. Useful so that other
426         methods that do not want to alter the contents of the list (e.g.
427         ``self.find``) can operate safely.
428         """
429         filtered = [i for i in self]
430         if lv_name:
431             filtered = [i for i in filtered if i.lv_name == lv_name]
432
433         if vg_name:
434             filtered = [i for i in filtered if i.vg_name == vg_name]
435
436         if lv_uuid:
437             filtered = [i for i in filtered if i.lv_uuid == lv_uuid]
438
439         if lv_path:
440             filtered = [i for i in filtered if i.lv_path == lv_path]
441
442         # at this point, `filtered` has either all the volumes in self or is an
443         # actual filtered list if any filters were applied
444         if lv_tags:
445             tag_filtered = []
446             for volume in filtered:
447                 # all the tags we got need to match on the volume
448                 matches = all(volume.tags.get(k) == str(v) for k, v in lv_tags.items())
449                 if matches:
450                     tag_filtered.append(volume)
451             return tag_filtered
452
453         return filtered
454
455     def filter(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
456         """
457         Filter out volumes on top level attributes like ``lv_name`` or by
458         ``lv_tags`` where a dict is required. For example, to find a volume
459         that has an OSD ID of 0, the filter would look like::
460
461             lv_tags={'ceph.osd_id': '0'}
462
463         """
464         if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]):
465             raise TypeError('.filter() requires lv_name, vg_name, lv_path, lv_uuid, or tags (none given)')
466         # first find the filtered volumes with the values in self
467         filtered_volumes = self._filter(
468             lv_name=lv_name,
469             vg_name=vg_name,
470             lv_path=lv_path,
471             lv_uuid=lv_uuid,
472             lv_tags=lv_tags
473         )
474         # then purge everything
475         self._purge()
476         # and add the filtered items
477         self.extend(filtered_volumes)
478
479     def get(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
480         """
481         This is a bit expensive, since it will try to filter out all the
482         matching items in the list, filter them out applying anything that was
483         added and return the matching item.
484
485         This method does *not* alter the list, and it will raise an error if
486         multiple LVs are matched
487
488         It is useful to use ``tags`` when trying to find a specific logical volume,
489         but it can also lead to multiple lvs being found, since a lot of metadata
490         is shared between lvs of a distinct OSD.
491         """
492         if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]):
493             return None
494         lvs = self._filter(
495             lv_name=lv_name,
496             vg_name=vg_name,
497             lv_path=lv_path,
498             lv_uuid=lv_uuid,
499             lv_tags=lv_tags
500         )
501         if not lvs:
502             return None
503         if len(lvs) > 1:
504             raise MultipleLVsError(lv_name, lv_path)
505         return lvs[0]
506
507
508 class PVolumes(list):
509     """
510     A list of all known (physical) volumes for the current system, with the ability
511     to filter them via keyword arguments.
512     """
513
514     def __init__(self):
515         self._populate()
516
517     def _populate(self):
518         # get all the pvs in the current system
519         for pv_item in get_api_pvs():
520             self.append(PVolume(**pv_item))
521
522     def _purge(self):
523         """
524         Deplete all the items in the list, used internally only so that we can
525         dynamically allocate the items when filtering without the concern of
526         messing up the contents
527         """
528         self[:] = []
529
530     def _filter(self, pv_name=None, pv_uuid=None, pv_tags=None):
531         """
532         The actual method that filters using a new list. Useful so that other
533         methods that do not want to alter the contents of the list (e.g.
534         ``self.find``) can operate safely.
535         """
536         filtered = [i for i in self]
537         if pv_name:
538             filtered = [i for i in filtered if i.pv_name == pv_name]
539
540         if pv_uuid:
541             filtered = [i for i in filtered if i.pv_uuid == pv_uuid]
542
543         # at this point, `filtered` has either all the physical volumes in self
544         # or is an actual filtered list if any filters were applied
545         if pv_tags:
546             tag_filtered = []
547             for pvolume in filtered:
548                 matches = all(pvolume.tags.get(k) == str(v) for k, v in pv_tags.items())
549                 if matches:
550                     tag_filtered.append(pvolume)
551             # return the tag_filtered pvolumes here, the `filtered` list is no
552             # longer useable
553             return tag_filtered
554
555         return filtered
556
557     def filter(self, pv_name=None, pv_uuid=None, pv_tags=None):
558         """
559         Filter out volumes on top level attributes like ``pv_name`` or by
560         ``pv_tags`` where a dict is required. For example, to find a physical volume
561         that has an OSD ID of 0, the filter would look like::
562
563             pv_tags={'ceph.osd_id': '0'}
564
565         """
566         if not any([pv_name, pv_uuid, pv_tags]):
567             raise TypeError('.filter() requires pv_name, pv_uuid, or pv_tags (none given)')
568         # first find the filtered volumes with the values in self
569         filtered_volumes = self._filter(
570             pv_name=pv_name,
571             pv_uuid=pv_uuid,
572             pv_tags=pv_tags
573         )
574         # then purge everything
575         self._purge()
576         # and add the filtered items
577         self.extend(filtered_volumes)
578
579     def get(self, pv_name=None, pv_uuid=None, pv_tags=None):
580         """
581         This is a bit expensive, since it will try to filter out all the
582         matching items in the list, filter them out applying anything that was
583         added and return the matching item.
584
585         This method does *not* alter the list, and it will raise an error if
586         multiple pvs are matched
587
588         It is useful to use ``tags`` when trying to find a specific logical volume,
589         but it can also lead to multiple pvs being found, since a lot of metadata
590         is shared between pvs of a distinct OSD.
591         """
592         if not any([pv_name, pv_uuid, pv_tags]):
593             return None
594         pvs = self._filter(
595             pv_name=pv_name,
596             pv_uuid=pv_uuid,
597             pv_tags=pv_tags
598         )
599         if not pvs:
600             return None
601         if len(pvs) > 1:
602             raise MultiplePVsError(pv_name)
603         return pvs[0]
604
605
606 class VolumeGroup(object):
607     """
608     Represents an LVM group, with some top-level attributes like ``vg_name``
609     """
610
611     def __init__(self, **kw):
612         for k, v in kw.items():
613             setattr(self, k, v)
614         self.name = kw['vg_name']
615         self.tags = parse_tags(kw.get('vg_tags', ''))
616
617     def __str__(self):
618         return '<%s>' % self.name
619
620     def __repr__(self):
621         return self.__str__()
622
623
624 class Volume(object):
625     """
626     Represents a Logical Volume from LVM, with some top-level attributes like
627     ``lv_name`` and parsed tags as a dictionary of key/value pairs.
628     """
629
630     def __init__(self, **kw):
631         for k, v in kw.items():
632             setattr(self, k, v)
633         self.lv_api = kw
634         self.name = kw['lv_name']
635         self.tags = parse_tags(kw['lv_tags'])
636
637     def __str__(self):
638         return '<%s>' % self.lv_api['lv_path']
639
640     def __repr__(self):
641         return self.__str__()
642
643     def as_dict(self):
644         obj = {}
645         obj.update(self.lv_api)
646         obj['tags'] = self.tags
647         obj['name'] = self.name
648         obj['type'] = self.tags['ceph.type']
649         obj['path'] = self.lv_path
650         return obj
651
652     def clear_tags(self):
653         """
654         Removes all tags from the Logical Volume.
655         """
656         for k, v in self.tags.items():
657             tag = "%s=%s" % (k, v)
658             process.run(['lvchange', '--deltag', tag, self.lv_path])
659
660     def set_tags(self, tags):
661         """
662         :param tags: A dictionary of tag names and values, like::
663
664             {
665                 "ceph.osd_fsid": "aaa-fff-bbbb",
666                 "ceph.osd_id": "0"
667             }
668
669         At the end of all modifications, the tags are refreshed to reflect
670         LVM's most current view.
671         """
672         for k, v in tags.items():
673             self.set_tag(k, v)
674         # after setting all the tags, refresh them for the current object, use the
675         # lv_* identifiers to filter because those shouldn't change
676         lv_object = get_lv(lv_name=self.lv_name, lv_path=self.lv_path)
677         self.tags = lv_object.tags
678
679     def set_tag(self, key, value):
680         """
681         Set the key/value pair as an LVM tag. Does not "refresh" the values of
682         the current object for its tags. Meant to be a "fire and forget" type
683         of modification.
684         """
685         # remove it first if it exists
686         if self.tags.get(key):
687             current_value = self.tags[key]
688             tag = "%s=%s" % (key, current_value)
689             process.call(['lvchange', '--deltag', tag, self.lv_api['lv_path']])
690
691         process.call(
692             [
693                 'lvchange',
694                 '--addtag', '%s=%s' % (key, value), self.lv_path
695             ]
696         )
697
698
699 class PVolume(object):
700     """
701     Represents a Physical Volume from LVM, with some top-level attributes like
702     ``pv_name`` and parsed tags as a dictionary of key/value pairs.
703     """
704
705     def __init__(self, **kw):
706         for k, v in kw.items():
707             setattr(self, k, v)
708         self.pv_api = kw
709         self.name = kw['pv_name']
710         self.tags = parse_tags(kw['pv_tags'])
711
712     def __str__(self):
713         return '<%s>' % self.pv_api['pv_name']
714
715     def __repr__(self):
716         return self.__str__()
717
718     def set_tags(self, tags):
719         """
720         :param tags: A dictionary of tag names and values, like::
721
722             {
723                 "ceph.osd_fsid": "aaa-fff-bbbb",
724                 "ceph.osd_id": "0"
725             }
726
727         At the end of all modifications, the tags are refreshed to reflect
728         LVM's most current view.
729         """
730         for k, v in tags.items():
731             self.set_tag(k, v)
732         # after setting all the tags, refresh them for the current object, use the
733         # pv_* identifiers to filter because those shouldn't change
734         pv_object = get_pv(pv_name=self.pv_name, pv_uuid=self.pv_uuid)
735         self.tags = pv_object.tags
736
737     def set_tag(self, key, value):
738         """
739         Set the key/value pair as an LVM tag. Does not "refresh" the values of
740         the current object for its tags. Meant to be a "fire and forget" type
741         of modification.
742
743         **warning**: Altering tags on a PV has to be done ensuring that the
744         device is actually the one intended. ``pv_name`` is *not* a persistent
745         value, only ``pv_uuid`` is. Using ``pv_uuid`` is the best way to make
746         sure the device getting changed is the one needed.
747         """
748         # remove it first if it exists
749         if self.tags.get(key):
750             current_value = self.tags[key]
751             tag = "%s=%s" % (key, current_value)
752             process.call(['pvchange', '--deltag', tag, self.pv_name])
753
754         process.call(
755             [
756                 'pvchange',
757                 '--addtag', '%s=%s' % (key, value), self.pv_name
758             ]
759         )