Skip to content

Commit bf9bdd3

Browse files
authored
Merge pull request ceph#62026 from guits/cv-api-lvm-refact
ceph-volume: Refactor LVM object handling
2 parents 0282e7f + 7193ada commit bf9bdd3

File tree

3 files changed

+119
-130
lines changed

3 files changed

+119
-130
lines changed

src/ceph-volume/ceph_volume/api/lvm.py

Lines changed: 108 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import logging
77
import os
88
import uuid
9-
from itertools import repeat
109
from math import floor
1110
from ceph_volume import process, util, conf
1211
from ceph_volume.exceptions import SizeAllocationError
@@ -323,17 +322,103 @@ def dmsetup_splitname(dev: str) -> Dict[str, Any]:
323322

324323

325324
def is_ceph_device(lv: "Volume") -> bool:
326-
try:
327-
lv.tags['ceph.osd_id']
328-
except (KeyError, AttributeError):
329-
logger.warning('device is not part of ceph: %s', lv)
330-
return False
325+
osd_id = lv.tags.get('ceph.osd_id', 'null')
331326

332-
if lv.tags['ceph.osd_id'] == 'null':
333-
return False
334-
else:
335-
return True
327+
if osd_id == 'null':
328+
logger.warning('device is not part of ceph: %s', lv)
329+
return False
330+
331+
return True
336332

333+
class Lvm:
334+
def __init__(self, name_key: str, tags_key: str, **kw: Any) -> None:
335+
self.name: str = kw.get(name_key, '')
336+
self.binary_change: str = ''
337+
self.path: str = ''
338+
if not self.name:
339+
raise ValueError(f'{self.__class__.__name__} must have a non-empty name')
340+
341+
self.api_data = kw
342+
self.tags = parse_tags(kw.get(tags_key, ''))
343+
344+
for k, v in kw.items():
345+
setattr(self, k, v)
346+
347+
def __str__(self) -> str:
348+
return f'<{self.name}>'
349+
350+
def __repr__(self) -> str:
351+
return self.__str__()
352+
353+
def _format_tag_args(self, op: str, tags: Dict[str, Any]) -> List[str]:
354+
result: List[str] = []
355+
for k, v in tags.items():
356+
result.extend([op, f'{k}={v}'])
357+
return result
358+
359+
def clear_tags(self, keys: Optional[List[str]] = None) -> None:
360+
"""
361+
Removes all or passed tags.
362+
"""
363+
if not keys:
364+
keys = list(self.tags.keys())
365+
366+
del_tags = {k: self.tags[k] for k in keys if k in self.tags}
367+
if not del_tags:
368+
# nothing to clear
369+
return
370+
del_tag_args = self._format_tag_args('--deltag', del_tags)
371+
# --deltag returns successful even if the to be deleted tag is not set
372+
process.call([self.binary_change] + del_tag_args + [self.path], run_on_host=True)
373+
for k in del_tags.keys():
374+
del self.tags[k]
375+
376+
def clear_tag(self, key: str) -> None:
377+
if self.tags.get(key):
378+
current_value = self.tags[key]
379+
tag = "%s=%s" % (key, current_value)
380+
process.call([self.binary_change, '--deltag', tag, self.path], run_on_host=True)
381+
del self.tags[key]
382+
383+
def set_tag(self, key: str, value: str) -> None:
384+
"""
385+
Set the key/value pair as an LVM tag.
386+
"""
387+
# remove it first if it exists
388+
self.clear_tag(key)
389+
390+
process.call(
391+
[
392+
self.binary_change,
393+
'--addtag', '%s=%s' % (key, value), self.path
394+
],
395+
run_on_host=True
396+
)
397+
self.tags[key] = value
398+
399+
def set_tags(self, tags: Dict[str, Any]) -> None:
400+
"""
401+
:param tags: A dictionary of tag names and values, like::
402+
403+
{
404+
"ceph.osd_fsid": "aaa-fff-bbbb",
405+
"ceph.osd_id": "0"
406+
}
407+
408+
At the end of all modifications, the tags are refreshed to reflect
409+
LVM's most current view.
410+
"""
411+
self.clear_tags(list(tags.keys()))
412+
add_tag_args = self._format_tag_args('--addtag', tags)
413+
process.call([self.binary_change] + add_tag_args + [self.path], run_on_host=True)
414+
for k, v in tags.items():
415+
self.tags[k] = v
416+
417+
def deactivate(self) -> None:
418+
"""
419+
Deactivate the LV by calling lvchange -an
420+
"""
421+
process.call([self.binary_change, '-an', self.path], run_on_host=True)
337422

338423
####################################
339424
#
@@ -343,7 +428,7 @@ def is_ceph_device(lv: "Volume") -> bool:
343428

344429
PV_FIELDS = 'pv_name,pv_tags,pv_uuid,vg_name,lv_uuid'
345430

346-
class PVolume:
431+
class PVolume(Lvm):
347432
"""
348433
Represents a Physical Volume from LVM, with some top-level attributes like
349434
``pv_name`` and parsed tags as a dictionary of key/value pairs.
@@ -352,18 +437,10 @@ class PVolume:
352437
def __init__(self, **kw: Any) -> None:
353438
self.pv_name: str = ''
354439
self.pv_uuid: str = ''
355-
self.lv_uuid: str = ''
356-
for k, v in kw.items():
357-
setattr(self, k, v)
440+
super().__init__('pv_name', 'pv_tags', **kw)
358441
self.pv_api = kw
359-
self.name = kw['pv_name']
360-
self.tags = parse_tags(kw['pv_tags'])
361-
362-
def __str__(self) -> str:
363-
return '<%s>' % self.pv_api['pv_name']
364-
365-
def __repr__(self) -> str:
366-
return self.__str__()
442+
self.binary_change: str = 'pvchange'
443+
self.path: str = self.pv_name
367444

368445
def set_tags(self, tags: Dict[str, Any]) -> None:
369446
"""
@@ -513,7 +590,7 @@ def get_single_pv(fields: str = PV_FIELDS, filters: Optional[Dict[str, Any]] = N
513590
VG_CMD_OPTIONS = ['--noheadings', '--readonly', '--units=b', '--nosuffix', '--separator=";"']
514591

515592

516-
class VolumeGroup(object):
593+
class VolumeGroup(Lvm):
517594
"""
518595
Represents an LVM group, with some top-level attributes like ``vg_name``
519596
"""
@@ -524,18 +601,8 @@ def __init__(self, **kw: Any) -> None:
524601
self.vg_free_count: str = ''
525602
self.vg_extent_size: str = ''
526603
self.vg_extent_count: str = ''
527-
for k, v in kw.items():
528-
setattr(self, k, v)
529-
self.name = kw['vg_name']
530-
if not self.name:
531-
raise ValueError('VolumeGroup must have a non-empty name')
532-
self.tags = parse_tags(kw.get('vg_tags', ''))
533-
534-
def __str__(self) -> str:
535-
return '<%s>' % self.name
536-
537-
def __repr__(self) -> str:
538-
return self.__str__()
604+
super().__init__('vg_name', 'vg_tags', **kw)
605+
self.vg_api = kw
539606

540607
@property
541608
def free(self) -> int:
@@ -825,7 +892,7 @@ def get_all_devices_vgs(name_prefix: str = '') -> List[VolumeGroup]:
825892
'--units=b', '--nosuffix']
826893

827894

828-
class Volume:
895+
class Volume(Lvm):
829896
"""
830897
Represents a Logical Volume from LVM, with some top-level attributes like
831898
``lv_name`` and parsed tags as a dictionary of key/value pairs.
@@ -837,22 +904,17 @@ def __init__(self, **kw: Any) -> None:
837904
self.lv_uuid: str = ''
838905
self.vg_name: str = ''
839906
self.lv_size: str = ''
907+
self.tags: Dict[str, Any] = {}
840908
self.lv_tags: Dict[str, Any] = {}
841-
for k, v in kw.items():
842-
setattr(self, k, v)
909+
super().__init__('lv_name', 'lv_tags', **kw)
843910
self.lv_api = kw
844-
self.name = kw['lv_name']
845-
if not self.name:
846-
raise ValueError('Volume must have a non-empty name')
847-
self.tags = parse_tags(kw['lv_tags'])
848911
self.encrypted = self.tags.get('ceph.encrypted', '0') == '1'
849912
self.used_by_ceph = 'ceph.osd_id' in self.tags
913+
self.binary_change: str = 'lvchange'
914+
self.path: str = self.lv_path
850915

851916
def __str__(self) -> str:
852-
return '<%s>' % self.lv_api['lv_path']
853-
854-
def __repr__(self) -> str:
855-
return self.__str__()
917+
return f'<{self.api_data.get("lv_path", self.name)}>'
856918

857919
def as_dict(self) -> Dict[Any, Any]:
858920
obj: Dict[Any, Any] = {}
@@ -884,79 +946,6 @@ def report(self) -> Dict[str, Any]:
884946
report[type_uuid] = self.tags['ceph.{}'.format(type_uuid)]
885947
return report
886948

887-
def _format_tag_args(self, op: str, tags: Dict[str, Any]) -> List[str]:
888-
tag_args = ['{}={}'.format(k, v) for k, v in tags.items()]
889-
# weird but efficient way of ziping two lists and getting a flat list
890-
return list(sum(zip(repeat(op), tag_args), ()))
891-
892-
def clear_tags(self, keys: Optional[List[str]] = None) -> None:
893-
"""
894-
Removes all or passed tags from the Logical Volume.
895-
"""
896-
if not keys:
897-
keys = list(self.tags.keys())
898-
899-
del_tags = {k: self.tags[k] for k in keys if k in self.tags}
900-
if not del_tags:
901-
# nothing to clear
902-
return
903-
del_tag_args = self._format_tag_args('--deltag', del_tags)
904-
# --deltag returns successful even if the to be deleted tag is not set
905-
process.call(['lvchange'] + del_tag_args + [self.lv_path], run_on_host=True)
906-
for k in del_tags.keys():
907-
del self.tags[k]
908-
909-
910-
def set_tags(self, tags: Dict[str, Any]) -> None:
911-
"""
912-
:param tags: A dictionary of tag names and values, like::
913-
914-
{
915-
"ceph.osd_fsid": "aaa-fff-bbbb",
916-
"ceph.osd_id": "0"
917-
}
918-
919-
At the end of all modifications, the tags are refreshed to reflect
920-
LVM's most current view.
921-
"""
922-
self.clear_tags(list(tags.keys()))
923-
add_tag_args = self._format_tag_args('--addtag', tags)
924-
process.call(['lvchange'] + add_tag_args + [self.lv_path], run_on_host=True)
925-
for k, v in tags.items():
926-
self.tags[k] = v
927-
928-
929-
def clear_tag(self, key: str) -> None:
930-
if self.tags.get(key):
931-
current_value = self.tags[key]
932-
tag = "%s=%s" % (key, current_value)
933-
process.call(['lvchange', '--deltag', tag, self.lv_path], run_on_host=True)
934-
del self.tags[key]
935-
936-
937-
def set_tag(self, key: str, value: str) -> None:
938-
"""
939-
Set the key/value pair as an LVM tag.
940-
"""
941-
# remove it first if it exists
942-
self.clear_tag(key)
943-
944-
process.call(
945-
[
946-
'lvchange',
947-
'--addtag', '%s=%s' % (key, value), self.lv_path
948-
],
949-
run_on_host=True
950-
)
951-
self.tags[key] = value
952-
953-
def deactivate(self) -> None:
954-
"""
955-
Deactivate the LV by calling lvchange -an
956-
"""
957-
process.call(['lvchange', '-an', self.lv_path], run_on_host=True)
958-
959-
960949
def create_lv(name_prefix: str,
961950
uuid: str,
962951
vg: Optional[VolumeGroup] = None,

src/ceph-volume/ceph_volume/tests/api/test_lvm.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,20 @@ def test_multiple_csv_expands_in_dict(self):
3131
assert result['ceph.fsid'] == '0000'
3232

3333

34-
class TestVolume(object):
35-
34+
class TestVolume:
3635
def test_is_ceph_device(self):
3736
lv_tags = "ceph.type=data,ceph.osd_id=0"
3837
osd = api.Volume(lv_name='osd/volume', lv_tags=lv_tags)
3938
assert api.is_ceph_device(osd)
4039

41-
@pytest.mark.parametrize('dev',[
42-
'/dev/sdb',
43-
api.VolumeGroup(vg_name='foo'),
44-
api.Volume(lv_name='vg/no_osd', lv_tags='', lv_path='lv/path'),
45-
api.Volume(lv_name='vg/no_osd', lv_tags='ceph.osd_id=null', lv_path='lv/path'),
46-
None,
47-
])
40+
@pytest.mark.parametrize('dev',
41+
[api.VolumeGroup(vg_name='foo'),
42+
api.Volume(lv_name='vg/no_osd',
43+
lv_tags='',
44+
lv_path='lv/path'),
45+
api.Volume(lv_name='vg/no_osd',
46+
lv_tags='ceph.osd_id=null',
47+
lv_path='lv/path')])
4848
def test_is_not_ceph_device(self, dev):
4949
assert not api.is_ceph_device(dev)
5050

src/ceph-volume/ceph_volume/tests/util/test_device.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def test_lvm_size_rounds_down(self, fake_call, device_info):
5050

5151
def test_is_lv(self, fake_call, device_info, monkeypatch):
5252
monkeypatch.setattr('ceph_volume.util.device.Device.is_lv', lambda: True)
53-
data = {"lv_path": "vg/lv", "vg_name": "vg", "name": "lv"}
53+
data = {"lv_path": "vg/lv", "vg_name": "vg", "name": "lv", "tags": {}}
5454
lsblk = {"TYPE": "lvm", "NAME": "vg-lv"}
5555
device_info(lv=data,lsblk=lsblk)
5656
disk = device.Device("vg/lv")
@@ -322,7 +322,7 @@ def test_reject_lv_symlink_to_device(self,
322322
fake_call):
323323
m_os_path_islink.return_value = True
324324
m_os_path_realpath.return_value = '/dev/mapper/vg-lv'
325-
lv = {"lv_path": "/dev/vg/lv", "vg_name": "vg", "name": "lv"}
325+
lv = {"lv_path": "/dev/vg/lv", "vg_name": "vg", "name": "lv", "tags": {}}
326326
lsblk = {"TYPE": "lvm", "NAME": "vg-lv"}
327327
device_info(lv=lv,lsblk=lsblk)
328328
disk = device.Device("/dev/vg/lv")

0 commit comments

Comments
 (0)