mirror of
https://github.com/ceph/ceph
synced 2026-08-02 07:03:18 +00:00
Merge c7744260f4 into eed82023c5
This commit is contained in:
commit
17fa0fe4b6
@ -5,6 +5,7 @@ import argparse
|
||||
from ceph_volume import terminal
|
||||
from ceph_volume.objectstore.lvm import Lvm as LVMActivate
|
||||
from ceph_volume.objectstore.raw import Raw as RAWActivate
|
||||
from ceph_volume.objectstore.raw import RawSeastore as RAWSeastoreActivate
|
||||
from ceph_volume.devices.simple.activate import Activate as SimpleActivate
|
||||
from ceph_volume.util.lvm_osd_mappers import OsdLvmMappers
|
||||
|
||||
@ -50,13 +51,21 @@ class Activate(object):
|
||||
if self.args.osd_id is not None and self.args.osd_fsid is not None:
|
||||
OsdLvmMappers(self.args.osd_id, self.args.osd_fsid).close()
|
||||
|
||||
# first try raw
|
||||
# first try raw bluestore
|
||||
try:
|
||||
raw_activate = RAWActivate(self.args)
|
||||
raw_activate.activate()
|
||||
return
|
||||
except Exception as e:
|
||||
terminal.info(f'Failed to activate via raw: {e}')
|
||||
terminal.info(f'Failed to activate via raw bluestore: {e}')
|
||||
|
||||
# then try raw seastore
|
||||
try:
|
||||
raw_seastore_activate = RAWSeastoreActivate(self.args)
|
||||
raw_seastore_activate.activate()
|
||||
return
|
||||
except Exception as e:
|
||||
terminal.info(f'Failed to activate via raw seastore: {e}')
|
||||
|
||||
# then try lvm
|
||||
try:
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
from __future__ import print_function
|
||||
from typing import Any, Dict, Optional, List as _List
|
||||
from typing import Any, Dict, Optional, Set, List as _List
|
||||
import os
|
||||
import argparse
|
||||
import json
|
||||
@ -48,9 +48,48 @@ def _get_bluestore_info(devices: _List[str]) -> Dict[str, Any]:
|
||||
return result
|
||||
|
||||
|
||||
def bluestore_device_realpaths(report: Dict[str, Any]) -> Set[str]:
|
||||
"""Real paths of any block device paths referenced by a BlueStore raw report."""
|
||||
paths: Set[str] = set()
|
||||
for _uuid, details in report.items():
|
||||
if not isinstance(details, dict):
|
||||
continue
|
||||
for key in ('device', 'device_db', 'device_wal'):
|
||||
dev = details.get(key)
|
||||
if not dev:
|
||||
continue
|
||||
try:
|
||||
paths.add(os.path.realpath(dev))
|
||||
except OSError:
|
||||
paths.add(dev)
|
||||
return paths
|
||||
|
||||
|
||||
def get_seastore_info(
|
||||
devices: _List[str],
|
||||
bluestore_realpaths: Set[str],
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
SeaStore raw OSDs: on-disk SeaStore signature on each device from ``lsblk`` (no host tree).
|
||||
"""
|
||||
result: Dict[str, Any] = {}
|
||||
for device in devices:
|
||||
try:
|
||||
if os.path.realpath(device) in bluestore_realpaths:
|
||||
continue
|
||||
except OSError:
|
||||
pass
|
||||
row = disk.seastore_raw_device_report(device)
|
||||
if not row:
|
||||
continue
|
||||
osd_uuid = row['osd_uuid']
|
||||
result[osd_uuid] = row
|
||||
return result
|
||||
|
||||
|
||||
class List(object):
|
||||
|
||||
help = 'list BlueStore OSDs on raw devices'
|
||||
help = 'list BlueStore and SeaStore OSDs on raw devices'
|
||||
|
||||
def __init__(self, argv: _List[str]) -> None:
|
||||
self.argv = argv
|
||||
@ -76,6 +115,10 @@ class List(object):
|
||||
logger.warning(('ignoring child device {} whose parent {} is a BlueStore OSD.'.format(path, parent_device),
|
||||
'device is likely a phantom Atari partition. device info: {}'.format(info_device)))
|
||||
continue
|
||||
if disk.has_seastore_label(parent_device):
|
||||
logger.warning(('ignoring child device {} whose parent {} is a SeaStore OSD.'.format(path, parent_device),
|
||||
'device is likely a phantom Atari partition. device info: {}'.format(info_device)))
|
||||
continue
|
||||
except OSError as e:
|
||||
logger.error(('ignoring child device {} to avoid reporting invalid BlueStore data from phantom Atari partitions.'.format(path),
|
||||
'failed to determine if parent device {} is BlueStore. err: {}'.format(parent_device, e)))
|
||||
@ -143,13 +186,14 @@ class List(object):
|
||||
# the child so as not to give a false negative.
|
||||
self.exclude_atari_partitions()
|
||||
self.exclude_lvm_osd_devices()
|
||||
|
||||
else:
|
||||
self.devices_to_scan = devices
|
||||
self.devices_to_scan = list(devices)
|
||||
|
||||
result: Dict[str, Any] = {}
|
||||
logger.debug('inspecting devices: {}'.format(self.devices_to_scan))
|
||||
result = _get_bluestore_info(self.devices_to_scan)
|
||||
bluestore_report = _get_bluestore_info(self.devices_to_scan)
|
||||
bluestore_paths = bluestore_device_realpaths(bluestore_report)
|
||||
seastore_report = get_seastore_info(self.devices_to_scan, bluestore_paths)
|
||||
result: Dict[str, Any] = {**bluestore_report, **seastore_report}
|
||||
|
||||
return result
|
||||
|
||||
@ -168,7 +212,7 @@ class List(object):
|
||||
List OSDs on raw devices with raw device labels (usually the first
|
||||
block of the device).
|
||||
|
||||
Full listing of all identifiable (currently, BlueStore) OSDs
|
||||
Full listing of all identifiable BlueStore and SeaStore OSDs
|
||||
on raw devices:
|
||||
|
||||
ceph-volume raw list
|
||||
|
||||
@ -14,6 +14,7 @@ mapping: Dict[str, Any] = {
|
||||
ObjectStore.seastore: lvm.Lvm
|
||||
},
|
||||
'RAW': {
|
||||
ObjectStore.bluestore: raw.Raw
|
||||
ObjectStore.bluestore: raw.Raw,
|
||||
ObjectStore.seastore: raw.RawSeastore,
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,17 +93,18 @@ class BaseObjectStore:
|
||||
in as a keyword argument it is used as part of the ceph-osd command
|
||||
"""
|
||||
|
||||
if self.wal_device_path:
|
||||
self.osd_mkfs_cmd.extend(
|
||||
['--bluestore-block-wal-path', self.wal_device_path]
|
||||
)
|
||||
system.chown(self.wal_device_path)
|
||||
if self.objectstore == 'bluestore':
|
||||
if self.wal_device_path:
|
||||
self.osd_mkfs_cmd.extend(
|
||||
['--bluestore-block-wal-path', self.wal_device_path]
|
||||
)
|
||||
system.chown(self.wal_device_path)
|
||||
|
||||
if self.db_device_path:
|
||||
self.osd_mkfs_cmd.extend(
|
||||
['--bluestore-block-db-path', self.db_device_path]
|
||||
)
|
||||
system.chown(self.db_device_path)
|
||||
if self.db_device_path:
|
||||
self.osd_mkfs_cmd.extend(
|
||||
['--bluestore-block-db-path', self.db_device_path]
|
||||
)
|
||||
system.chown(self.db_device_path)
|
||||
|
||||
if self.get_osdspec_affinity():
|
||||
self.osd_mkfs_cmd.extend(['--osdspec-affinity',
|
||||
@ -145,7 +146,7 @@ class BaseObjectStore:
|
||||
return '/var/lib/ceph/osd/%s-%s/' % (conf.cluster, self.osd_id)
|
||||
|
||||
def get_default_entrypoint_cmd(self) -> str:
|
||||
if self.osd_type == "crimson":
|
||||
if self.osd_type == "crimson" or self.objectstore == "seastore":
|
||||
return "ceph-osd-crimson"
|
||||
return "ceph-osd"
|
||||
|
||||
|
||||
@ -223,6 +223,8 @@ class Raw(BaseObjectStore):
|
||||
filter_osd_fsid = self.osd_fsid
|
||||
|
||||
for osd_uuid, meta in found.items():
|
||||
if meta.get('type') == 'seastore':
|
||||
continue
|
||||
realpath_device = os.path.realpath(meta['device'])
|
||||
if lvm_api.is_ceph_volume_lvm_prepared(realpath_device,
|
||||
lvm_prepare_lv_paths):
|
||||
@ -282,3 +284,165 @@ class Raw(BaseObjectStore):
|
||||
# An option could be to simply rename the mapper but the uuid remains unchanged in sysfs
|
||||
encryption_utils.luks_close(self.temp_mapper)
|
||||
encryption_utils.luks_open('', device, new_mapper, self.with_tpm)
|
||||
|
||||
|
||||
class RawSeastore(Raw):
|
||||
|
||||
def prepare(self) -> None:
|
||||
block_db = getattr(self.args, 'block_db', None) or ''
|
||||
block_wal = getattr(self.args, 'block_wal', None) or ''
|
||||
if block_db or block_wal:
|
||||
raise RuntimeError(
|
||||
'SeaStore raw OSDs do not support --block.db or --block.wal in ceph-volume.'
|
||||
)
|
||||
super().prepare()
|
||||
|
||||
def pre_activate_tpm2(self, device: str) -> None:
|
||||
"""Pre-activate a TPM2-encrypted SeaStore device.
|
||||
|
||||
SeaStore raw OSDs have a single data device whose role is always
|
||||
'block', so we can build the canonical mapper name directly without
|
||||
reading a BlueStore header.
|
||||
"""
|
||||
self.with_tpm = 1
|
||||
temp_mapper: str = f'activating-{os.path.basename(device)}'
|
||||
temp_mapper_path: str = f'/dev/mapper/{temp_mapper}'
|
||||
if not disk.BlockSysFs(device).has_active_dmcrypt_mapper:
|
||||
encryption_utils.luks_open('', device, temp_mapper, self.with_tpm)
|
||||
kname: str = disk.get_parent_device_from_mapper(temp_mapper_path,
|
||||
abspath=False)
|
||||
new_mapper: str = f'ceph-{self.osd_fsid}-{kname}-block-dmcrypt'
|
||||
self.block_device_path = f'/dev/mapper/{new_mapper}'
|
||||
self.devices.append(self.block_device_path)
|
||||
encryption_utils.luks_close(temp_mapper)
|
||||
encryption_utils.luks_open('', device, new_mapper, self.with_tpm)
|
||||
|
||||
def _activate(self) -> None:
|
||||
mappers: Optional[RawOsdCryptMappers] = None
|
||||
if RawOsdCryptMappers.backing_device_path(self.block_device_path):
|
||||
mappers = RawOsdCryptMappers(
|
||||
self.osd_id,
|
||||
self.osd_fsid,
|
||||
self.block_device_path,
|
||||
cluster_name=conf.cluster,
|
||||
dmcrypt_secret=os.getenv('CEPH_VOLUME_DMCRYPT_SECRET') or None,
|
||||
with_tpm=bool(self.with_tpm),
|
||||
)
|
||||
if mappers is not None and mappers.applies():
|
||||
try:
|
||||
mappers.refresh()
|
||||
except RuntimeError as e:
|
||||
mlogger.info(
|
||||
'Failed to refresh dmcrypt mappers for osd.%s uuid %s: %s'
|
||||
' (is the OSD already running?)',
|
||||
self.osd_id,
|
||||
self.osd_fsid,
|
||||
e,
|
||||
)
|
||||
(self.block_device_path, _, _) = mappers.mapper_paths()
|
||||
|
||||
self.osd_path = '/var/lib/ceph/osd/%s-%s' % (conf.cluster, self.osd_id)
|
||||
if not system.path_is_mounted(self.osd_path):
|
||||
prepare_utils.create_osd_path(self.osd_id, tmpfs=not self.args.no_tmpfs)
|
||||
|
||||
self.unlink_bs_symlinks()
|
||||
system.chown(self.osd_path)
|
||||
prepare_utils.link_block(self.block_device_path, self.osd_id)
|
||||
system.chown(self.osd_path)
|
||||
terminal.success("ceph-volume raw activate "
|
||||
"successful for osd ID: %s" % self.osd_id)
|
||||
|
||||
def _find_seastore_candidates(self, devices: List[str]) -> List[str]:
|
||||
"""Return block devices from *devices* that carry a SeaStore signature."""
|
||||
candidates = []
|
||||
for dev in devices:
|
||||
if not dev:
|
||||
continue
|
||||
try:
|
||||
if disk.has_seastore_label(dev):
|
||||
candidates.append(dev)
|
||||
except OSError as exc:
|
||||
logger.warning('could not read SeaStore label from %s: %s', dev, exc)
|
||||
return candidates
|
||||
|
||||
@decorators.needs_root
|
||||
def activate(self) -> None:
|
||||
if not (self.devices or self.osd_id or self.osd_fsid):
|
||||
raise RuntimeError(
|
||||
'SeaStore activation requires at least --osd-id, --osd-uuid, '
|
||||
'or an explicit device.'
|
||||
)
|
||||
if not self.osd_id or not self.osd_fsid:
|
||||
raise RuntimeError(
|
||||
'SeaStore activation requires both --osd-id and --osd-uuid.'
|
||||
)
|
||||
|
||||
osd_id = self.osd_id
|
||||
osd_fsid = self.osd_fsid
|
||||
|
||||
# Pre activation for encrypted devices: open any matching Ceph LUKS
|
||||
# device before scanning for the SeaStore signature, because a closed
|
||||
# LUKS mapper hides the on-disk magic entirely.
|
||||
# lsblk_all() is called once here and reused for the candidate scan
|
||||
# when no explicit devices are given, avoiding a second lsblk call.
|
||||
# LVM prepared devices are excluded from both scans so that LVM backed
|
||||
# OSDs are not claimed by the raw path and can fall through to LVMActivate.
|
||||
lvm_prepare_lv_paths = lvm_api.ceph_volume_lvm_prepare_lv_paths()
|
||||
all_devices = disk.lsblk_all(abspath=True)
|
||||
for d in all_devices:
|
||||
device: str = d.get('NAME', '')
|
||||
if lvm_api.is_ceph_volume_lvm_prepared(device, lvm_prepare_lv_paths):
|
||||
continue
|
||||
luks2 = encryption_utils.CephLuks2(device)
|
||||
if not luks2.is_ceph_encrypted or luks2.osd_fsid != osd_fsid:
|
||||
continue
|
||||
if luks2.is_tpm2_enrolled:
|
||||
self.pre_activate_tpm2(device)
|
||||
else:
|
||||
# Key-based dmcrypt: open the mapper so the seastore magic
|
||||
# is readable by _find_seastore_candidates().
|
||||
kname = os.path.basename(os.path.realpath(device))
|
||||
mapper = f'ceph-{osd_fsid}-{kname}-block-dmcrypt'
|
||||
if not disk.BlockSysFs(device).has_active_dmcrypt_mapper:
|
||||
encryption_utils.luks_open(
|
||||
os.getenv('CEPH_VOLUME_DMCRYPT_SECRET', ''),
|
||||
device,
|
||||
mapper,
|
||||
0,
|
||||
)
|
||||
self.block_device_path = f'/dev/mapper/{mapper}'
|
||||
self.devices = [self.block_device_path]
|
||||
|
||||
# Build the candidate device list: either the explicitly provided
|
||||
# devices, or all block devices discovered above (cephadm path).
|
||||
# LVM prepared devices are filtered out so an LVM OSD carrying the
|
||||
# Crimson signature is not claimed here.
|
||||
# NOTE: SeaStore does not expose the OSD fsid in a format readable
|
||||
# without a DENC parser, so we cannot filter by osd_fsid here.
|
||||
# If multiple SeaStore OSDs are present on the host, an explicit
|
||||
# --device must be passed.
|
||||
if self.devices:
|
||||
scan_devices = list(self.devices)
|
||||
else:
|
||||
scan_devices = [
|
||||
d.get('NAME', '') for d in all_devices
|
||||
if not lvm_api.is_ceph_volume_lvm_prepared(
|
||||
d.get('NAME', ''), lvm_prepare_lv_paths)
|
||||
]
|
||||
|
||||
candidates = self._find_seastore_candidates(scan_devices)
|
||||
|
||||
if not candidates:
|
||||
raise RuntimeError('did not find any SeaStore device to activate')
|
||||
if len(candidates) > 1:
|
||||
raise RuntimeError(
|
||||
'multiple SeaStore devices found; cannot determine which '
|
||||
'belongs to osd.%s — pass an explicit --device.' % osd_id
|
||||
)
|
||||
|
||||
self.block_device_path = candidates[0]
|
||||
self.db_device_path = ''
|
||||
self.wal_device_path = ''
|
||||
self.osd_id = str(osd_id)
|
||||
self.osd_fsid = str(osd_fsid)
|
||||
self._activate()
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from ceph_volume.devices.raw import list as raw_list
|
||||
from ceph_volume.util import disk
|
||||
|
||||
|
||||
def test_bluestore_device_realpaths_collects_known_keys(monkeypatch):
|
||||
monkeypatch.setattr(os.path, 'realpath', lambda p: p)
|
||||
report = {
|
||||
'u1': {
|
||||
'device': '/dev/sda',
|
||||
'device_db': '/dev/sdb',
|
||||
'type': 'bluestore',
|
||||
}
|
||||
}
|
||||
paths = raw_list.bluestore_device_realpaths(report)
|
||||
assert paths == {'/dev/sda', '/dev/sdb'}
|
||||
|
||||
|
||||
def test_get_seastore_info_skips_bluestore_paths(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
raw_list.disk,
|
||||
'seastore_raw_device_report',
|
||||
lambda dev: {
|
||||
'type': 'seastore',
|
||||
'device': dev,
|
||||
'osd_uuid': '11111111-1111-1111-1111-111111111111',
|
||||
'synthetic_osd_uuid': True,
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(os.path, 'realpath', lambda p: '/same')
|
||||
bluestore_paths = {'/same'}
|
||||
out = raw_list.get_seastore_info(['/dev/vdc'], bluestore_paths)
|
||||
assert out == {}
|
||||
|
||||
|
||||
def test_seastore_raw_device_report_synthetic_uuid(monkeypatch):
|
||||
dev = '/dev/vdz'
|
||||
monkeypatch.setattr(disk, 'has_seastore_label', lambda _d: True)
|
||||
monkeypatch.setattr(os.path, 'realpath', lambda p: p)
|
||||
expected = str(
|
||||
uuid.uuid5(uuid.NAMESPACE_URL, 'ceph-volume:seastore-raw:' + dev)
|
||||
)
|
||||
row = disk.seastore_raw_device_report(dev)
|
||||
assert row is not None
|
||||
assert row['type'] == 'seastore'
|
||||
assert row['device'] == dev
|
||||
assert row['osd_uuid'] == expected
|
||||
assert row['synthetic_osd_uuid'] is True
|
||||
assert disk.seastore_raw_device_report(dev)['osd_uuid'] == expected
|
||||
|
||||
|
||||
def test_get_seastore_info_merges_when_not_bluestore(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
raw_list.disk,
|
||||
'seastore_raw_device_report',
|
||||
lambda dev: {
|
||||
'type': 'seastore',
|
||||
'device': dev,
|
||||
'osd_uuid': '22222222-2222-2222-2222-222222222222',
|
||||
'synthetic_osd_uuid': True,
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(os.path, 'realpath', lambda p: p)
|
||||
out = raw_list.get_seastore_info(['/dev/vdd'], set())
|
||||
u = '22222222-2222-2222-2222-222222222222'
|
||||
assert u in out
|
||||
assert out[u]['type'] == 'seastore'
|
||||
assert out[u]['synthetic_osd_uuid'] is True
|
||||
@ -312,6 +312,7 @@ class TestBaseObjectStore:
|
||||
@patch('ceph_volume.objectstore.baseobjectstore.prepare_utils.create_key', Mock(return_value=['AQCee6ZkzhOrJRAAZWSvNC3KdXOpC2w8ly4AZQ==']))
|
||||
def setup_method(self, m_create_key):
|
||||
self.b = BaseObjectStore([])
|
||||
self.b.objectstore = 'bluestore'
|
||||
self.b.osd_mkfs_cmd = ['binary', 'arg1']
|
||||
|
||||
def test_add_objectstore_opts_wal_device_path(self, monkeypatch):
|
||||
@ -326,6 +327,14 @@ class TestBaseObjectStore:
|
||||
self.b.add_objectstore_opts()
|
||||
assert self.b.osd_mkfs_cmd == ['binary', 'arg1', '--bluestore-block-db-path', '/dev/ssd1']
|
||||
|
||||
def test_add_objectstore_opts_skips_bluestore_paths_for_seastore(self, monkeypatch):
|
||||
monkeypatch.setattr('ceph_volume.util.system.chown', lambda path: 0)
|
||||
self.b.objectstore = 'seastore'
|
||||
self.b.wal_device_path = '/dev/nvme0n1'
|
||||
self.b.db_device_path = '/dev/ssd1'
|
||||
self.b.add_objectstore_opts()
|
||||
assert self.b.osd_mkfs_cmd == ['binary', 'arg1']
|
||||
|
||||
def test_add_objectstore_opts_osdspec_affinity(self, monkeypatch):
|
||||
monkeypatch.setattr('ceph_volume.util.system.chown', lambda path: 0)
|
||||
self.b.get_osdspec_affinity = lambda: 'foo'
|
||||
|
||||
@ -240,6 +240,54 @@ class TestRaw:
|
||||
'type': 'bluestore'},
|
||||
tmpfs=True)]
|
||||
|
||||
@patch('ceph_volume.objectstore.raw.disk.lsblk_all', return_value=[])
|
||||
@patch('ceph_volume.objectstore.raw.lvm_api.ceph_volume_lvm_prepare_lv_paths',
|
||||
return_value=[])
|
||||
@patch('ceph_volume.objectstore.raw.lvm_api.is_ceph_volume_lvm_prepared',
|
||||
return_value=False)
|
||||
@patch('ceph_volume.objectstore.raw.encryption_utils.CephLuks2',
|
||||
return_value=MagicMock(is_ceph_encrypted=False))
|
||||
def test_activate_skips_seastore_rows(self,
|
||||
m_luks2,
|
||||
m_is_lvm,
|
||||
m_lvm_paths,
|
||||
m_lsblk,
|
||||
is_root,
|
||||
monkeypatch):
|
||||
"""Raw (BlueStore) activate must ignore seastore rows in direct_report.
|
||||
|
||||
Before the fix, iterating a mixed report would raise KeyError on
|
||||
meta['osd_id'] for any seastore entry. After the fix the bluestore
|
||||
OSD is activated and the seastore row is silently skipped.
|
||||
"""
|
||||
mixed_report = {
|
||||
# bluestore OSD
|
||||
'824f7edf-371f-4b75-9231-4ab62a32d5c0': {
|
||||
'ceph_fsid': '7dccab18-14cf-11ee-837b-5254008f8ca5',
|
||||
'device': '/dev/sda',
|
||||
'osd_id': 8,
|
||||
'osd_uuid': '824f7edf-371f-4b75-9231-4ab62a32d5c0',
|
||||
'type': 'bluestore',
|
||||
},
|
||||
# seastore OSD — no osd_id / ceph_fsid keys
|
||||
'aaaaaaaa-0000-0000-0000-000000000001': {
|
||||
'type': 'seastore',
|
||||
'device': '/dev/sdb',
|
||||
'osd_uuid': 'aaaaaaaa-0000-0000-0000-000000000001',
|
||||
'synthetic_osd_uuid': True,
|
||||
},
|
||||
}
|
||||
monkeypatch.setattr('ceph_volume.objectstore.raw.direct_report',
|
||||
lambda x: mixed_report)
|
||||
self.raw_bs._activate = MagicMock()
|
||||
# devices non-empty so the assert in activate() passes; no osd filter.
|
||||
self.raw_bs.devices = ['/dev/sda']
|
||||
self.raw_bs.osd_id = None
|
||||
self.raw_bs.osd_fsid = None
|
||||
self.raw_bs.activate()
|
||||
# _activate called exactly once (for the bluestore OSD only)
|
||||
assert self.raw_bs._activate.call_count == 1
|
||||
|
||||
@patch('ceph_volume.objectstore.raw.encryption_utils.rename_mapper', Mock(return_value=MagicMock()))
|
||||
@patch('ceph_volume.util.disk.get_bluestore_header')
|
||||
@patch('ceph_volume.objectstore.raw.encryption_utils.luks_close', Mock(return_value=MagicMock()))
|
||||
|
||||
@ -0,0 +1,388 @@
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from ceph_volume.objectstore import mapping
|
||||
from ceph_volume.objectstore.raw import RawSeastore
|
||||
|
||||
|
||||
class TestRawSeastoreMapping:
|
||||
def test_raw_mapping_includes_seastore(self):
|
||||
assert mapping['RAW']['seastore'] is RawSeastore
|
||||
|
||||
|
||||
class TestRawSeastorePrepare:
|
||||
@patch('ceph_volume.objectstore.raw.prepare_utils.create_key',
|
||||
MagicMock(return_value=['AQCee6ZkzhOrJRAAZWSvNC3KdXOpC2w8ly4AZQ==']))
|
||||
def test_prepare_rejects_block_db(self, factory, is_root):
|
||||
args = factory(
|
||||
objectstore='seastore',
|
||||
data='/dev/foo',
|
||||
block_db='/dev/db',
|
||||
block_wal='',
|
||||
no_tmpfs=False,
|
||||
dmcrypt=False,
|
||||
with_tpm=False,
|
||||
osd_id=None,
|
||||
osd_type='crimson',
|
||||
)
|
||||
rs = RawSeastore(args=args)
|
||||
with pytest.raises(RuntimeError, match='SeaStore raw OSDs do not support'):
|
||||
rs.prepare()
|
||||
|
||||
@patch('ceph_volume.objectstore.raw.prepare_utils.create_key',
|
||||
MagicMock(return_value=['AQCee6ZkzhOrJRAAZWSvNC3KdXOpC2w8ly4AZQ==']))
|
||||
@patch('ceph_volume.objectstore.raw.Raw.prepare')
|
||||
def test_prepare_passes_without_db_wal(self, m_raw_prepare, factory, is_root):
|
||||
args = factory(
|
||||
objectstore='seastore',
|
||||
data='/dev/foo',
|
||||
block_db='',
|
||||
block_wal='',
|
||||
no_tmpfs=False,
|
||||
dmcrypt=False,
|
||||
with_tpm=False,
|
||||
osd_id=None,
|
||||
osd_type='crimson',
|
||||
)
|
||||
rs = RawSeastore(args=args)
|
||||
rs.prepare()
|
||||
m_raw_prepare.assert_called_once()
|
||||
|
||||
|
||||
class TestRawSeastoreMkfs:
|
||||
@patch('ceph_volume.objectstore.raw.prepare_utils.create_key',
|
||||
MagicMock(return_value=['AQCee6ZkzhOrJRAAZWSvNC3KdXOpC2w8ly4AZQ==']))
|
||||
def test_mkfs_entrypoint_is_crimson(self, factory):
|
||||
args = factory(
|
||||
objectstore='seastore',
|
||||
data='/dev/foo',
|
||||
block_db='',
|
||||
block_wal='',
|
||||
no_tmpfs=False,
|
||||
dmcrypt=False,
|
||||
with_tpm=False,
|
||||
osd_id=None,
|
||||
osd_type='crimson',
|
||||
)
|
||||
rs = RawSeastore(args=args)
|
||||
rs.objectstore = 'seastore'
|
||||
assert rs.get_default_entrypoint_cmd() == 'ceph-osd-crimson'
|
||||
|
||||
|
||||
class TestRawSeastorePreActivateTpm2:
|
||||
@patch('ceph_volume.objectstore.raw.prepare_utils.create_key',
|
||||
MagicMock(return_value=['AQCee6ZkzhOrJRAAZWSvNC3KdXOpC2w8ly4AZQ==']))
|
||||
@patch('ceph_volume.objectstore.raw.encryption_utils.luks_open',
|
||||
MagicMock())
|
||||
@patch('ceph_volume.objectstore.raw.encryption_utils.luks_close',
|
||||
MagicMock())
|
||||
@patch('ceph_volume.objectstore.raw.disk.get_parent_device_from_mapper',
|
||||
return_value='sda')
|
||||
@patch('ceph_volume.objectstore.raw.disk.BlockSysFs')
|
||||
def test_pre_activate_tpm2_builds_block_mapper(self,
|
||||
m_blocksysfs,
|
||||
m_get_parent,
|
||||
factory):
|
||||
m_blocksysfs.return_value.has_active_dmcrypt_mapper = False
|
||||
args = factory(objectstore='seastore', no_tmpfs=False)
|
||||
rs = RawSeastore(args=args)
|
||||
rs.osd_fsid = 'aaaa-bbbb'
|
||||
rs.devices = []
|
||||
rs.pre_activate_tpm2('/dev/sda')
|
||||
assert rs.block_device_path == '/dev/mapper/ceph-aaaa-bbbb-sda-block-dmcrypt'
|
||||
assert '/dev/mapper/ceph-aaaa-bbbb-sda-block-dmcrypt' in rs.devices
|
||||
|
||||
@patch('ceph_volume.objectstore.raw.prepare_utils.create_key',
|
||||
MagicMock(return_value=['AQCee6ZkzhOrJRAAZWSvNC3KdXOpC2w8ly4AZQ==']))
|
||||
@patch('ceph_volume.objectstore.raw.disk.BlockSysFs')
|
||||
def test_pre_activate_tpm2_skips_when_mapper_already_active(self,
|
||||
m_blocksysfs,
|
||||
factory):
|
||||
m_blocksysfs.return_value.has_active_dmcrypt_mapper = True
|
||||
args = factory(objectstore='seastore', no_tmpfs=False)
|
||||
rs = RawSeastore(args=args)
|
||||
rs.osd_fsid = 'aaaa-bbbb'
|
||||
rs.devices = []
|
||||
rs.pre_activate_tpm2('/dev/sda')
|
||||
# no mapper was opened, block_device_path untouched
|
||||
assert rs.block_device_path == ''
|
||||
assert rs.devices == []
|
||||
|
||||
|
||||
class TestRawSeastoreActivateDmcrypt:
|
||||
@patch('ceph_volume.objectstore.raw.prepare_utils.create_key',
|
||||
MagicMock(return_value=['AQCee6ZkzhOrJRAAZWSvNC3KdXOpC2w8ly4AZQ==']))
|
||||
@patch('ceph_volume.objectstore.raw.RawOsdCryptMappers.backing_device_path',
|
||||
return_value='/dev/sda')
|
||||
@patch('ceph_volume.objectstore.raw.RawOsdCryptMappers')
|
||||
@patch('ceph_volume.objectstore.raw.prepare_utils.link_block', MagicMock())
|
||||
@patch('ceph_volume.objectstore.raw.prepare_utils.create_osd_path', MagicMock())
|
||||
@patch('ceph_volume.objectstore.raw.system.chown', MagicMock())
|
||||
@patch('ceph_volume.objectstore.raw.system.path_is_mounted', return_value=False)
|
||||
@patch('ceph_volume.conf.cluster', 'ceph')
|
||||
def test_activate_with_dmcrypt_opens_mapper(self,
|
||||
m_mounted,
|
||||
m_mappers_cls,
|
||||
m_backing,
|
||||
factory):
|
||||
mapper_instance = MagicMock()
|
||||
mapper_instance.applies.return_value = True
|
||||
mapper_instance.mapper_paths.return_value = (
|
||||
'/dev/mapper/ceph-fsid-sda-block-dmcrypt', '', '',
|
||||
)
|
||||
m_mappers_cls.return_value = mapper_instance
|
||||
|
||||
args = factory(objectstore='seastore', no_tmpfs=False)
|
||||
rs = RawSeastore(args=args)
|
||||
rs.block_device_path = '/dev/mapper/ceph-fsid-sda-block-dmcrypt'
|
||||
rs.osd_id = '1'
|
||||
rs.osd_fsid = 'test-fsid'
|
||||
rs._activate()
|
||||
|
||||
m_mappers_cls.assert_called_once_with(
|
||||
'1', 'test-fsid',
|
||||
'/dev/mapper/ceph-fsid-sda-block-dmcrypt',
|
||||
cluster_name='ceph',
|
||||
dmcrypt_secret=None,
|
||||
with_tpm=False,
|
||||
)
|
||||
mapper_instance.refresh.assert_called_once()
|
||||
assert rs.block_device_path == '/dev/mapper/ceph-fsid-sda-block-dmcrypt'
|
||||
|
||||
@patch('ceph_volume.objectstore.raw.prepare_utils.create_key',
|
||||
MagicMock(return_value=['AQCee6ZkzhOrJRAAZWSvNC3KdXOpC2w8ly4AZQ==']))
|
||||
@patch('ceph_volume.objectstore.raw.RawOsdCryptMappers.backing_device_path',
|
||||
return_value='')
|
||||
@patch('ceph_volume.objectstore.raw.RawOsdCryptMappers')
|
||||
@patch('ceph_volume.objectstore.raw.prepare_utils.link_block', MagicMock())
|
||||
@patch('ceph_volume.objectstore.raw.prepare_utils.create_osd_path', MagicMock())
|
||||
@patch('ceph_volume.objectstore.raw.system.chown', MagicMock())
|
||||
@patch('ceph_volume.objectstore.raw.system.path_is_mounted', return_value=False)
|
||||
@patch('ceph_volume.conf.cluster', 'ceph')
|
||||
def test_activate_clear_skips_mappers(self,
|
||||
m_mounted,
|
||||
m_mappers_cls,
|
||||
m_backing,
|
||||
factory):
|
||||
# backing_device_path returns '' → no RawOsdCryptMappers instantiated
|
||||
args = factory(objectstore='seastore', no_tmpfs=False)
|
||||
rs = RawSeastore(args=args)
|
||||
rs.block_device_path = '/dev/sda'
|
||||
rs.osd_id = '1'
|
||||
rs.osd_fsid = 'test-fsid'
|
||||
rs._activate()
|
||||
m_mappers_cls.assert_not_called()
|
||||
|
||||
|
||||
class TestRawSeastoreActivate:
|
||||
@patch('ceph_volume.objectstore.raw.prepare_utils.create_key')
|
||||
@patch('ceph_volume.objectstore.raw.RawSeastore._activate')
|
||||
@patch('ceph_volume.objectstore.raw.disk.has_seastore_label', return_value=True)
|
||||
@patch('ceph_volume.objectstore.raw.disk.lsblk_all', return_value=[])
|
||||
@patch('ceph_volume.objectstore.raw.lvm_api.is_ceph_volume_lvm_prepared',
|
||||
return_value=False)
|
||||
@patch('ceph_volume.objectstore.raw.lvm_api.ceph_volume_lvm_prepare_lv_paths',
|
||||
return_value=[])
|
||||
@patch('ceph_volume.objectstore.raw.encryption_utils.CephLuks2',
|
||||
return_value=MagicMock(is_ceph_encrypted=False))
|
||||
def test_explicit_activate(self, m_luks2, m_lvm_paths, m_is_lvm,
|
||||
m_lsblk, m_label, m_activate,
|
||||
m_create_key, factory, is_root):
|
||||
m_create_key.return_value = ['AQCee6ZkzhOrJRAAZWSvNC3KdXOpC2w8ly4AZQ==']
|
||||
args = factory(
|
||||
objectstore='seastore',
|
||||
osd_id='7',
|
||||
osd_fsid='aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
|
||||
devices=['/dev/sdz'],
|
||||
no_tmpfs=False,
|
||||
)
|
||||
rs = RawSeastore(args=args)
|
||||
rs.devices = list(args.devices)
|
||||
rs.osd_id = args.osd_id
|
||||
rs.osd_fsid = args.osd_fsid
|
||||
rs.activate()
|
||||
assert rs.block_device_path == '/dev/sdz'
|
||||
m_label.assert_called()
|
||||
m_activate.assert_called_once_with()
|
||||
|
||||
@patch('ceph_volume.objectstore.raw.prepare_utils.create_key')
|
||||
def test_activate_requires_both_osd_id_and_fsid(self, m_create_key,
|
||||
factory, is_root):
|
||||
m_create_key.return_value = ['AQCee6ZkzhOrJRAAZWSvNC3KdXOpC2w8ly4AZQ==']
|
||||
# osd_id provided but no osd_fsid
|
||||
args = factory(objectstore='seastore', osd_id='1', osd_fsid=None,
|
||||
devices=[], no_tmpfs=False)
|
||||
rs = RawSeastore(args=args)
|
||||
rs.devices = []
|
||||
rs.osd_id = '1'
|
||||
rs.osd_fsid = ''
|
||||
with pytest.raises(RuntimeError, match='requires both --osd-id and --osd-uuid'):
|
||||
rs.activate()
|
||||
|
||||
@patch('ceph_volume.objectstore.raw.prepare_utils.create_key')
|
||||
def test_activate_requires_at_least_one_arg(self, m_create_key,
|
||||
factory, is_root):
|
||||
m_create_key.return_value = ['AQCee6ZkzhOrJRAAZWSvNC3KdXOpC2w8ly4AZQ==']
|
||||
args = factory(
|
||||
objectstore='seastore',
|
||||
osd_id=None,
|
||||
osd_fsid=None,
|
||||
devices=[],
|
||||
no_tmpfs=False,
|
||||
)
|
||||
rs = RawSeastore(args=args)
|
||||
rs.devices = []
|
||||
rs.osd_id = ''
|
||||
rs.osd_fsid = ''
|
||||
with pytest.raises(RuntimeError, match='requires at least'):
|
||||
rs.activate()
|
||||
|
||||
@patch('ceph_volume.objectstore.raw.prepare_utils.create_key')
|
||||
@patch('ceph_volume.objectstore.raw.RawSeastore._activate')
|
||||
@patch('ceph_volume.objectstore.raw.disk.lsblk_all',
|
||||
return_value=[{'NAME': '/dev/sda'}])
|
||||
@patch('ceph_volume.objectstore.raw.lvm_api.is_ceph_volume_lvm_prepared',
|
||||
return_value=False)
|
||||
@patch('ceph_volume.objectstore.raw.lvm_api.ceph_volume_lvm_prepare_lv_paths',
|
||||
return_value=[])
|
||||
@patch('ceph_volume.objectstore.raw.encryption_utils.CephLuks2',
|
||||
return_value=MagicMock(is_ceph_encrypted=False))
|
||||
def test_activate_dmcrypt_key_based_opens_mapper(self, m_luks2, m_lvm_paths,
|
||||
m_is_lvm, m_lsblk,
|
||||
m_activate, m_create_key,
|
||||
factory, is_root,
|
||||
monkeypatch):
|
||||
"""Key-based dmcrypt: LUKS device matching osd_fsid is opened before
|
||||
the seastore signature scan so the magic is readable."""
|
||||
m_create_key.return_value = ['AQCee6ZkzhOrJRAAZWSvNC3KdXOpC2w8ly4AZQ==']
|
||||
luks2_match = MagicMock()
|
||||
luks2_match.is_ceph_encrypted = True
|
||||
luks2_match.is_tpm2_enrolled = False
|
||||
luks2_match.osd_fsid = 'aaaa-bbbb'
|
||||
m_luks2.return_value = luks2_match
|
||||
monkeypatch.setattr(
|
||||
'ceph_volume.objectstore.raw.disk.BlockSysFs',
|
||||
lambda dev: MagicMock(has_active_dmcrypt_mapper=False),
|
||||
)
|
||||
m_luks_open = MagicMock()
|
||||
monkeypatch.setattr('ceph_volume.objectstore.raw.encryption_utils.luks_open',
|
||||
m_luks_open)
|
||||
monkeypatch.setattr('ceph_volume.objectstore.raw.disk.has_seastore_label',
|
||||
lambda dev: dev.startswith('/dev/mapper/'))
|
||||
monkeypatch.setattr('os.path.basename', lambda p: 'sda')
|
||||
monkeypatch.setattr('os.path.realpath', lambda p: p)
|
||||
|
||||
args = factory(objectstore='seastore', no_tmpfs=False)
|
||||
rs = RawSeastore(args=args)
|
||||
rs.devices = []
|
||||
rs.osd_id = '1'
|
||||
rs.osd_fsid = 'aaaa-bbbb'
|
||||
rs.activate()
|
||||
|
||||
m_luks_open.assert_called_once()
|
||||
assert rs.block_device_path == '/dev/mapper/ceph-aaaa-bbbb-sda-block-dmcrypt'
|
||||
m_activate.assert_called_once_with()
|
||||
|
||||
@patch('ceph_volume.objectstore.raw.prepare_utils.create_key')
|
||||
@patch('ceph_volume.objectstore.raw.disk.has_seastore_label', return_value=False)
|
||||
@patch('ceph_volume.objectstore.raw.disk.lsblk_all', return_value=[])
|
||||
@patch('ceph_volume.objectstore.raw.lvm_api.is_ceph_volume_lvm_prepared',
|
||||
return_value=False)
|
||||
@patch('ceph_volume.objectstore.raw.lvm_api.ceph_volume_lvm_prepare_lv_paths',
|
||||
return_value=[])
|
||||
@patch('ceph_volume.objectstore.raw.encryption_utils.CephLuks2',
|
||||
return_value=MagicMock(is_ceph_encrypted=False))
|
||||
def test_activate_errors_without_signature(self, m_luks2, m_lvm_paths,
|
||||
m_is_lvm, m_lsblk, m_label,
|
||||
m_create_key, factory, is_root):
|
||||
m_create_key.return_value = ['AQCee6ZkzhOrJRAAZWSvNC3KdXOpC2w8ly4AZQ==']
|
||||
args = factory(
|
||||
objectstore='seastore',
|
||||
osd_id='1',
|
||||
osd_fsid='aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
|
||||
devices=['/dev/sdz'],
|
||||
no_tmpfs=False,
|
||||
)
|
||||
rs = RawSeastore(args=args)
|
||||
rs.devices = list(args.devices)
|
||||
rs.osd_id = args.osd_id
|
||||
rs.osd_fsid = args.osd_fsid
|
||||
with pytest.raises(RuntimeError, match='did not find any SeaStore device'):
|
||||
rs.activate()
|
||||
|
||||
@patch('ceph_volume.objectstore.raw.prepare_utils.create_key')
|
||||
@patch('ceph_volume.objectstore.raw.RawSeastore._activate')
|
||||
@patch('ceph_volume.objectstore.raw.disk.lsblk_all',
|
||||
return_value=[{'NAME': '/dev/sda'}, {'NAME': '/dev/sdb'}])
|
||||
@patch('ceph_volume.objectstore.raw.lvm_api.is_ceph_volume_lvm_prepared',
|
||||
return_value=False)
|
||||
@patch('ceph_volume.objectstore.raw.lvm_api.ceph_volume_lvm_prepare_lv_paths',
|
||||
return_value=[])
|
||||
@patch('ceph_volume.objectstore.raw.encryption_utils.CephLuks2',
|
||||
return_value=MagicMock(is_ceph_encrypted=False))
|
||||
def test_activate_without_devices_scans_all(self, m_luks2, m_lvm_paths,
|
||||
m_is_lvm, m_lsblk_all,
|
||||
m_activate,
|
||||
m_create_key, factory, is_root,
|
||||
monkeypatch):
|
||||
"""cephadm path: --osd-id + --osd-uuid without explicit devices.
|
||||
Only /dev/sda has a seastore signature, /dev/sdb does not."""
|
||||
m_create_key.return_value = ['AQCee6ZkzhOrJRAAZWSvNC3KdXOpC2w8ly4AZQ==']
|
||||
monkeypatch.setattr(
|
||||
'ceph_volume.objectstore.raw.disk.has_seastore_label',
|
||||
lambda dev: dev == '/dev/sda',
|
||||
)
|
||||
args = factory(
|
||||
objectstore='seastore',
|
||||
osd_id='3',
|
||||
osd_fsid='aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
|
||||
no_tmpfs=False,
|
||||
)
|
||||
rs = RawSeastore(args=args)
|
||||
rs.devices = []
|
||||
rs.osd_id = '3'
|
||||
rs.osd_fsid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
|
||||
rs.activate()
|
||||
assert rs.block_device_path == '/dev/sda'
|
||||
m_activate.assert_called_once_with()
|
||||
|
||||
@patch('ceph_volume.objectstore.raw.prepare_utils.create_key')
|
||||
@patch('ceph_volume.objectstore.raw.RawSeastore._activate')
|
||||
@patch('ceph_volume.objectstore.raw.disk.lsblk_all',
|
||||
return_value=[{'NAME': '/dev/sda'}, {'NAME': '/dev/dm-0'}])
|
||||
@patch('ceph_volume.objectstore.raw.lvm_api.ceph_volume_lvm_prepare_lv_paths',
|
||||
return_value=['/dev/dm-0'])
|
||||
@patch('ceph_volume.objectstore.raw.encryption_utils.CephLuks2',
|
||||
return_value=MagicMock(is_ceph_encrypted=False))
|
||||
def test_activate_skips_lvm_prepared_devices(self, m_luks2, m_lvm_paths,
|
||||
m_lsblk_all, m_activate,
|
||||
m_create_key, factory, is_root,
|
||||
monkeypatch):
|
||||
"""LVM-prepared devices must be excluded from both the LUKS pre-scan and
|
||||
the candidate scan so they fall through to LVMActivate instead of being
|
||||
claimed by the raw SeaStore path."""
|
||||
m_create_key.return_value = ['AQCee6ZkzhOrJRAAZWSvNC3KdXOpC2w8ly4AZQ==']
|
||||
# /dev/dm-0 is LVM-prepared; /dev/sda is a raw SeaStore device.
|
||||
# is_ceph_volume_lvm_prepared returns True only for the LVM path.
|
||||
monkeypatch.setattr(
|
||||
'ceph_volume.objectstore.raw.lvm_api.is_ceph_volume_lvm_prepared',
|
||||
lambda dev, paths: dev == '/dev/dm-0',
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
'ceph_volume.objectstore.raw.disk.has_seastore_label',
|
||||
lambda dev: dev == '/dev/sda',
|
||||
)
|
||||
args = factory(
|
||||
objectstore='seastore',
|
||||
osd_id='5',
|
||||
osd_fsid='bbbbbbbb-cccc-dddd-eeee-ffffffffffff',
|
||||
no_tmpfs=False,
|
||||
)
|
||||
rs = RawSeastore(args=args)
|
||||
rs.devices = []
|
||||
rs.osd_id = '5'
|
||||
rs.osd_fsid = 'bbbbbbbb-cccc-dddd-eeee-ffffffffffff'
|
||||
rs.activate()
|
||||
# Only /dev/sda (the raw device) should be picked up.
|
||||
assert rs.block_device_path == '/dev/sda'
|
||||
m_activate.assert_called_once_with()
|
||||
@ -141,24 +141,25 @@ class TestOsdMkfs(object):
|
||||
assert '--keyfile' not in fake_call.calls[0]['args'][0]
|
||||
|
||||
def test_wal_is_added(self, fake_call, monkeypatch, objectstore, factory):
|
||||
args = factory(objecstore='bluestore',
|
||||
args = factory(objectstore='bluestore',
|
||||
osd_id='1',
|
||||
osd_fid='asdf',
|
||||
osd_fsid='asdf',
|
||||
wal_device_path='/dev/smm1',
|
||||
cephx_secret='foo',
|
||||
dmcrypt=False)
|
||||
monkeypatch.setattr(system, 'chown', lambda path: True)
|
||||
o = BaseObjectStore(args)
|
||||
o.objectstore = 'bluestore'
|
||||
o.wal_device_path = '/dev/smm1'
|
||||
o.osd_mkfs()
|
||||
assert '--bluestore-block-wal-path' in fake_call.calls[2]['args'][0]
|
||||
assert '/dev/smm1' in fake_call.calls[2]['args'][0]
|
||||
|
||||
def test_db_is_added(self, fake_call, monkeypatch, factory):
|
||||
args = factory(dmcrypt=False)
|
||||
args = factory(objectstore='bluestore', dmcrypt=False)
|
||||
monkeypatch.setattr(system, 'chown', lambda path: True)
|
||||
o = BaseObjectStore(args)
|
||||
o.args = args
|
||||
o.objectstore = 'bluestore'
|
||||
o.db_device_path = '/dev/smm2'
|
||||
o.osd_mkfs()
|
||||
assert '--bluestore-block-db-path' in fake_call.calls[2]['args'][0]
|
||||
|
||||
@ -2,6 +2,7 @@ import errno
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
import stat
|
||||
import time
|
||||
import json
|
||||
@ -1053,7 +1054,10 @@ def has_bluestore_label(device_path: str) -> bool:
|
||||
|
||||
def has_seastore_label(device_path: str) -> bool:
|
||||
is_seastore = False
|
||||
seastore_disk_signature = b'seastore block device\n' # 23 bytes including newline
|
||||
# 23-byte magic written at offset 0 by all Crimson device types:
|
||||
# "CRIMSON_DEVICE" followed by 9 null bytes.
|
||||
# See src/crimson/os/seastore/device.h: CRIMSON_DEVICE_SUPERBLOCK_MAGIC
|
||||
seastore_disk_signature = b'CRIMSON_DEVICE\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
|
||||
try:
|
||||
with open(device_path, "rb") as fd:
|
||||
@ -1061,12 +1065,44 @@ def has_seastore_label(device_path: str) -> bool:
|
||||
if signature == seastore_disk_signature:
|
||||
is_seastore = True
|
||||
except IsADirectoryError:
|
||||
print(f'{device_path} is a directory, skipping.')
|
||||
except Exception as e:
|
||||
print(f'Error reading {device_path}: {e}')
|
||||
logger.info('%s is a directory, skipping.', device_path)
|
||||
except OSError as e:
|
||||
logger.debug('Error reading %s: %s', device_path, e)
|
||||
|
||||
return is_seastore
|
||||
|
||||
|
||||
def seastore_raw_device_report(device: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Build a raw-list style dict for a SeaStore whole-disk device.
|
||||
|
||||
Detection uses only the on-disk SeaStore volume header (``has_seastore_label``) on the
|
||||
device path coming from ``lsblk`` — no ``/var/lib/ceph`` tree and no orchestrator-specific
|
||||
layout is consulted.
|
||||
|
||||
``osd_uuid`` is a deterministic UUIDv5 derived from the canonical device path. It is a
|
||||
**listing identifier**, not necessarily the OSD fsid known to the cluster. The field
|
||||
``synthetic_osd_uuid`` is always true for SeaStore entries produced this way.
|
||||
"""
|
||||
if not has_seastore_label(device):
|
||||
return None
|
||||
try:
|
||||
block_real = os.path.realpath(device)
|
||||
except OSError as e:
|
||||
logger.debug('Cannot resolve %s: %s', device, e)
|
||||
return None
|
||||
|
||||
synthetic = str(
|
||||
uuid.uuid5(uuid.NAMESPACE_URL, 'ceph-volume:seastore-raw:' + block_real)
|
||||
)
|
||||
return {
|
||||
'type': 'seastore',
|
||||
'device': device,
|
||||
'osd_uuid': synthetic,
|
||||
'synthetic_osd_uuid': True,
|
||||
}
|
||||
|
||||
|
||||
def get_lvm_mappers(sys_block_path: str = '/sys/block') -> List[str]:
|
||||
"""
|
||||
Retrieve a list of Logical Volume Manager (LVM) device mappers.
|
||||
|
||||
@ -3592,6 +3592,16 @@ Then run the following:
|
||||
assert data_devices is not None
|
||||
device_list = [d.path for d in drive_group.data_devices.paths] if drive_group.data_devices else []
|
||||
|
||||
# DriveGroupSpec stores osd_type as str and validate() compares to 'classic'/'crimson';
|
||||
# pass a string, not an OSDType enum member.
|
||||
ot = drive_group.osd_type
|
||||
if isinstance(ot, OSDType):
|
||||
osd_type_str: str = ot.value
|
||||
elif isinstance(ot, str):
|
||||
osd_type_str = ot
|
||||
else:
|
||||
osd_type_str = 'classic'
|
||||
|
||||
osd_default_spec = DriveGroupSpec(
|
||||
service_id="default",
|
||||
placement=PlacementSpec(host_pattern=host),
|
||||
@ -3602,7 +3612,7 @@ Then run the following:
|
||||
unmanaged=False,
|
||||
method=drive_group.method,
|
||||
objectstore=drive_group.objectstore,
|
||||
osd_type=OSDType(drive_group.osd_type)
|
||||
osd_type=osd_type_str
|
||||
)
|
||||
|
||||
self.log.info(f"Creating OSDs with service ID: {drive_group.service_id} on {host}:{device_list}")
|
||||
|
||||
@ -145,14 +145,23 @@ def wait(m: CephadmOrchestrator, c: OrchResult) -> Any:
|
||||
return raise_if_exception(c)
|
||||
|
||||
|
||||
def _noop_refresh_hosts_and_daemons(self: Any) -> None:
|
||||
"""Avoid SSH / agent checks in unit tests; they mark hosts offline without real infra."""
|
||||
return None
|
||||
|
||||
|
||||
@contextmanager
|
||||
def with_host(m: CephadmOrchestrator, name, addr='1::4', refresh_hosts=True, rm_with_force=True):
|
||||
with mock.patch("cephadm.utils.resolve_ip", return_value=addr):
|
||||
# Skip real SSH in unit tests (check-host / key setup); inventory still updates.
|
||||
with mock.patch("cephadm.utils.resolve_ip", return_value=addr), \
|
||||
mock.patch.object(m, "_check_valid_addr", return_value=addr), \
|
||||
mock.patch.object(CephadmServe, "_refresh_hosts_and_daemons", _noop_refresh_hosts_and_daemons):
|
||||
wait(m, m.add_host(HostSpec(hostname=name)))
|
||||
if refresh_hosts:
|
||||
CephadmServe(m)._refresh_hosts_and_daemons()
|
||||
receive_agent_metadata(m, name)
|
||||
yield
|
||||
m.offline_hosts.discard(name)
|
||||
wait(m, m.remove_host(name, force=rm_with_force))
|
||||
|
||||
|
||||
|
||||
@ -1399,13 +1399,19 @@ class TestCephadm(object):
|
||||
@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
|
||||
def test_create_osds(self, cephadm_module):
|
||||
with with_host(cephadm_module, 'test'):
|
||||
dg = DriveGroupSpec(placement=PlacementSpec(host_pattern='test'),
|
||||
data_devices=DeviceSelection(paths=['']))
|
||||
# Non-empty paths (empty string is rejected by validate_no_empty_device_paths).
|
||||
# db_devices with a filter but no paths makes is_path_lists_only False so an empty
|
||||
# device cache yields "No devices found".
|
||||
dg = DriveGroupSpec(
|
||||
placement=PlacementSpec(host_pattern='test'),
|
||||
data_devices=DeviceSelection(paths=['/dev/unused']),
|
||||
db_devices=DeviceSelection(rotational=True),
|
||||
)
|
||||
c = cephadm_module.create_osds(dg)
|
||||
out = wait(cephadm_module, c)
|
||||
assert "Error: Device path is empty." in out
|
||||
bad_dg = DriveGroupSpec(placement=PlacementSpec(host_pattern='invalid_host'),
|
||||
data_devices=DeviceSelection(paths=['']))
|
||||
data_devices=DeviceSelection(paths=['/dev/unused']))
|
||||
c = cephadm_module.create_osds(bad_dg)
|
||||
out = wait(cephadm_module, c)
|
||||
assert "Invalid 'host:device' spec: host not found in cluster" in out
|
||||
@ -1413,8 +1419,11 @@ class TestCephadm(object):
|
||||
@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
|
||||
def test_create_noncollocated_osd(self, cephadm_module):
|
||||
with with_host(cephadm_module, 'test'):
|
||||
dg = DriveGroupSpec(placement=PlacementSpec(host_pattern='test'),
|
||||
data_devices=DeviceSelection(paths=['']))
|
||||
dg = DriveGroupSpec(
|
||||
placement=PlacementSpec(host_pattern='test'),
|
||||
data_devices=DeviceSelection(paths=['/dev/unused']),
|
||||
db_devices=DeviceSelection(rotational=True),
|
||||
)
|
||||
c = cephadm_module.create_osds(dg)
|
||||
out = wait(cephadm_module, c)
|
||||
assert "Error: Device path is empty." in out
|
||||
@ -1448,7 +1457,7 @@ class TestCephadm(object):
|
||||
def test_prepare_drivegroup(self, cephadm_module):
|
||||
with with_host(cephadm_module, 'test'):
|
||||
dg = DriveGroupSpec(placement=PlacementSpec(host_pattern='test'),
|
||||
data_devices=DeviceSelection(paths=['']))
|
||||
data_devices=DeviceSelection(paths=['/dev/unused']))
|
||||
out = cephadm_module.osd_service.prepare_drivegroup(dg)
|
||||
assert len(out) == 1
|
||||
f1 = out[0]
|
||||
@ -1549,6 +1558,24 @@ class TestCephadm(object):
|
||||
assert all(any(cmd in exp_cmd for exp_cmd in exp_commands)
|
||||
for cmd in out), f'Expected cmds from f{out} in {exp_commands}'
|
||||
|
||||
@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
|
||||
def test_raw_seastore_driveselection_to_ceph_volume(self, cephadm_module):
|
||||
with with_host(cephadm_module, 'test'):
|
||||
dg = DriveGroupSpec(
|
||||
service_id='test.spec',
|
||||
method='raw',
|
||||
placement=PlacementSpec(host_pattern='test'),
|
||||
data_devices=DeviceSelection(paths=['/dev/sda']),
|
||||
objectstore='seastore',
|
||||
osd_type='crimson',
|
||||
)
|
||||
dg.validate()
|
||||
ds = DriveSelection(dg, Devices([Device(path='/dev/sda')]))
|
||||
out = cephadm_module.osd_service.driveselection_to_ceph_volume(ds, [], False)
|
||||
assert out == [
|
||||
'raw prepare --objectstore seastore --data /dev/sda --osd-type crimson',
|
||||
]
|
||||
|
||||
@mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm(
|
||||
json.dumps([
|
||||
dict(
|
||||
|
||||
@ -397,9 +397,14 @@ class DriveGroupSpec(ServiceSpec):
|
||||
self.service_id,
|
||||
'method raw only supports bluestore')
|
||||
if self.method == 'raw' and self.objectstore == 'seastore':
|
||||
raise DriveGroupValidationError(
|
||||
self.service_id,
|
||||
'method raw only supports bluestore')
|
||||
if self.db_devices is not None:
|
||||
raise DriveGroupValidationError(
|
||||
self.service_id,
|
||||
'method raw with objectstore seastore does not support db_devices')
|
||||
if self.wal_devices is not None:
|
||||
raise DriveGroupValidationError(
|
||||
self.service_id,
|
||||
'method raw with objectstore seastore does not support wal_devices')
|
||||
if self.data_devices.paths is not None:
|
||||
for device in list(self.data_devices.paths):
|
||||
if not device.path:
|
||||
|
||||
@ -86,7 +86,6 @@ class to_ceph_volume(object):
|
||||
continue
|
||||
|
||||
if self.spec.method == 'raw':
|
||||
assert self.spec.objectstore == 'bluestore'
|
||||
# ceph-volume raw prepare only support 1:1 ratio of data to db/wal devices
|
||||
# for raw prepare each data device needs its own prepare command
|
||||
dev_counter = 0
|
||||
@ -95,12 +94,19 @@ class to_ceph_volume(object):
|
||||
wal_devices.reverse()
|
||||
|
||||
while dev_counter < len(data_devices):
|
||||
cmd = "raw prepare --bluestore"
|
||||
if self.spec.objectstore == 'bluestore':
|
||||
cmd = "raw prepare --bluestore"
|
||||
elif self.spec.objectstore == 'seastore':
|
||||
cmd = "raw prepare --objectstore seastore"
|
||||
else:
|
||||
raise ValueError(
|
||||
'raw method only supports bluestore or seastore objectstore')
|
||||
cmd += " --data {}".format(data_devices[dev_counter])
|
||||
if db_devices:
|
||||
cmd += " --block.db {}".format(db_devices.pop())
|
||||
if wal_devices:
|
||||
cmd += " --block.wal {}".format(wal_devices.pop())
|
||||
if self.spec.objectstore == 'bluestore':
|
||||
if db_devices:
|
||||
cmd += " --block.db {}".format(db_devices.pop())
|
||||
if wal_devices:
|
||||
cmd += " --block.wal {}".format(wal_devices.pop())
|
||||
if d != self.NO_CRUSH:
|
||||
cmd += " --crush-device-class {}".format(d)
|
||||
|
||||
|
||||
@ -694,6 +694,65 @@ def test_drive_group_seastore_with_crimson_valid():
|
||||
spec.validate()
|
||||
|
||||
|
||||
def test_drive_group_raw_seastore_rejects_db_devices():
|
||||
spec = DriveGroupSpec(
|
||||
placement=PlacementSpec(host_pattern='*'),
|
||||
service_id='foobar',
|
||||
data_devices=DeviceSelection(paths=['/dev/sda']),
|
||||
db_devices=DeviceSelection(paths=['/dev/sdb']),
|
||||
method='raw',
|
||||
objectstore='seastore',
|
||||
osd_type='crimson',
|
||||
)
|
||||
with pytest.raises(DriveGroupValidationError, match='does not support db_devices'):
|
||||
spec.validate()
|
||||
|
||||
|
||||
def test_drive_group_raw_seastore_rejects_wal_devices():
|
||||
spec = DriveGroupSpec(
|
||||
placement=PlacementSpec(host_pattern='*'),
|
||||
service_id='foobar',
|
||||
data_devices=DeviceSelection(paths=['/dev/sda']),
|
||||
wal_devices=DeviceSelection(paths=['/dev/sdc']),
|
||||
method='raw',
|
||||
objectstore='seastore',
|
||||
osd_type='crimson',
|
||||
)
|
||||
with pytest.raises(DriveGroupValidationError, match='does not support wal_devices'):
|
||||
spec.validate()
|
||||
|
||||
|
||||
def test_drive_group_raw_seastore_valid():
|
||||
spec = DriveGroupSpec(
|
||||
placement=PlacementSpec(host_pattern='*'),
|
||||
service_id='foobar',
|
||||
data_devices=DeviceSelection(paths=['/dev/sda']),
|
||||
method='raw',
|
||||
objectstore='seastore',
|
||||
osd_type='crimson',
|
||||
)
|
||||
spec.validate()
|
||||
|
||||
|
||||
def test_raw_ceph_volume_command_seastore():
|
||||
spec = DriveGroupSpec(
|
||||
placement=PlacementSpec(host_pattern='*'),
|
||||
service_id='foobar',
|
||||
data_devices=DeviceSelection(rotational=True),
|
||||
method='raw',
|
||||
objectstore='seastore',
|
||||
osd_type='crimson',
|
||||
)
|
||||
spec.validate()
|
||||
inventory = _mk_inventory(_mk_device(rotational=True) + _mk_device(rotational=True))
|
||||
sel = drive_selection.DriveSelection(spec, inventory)
|
||||
cmds = translate.to_ceph_volume(sel, []).run()
|
||||
assert cmds == [
|
||||
'raw prepare --objectstore seastore --data /dev/sda --osd-type crimson',
|
||||
'raw prepare --objectstore seastore --data /dev/sdb --osd-type crimson',
|
||||
]
|
||||
|
||||
|
||||
def test_drive_group_osd_type_invalid():
|
||||
spec = DriveGroupSpec(
|
||||
placement=PlacementSpec(host_pattern='*'),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user