]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
ceph-volume: add TPM2 token enrollment support for encrypted OSDs
authorGuillaume Abrioux <gabrioux@ibm.com>
Thu, 16 May 2024 15:47:19 +0000 (11:47 -0400)
committerGuillaume Abrioux <gabrioux@ibm.com>
Mon, 12 Aug 2024 08:43:59 +0000 (08:43 +0000)
This adds the required changes to ceph-volume and cephadm in order to support
deploying tpm2 token enrolled encrypted OSDs.

Adding `--with-tpm` when deploying with `--dmcrypt` will enroll a tpm2
token to the corresponding LUKS2 devices.

Example of a osd service spec:

```
service_type: osd
service_id: tpm2_osds
placement:
  host_pattern: '*'
spec:
  data_devices:
    paths:
      - /dev/sdb
  encrypted: true
  tpm2: true
```

Signed-off-by: Guillaume Abrioux <gabrioux@ibm.com>
28 files changed:
doc/ceph-volume/lvm/prepare.rst
doc/cephadm/services/osd.rst
src/ceph-volume/ceph_volume/__init__.py
src/ceph-volume/ceph_volume/devices/lvm/batch.py
src/ceph-volume/ceph_volume/devices/lvm/common.py
src/ceph-volume/ceph_volume/devices/raw/activate.py
src/ceph-volume/ceph_volume/devices/raw/common.py
src/ceph-volume/ceph_volume/devices/raw/list.py
src/ceph-volume/ceph_volume/devices/raw/prepare.py
src/ceph-volume/ceph_volume/objectstore/__init__.py
src/ceph-volume/ceph_volume/objectstore/baseobjectstore.py
src/ceph-volume/ceph_volume/objectstore/bluestore.py
src/ceph-volume/ceph_volume/objectstore/lvmbluestore.py
src/ceph-volume/ceph_volume/objectstore/rawbluestore.py
src/ceph-volume/ceph_volume/tests/conftest.py
src/ceph-volume/ceph_volume/tests/devices/lvm/test_prepare.py
src/ceph-volume/ceph_volume/tests/devices/raw/test_prepare.py
src/ceph-volume/ceph_volume/tests/objectstore/test_lvmbluestore.py
src/ceph-volume/ceph_volume/tests/objectstore/test_rawbluestore.py
src/ceph-volume/ceph_volume/tests/test_ceph_volume.py [new file with mode: 0644]
src/ceph-volume/ceph_volume/tests/util/test_disk.py
src/ceph-volume/ceph_volume/tests/util/test_encryption.py
src/ceph-volume/ceph_volume/tests/util/test_prepare.py
src/ceph-volume/ceph_volume/util/device.py
src/ceph-volume/ceph_volume/util/disk.py
src/ceph-volume/ceph_volume/util/encryption.py
src/python-common/ceph/deployment/drive_group.py
src/python-common/ceph/deployment/translate.py

index 2faf12a4e1fe8c2586a976bbe52ecae02d35f8b4..c7dae83d06272314c30b0ce0200bdb4dbcbc4c1d 100644 (file)
@@ -61,6 +61,12 @@ For enabling :ref:`encryption <ceph-volume-lvm-encryption>`, the ``--dmcrypt`` f
 
     ceph-volume lvm prepare --bluestore --dmcrypt --data vg/lv
 
+Starting with Ceph Squid, you can opt for TPM2 token enrollment for the created LUKS2 devices with the ``--with-tpm`` flag:
+
+.. prompt:: bash #
+
+    ceph-volume lvm prepare --bluestore --dmcrypt --with-tpm --data vg/lv
+
 If a ``block.db`` device or a ``block.wal`` device is needed, it can be
 specified with ``--block.db`` or ``--block.wal``. These can be physical
 devices, partitions, or logical volumes. ``block.db`` and ``block.wal`` are
index 5ad39de85696908d33ceb05c62134e859476cfb3..3ed091a06a52106b22c390664269ad2d42182f67 100644 (file)
@@ -666,6 +666,21 @@ This example would deploy all OSDs with encryption enabled.
         all: true
       encrypted: true
 
+Ceph Squid onwards support tpm2 token enrollment to LUKS2 devices.
+You can add the `tpm2` to your OSD spec:
+
+.. code-block:: yaml
+
+    service_type: osd
+    service_id: example_osd_spec_with_tpm2
+    placement:
+      host_pattern: '*'
+    spec:
+      data_devices:
+        all: true
+      encrypted: true
+      tpm2: true
+
 See a full list in the DriveGroupSpecs
 
 .. py:currentmodule:: ceph.deployment.drive_group
index 598f0b27ef9bee73a6961f7f5299d6b04e5d016e..b10100c02185a0f621068e34aa26282bcb4a34a2 100644 (file)
@@ -1,8 +1,33 @@
+import os
+import logging
 from collections import namedtuple
 
 
 sys_info = namedtuple('sys_info', ['devices'])
 sys_info.devices = dict()
+logger = logging.getLogger(__name__)
+
+
+class AllowLoopDevices:
+    allow = False
+    warned = False
+
+    @classmethod
+    def __call__(cls) -> bool:
+        val = os.environ.get("CEPH_VOLUME_ALLOW_LOOP_DEVICES", "false").lower()
+        if val not in ("false", 'no', '0'):
+            cls.allow = True
+            if not cls.warned:
+                logger.warning(
+                    "CEPH_VOLUME_ALLOW_LOOP_DEVICES is set in your "
+                    "environment, so we will allow the use of unattached loop"
+                    " devices as disks. This feature is intended for "
+                    "development purposes only and will never be supported in"
+                    " production. Issues filed based on this behavior will "
+                    "likely be ignored."
+                )
+                cls.warned = True
+        return cls.allow
 
 
 class UnloadedConfig(object):
@@ -14,6 +39,8 @@ class UnloadedConfig(object):
     def __getattr__(self, *a):
         raise RuntimeError("No valid ceph configuration file was loaded.")
 
+
+allow_loop_devices = AllowLoopDevices()
 conf = namedtuple('config', ['ceph', 'cluster', 'verbosity', 'path', 'log_path', 'dmcrypt_no_workqueue'])
 conf.ceph = UnloadedConfig()
 conf.dmcrypt_no_workqueue = None
index 6cd3bc39817772c69b8a672f3a599c23cda05c95..42f431356618986d4073ba3b53ba46aa15540f57 100644 (file)
@@ -267,6 +267,12 @@ class Batch(object):
             action=arg_validators.DmcryptAction,
             help='Enable device encryption via dm-crypt',
         )
+        parser.add_argument(
+            '--with-tpm',
+            dest='with_tpm',
+            help='Whether encrypted OSDs should be enrolled with TPM.',
+            action='store_true'
+        )
         parser.add_argument(
             '--crush-device-class',
             dest='crush_device_class',
@@ -423,6 +429,7 @@ class Batch(object):
         global_args = [
             'bluestore',
             'dmcrypt',
+            'with_tpm',
             'crush_device_class',
             'no_systemd',
         ]
index 90bed61a3bd948f4b0b40e9c49034370af6bd787..e18d98bb6fbd97cc422b9b20de3015e6f90e8f0d 100644 (file)
@@ -83,6 +83,11 @@ common_args = {
         'action': arg_validators.DmcryptAction,
         'help': 'Enable device encryption via dm-crypt',
     },
+    '--with-tpm': {
+        'dest': 'with_tpm',
+        'help': 'Whether encrypted OSDs should be enrolled with TPM.',
+        'action': 'store_true'
+    },
     '--no-systemd': {
         'dest': 'no_systemd',
         'action': 'store_true',
index 38c74ef9829a6db7933febf59659dd3751fd855a..0cec810728babf504c6554384cf0d861ee615b1d 100644 (file)
@@ -39,12 +39,19 @@ class Activate(object):
             '--device',
             help='The device for the OSD to start'
         )
+        parser.add_argument(
+            '--devices',
+            help='The device for the OSD to start',
+            nargs='*',
+            default=[]
+        )
         parser.add_argument(
             '--osd-id',
             help='OSD ID to activate'
         )
         parser.add_argument(
             '--osd-uuid',
+            dest='osd_fsid',
             help='OSD UUID to active'
         )
         parser.add_argument(
@@ -82,15 +89,11 @@ class Activate(object):
             return
         self.args = parser.parse_args(self.argv)
 
-        devs = []
         if self.args.device:
-            devs = [self.args.device]
-        if self.args.block_wal:
-            devs.append(self.args.block_wal)
-        if self.args.block_db:
-            devs.append(self.args.block_db)
+            if self.args.devices is None:
+                self.args.devices = [self.args.device]
+            else:
+                self.args.devices.append(self.args.device)
+
         self.objectstore = objectstore.mapping['RAW'][self.args.objectstore](args=self.args)
-        self.objectstore.activate(devs=devs,
-                                  start_osd_id=self.args.osd_id,
-                                  start_osd_uuid=self.args.osd_uuid,
-                                  tmpfs=not self.args.no_tmpfs)
+        self.objectstore.activate()
index e3aea2c7250dc5689c50c70a936937c7a80849a3..77db2f7daf9893764eceaf7d8f57f1094ae90bf1 100644 (file)
@@ -1,7 +1,7 @@
 import argparse
 from ceph_volume.util import arg_validators
 
-def create_parser(prog, description):
+def create_parser(prog: str, description: str) -> argparse.ArgumentParser:
     """
     Both prepare and create share the same parser, those are defined here to
     avoid duplication
@@ -58,6 +58,12 @@ def create_parser(prog, description):
         action=arg_validators.DmcryptAction,
         help='Enable device encryption via dm-crypt',
     )
+    parser.add_argument(
+        '--with-tpm',
+        dest='with_tpm',
+        help='Whether encrypted OSDs should be enrolled with TPM.',
+        action='store_true'
+    ),
     parser.add_argument(
         '--osd-id',
         help='Reuse an existing OSD id',
index 794bb18c103fd9db10a7c6dae892d7d5f3f319ad..f6ac08eab98a9038840df6627203b37f5ed9bcea 100644 (file)
@@ -5,7 +5,7 @@ import logging
 from textwrap import dedent
 from ceph_volume import decorators, process
 from ceph_volume.util import disk
-from typing import Any, Dict, List
+from typing import Any, Dict, List as _List
 
 logger = logging.getLogger(__name__)
 
@@ -20,7 +20,8 @@ def direct_report(devices):
     _list = List([])
     return _list.generate(devices)
 
-def _get_bluestore_info(dev):
+def _get_bluestore_info(dev: str) -> Dict[str, Any]:
+    result: Dict[str, Any] = {}
     out, err, rc = process.call([
         'ceph-bluestore-tool', 'show-label',
         '--dev', dev], verbose_on_failure=False)
@@ -28,42 +29,26 @@ def _get_bluestore_info(dev):
         # ceph-bluestore-tool returns an error (below) if device is not bluestore OSD
         #   > unable to read label for <device>: (2) No such file or directory
         # but it's possible the error could be for a different reason (like if the disk fails)
-        logger.debug('assuming device {} is not BlueStore; ceph-bluestore-tool failed to get info from device: {}\n{}'.format(dev, out, err))
-        return None
-    oj = json.loads(''.join(out))
-    if dev not in oj:
-        # should be impossible, so warn
-        logger.warning('skipping device {} because it is not reported in ceph-bluestore-tool output: {}'.format(dev, out))
-        return None
-    try:
-        r = {
-            'osd_uuid': oj[dev]['osd_uuid'],
-        }
-        if oj[dev]['description'] == 'main':
-            whoami = oj[dev]['whoami']
-            r.update({
-                'type': 'bluestore',
-                'osd_id': int(whoami),
-                'ceph_fsid': oj[dev]['ceph_fsid'],
-                'device': dev,
-            })
-        elif oj[dev]['description'] == 'bluefs db':
-            r['device_db'] = dev
-        elif oj[dev]['description'] == 'bluefs wal':
-            r['device_wal'] = dev
-        return r
-    except KeyError as e:
-        # this will appear for devices that have a bluestore header but aren't valid OSDs
-        # for example, due to incomplete rollback of OSDs: https://tracker.ceph.com/issues/51869
-        logger.error('device {} does not have all BlueStore data needed to be a valid OSD: {}\n{}'.format(dev, out, e))
-        return None
+        logger.debug(f'assuming device {dev} is not BlueStore; ceph-bluestore-tool failed to get info from device: {out}\n{err}')
+    else:
+        oj = json.loads(''.join(out))
+        if dev not in oj:
+            # should be impossible, so warn
+            logger.warning(f'skipping device {dev} because it is not reported in ceph-bluestore-tool output: {out}')
+        try:
+            result = disk.bluestore_info(dev, oj)
+        except KeyError as e:
+            # this will appear for devices that have a bluestore header but aren't valid OSDs
+            # for example, due to incomplete rollback of OSDs: https://tracker.ceph.com/issues/51869
+            logger.error(f'device {dev} does not have all BlueStore data needed to be a valid OSD: {out}\n{e}')
+    return result
 
 
 class List(object):
 
     help = 'list BlueStore OSDs on raw devices'
 
-    def __init__(self, argv):
+    def __init__(self, argv: _List[str]) -> None:
         self.argv = argv
 
     def is_atari_partitions(self, _lsblk: Dict[str, Any]) -> bool:
@@ -81,7 +66,7 @@ class List(object):
                 return True
         return False
 
-    def exclude_atari_partitions(self, _lsblk_all: Dict[str, Any]) -> List[Dict[str, Any]]:
+    def exclude_atari_partitions(self, _lsblk_all: Dict[str, Any]) -> _List[Dict[str, Any]]:
         return [_lsblk for _lsblk in _lsblk_all if not self.is_atari_partitions(_lsblk)]
 
     def generate(self, devs=None):
@@ -113,7 +98,7 @@ class List(object):
         logger.debug('inspecting devices: {}'.format(devs))
         for info_device in info_devices:
             bs_info = _get_bluestore_info(info_device['NAME'])
-            if bs_info is None:
+            if not bs_info:
                 # None is also returned in the rare event that there is an issue reading info from
                 # a BlueStore disk, so be sure to log our assumption that it isn't bluestore
                 logger.info('device {} does not have BlueStore information'.format(info_device['NAME']))
index e4308e55036755cc35c4e770d989ed3c3f0315c5..99dd6a69e9816a129d73eadc4362c7a524cfbf50 100644 (file)
@@ -42,11 +42,13 @@ class Prepare(object):
         self.args = parser.parse_args(self.argv)
         if self.args.bluestore:
             self.args.objectstore = 'bluestore'
-        if self.args.dmcrypt and not os.getenv('CEPH_VOLUME_DMCRYPT_SECRET'):
-            terminal.error('encryption was requested (--dmcrypt) but environment variable ' \
-                           'CEPH_VOLUME_DMCRYPT_SECRET is not set, you must set ' \
-                           'this variable to provide a dmcrypt secret.')
-            raise SystemExit(1)
+        if self.args.dmcrypt:
+            if not self.args.with_tpm and not os.getenv('CEPH_VOLUME_DMCRYPT_SECRET'):
+                terminal.error('encryption was requested (--dmcrypt) but environment variable ' \
+                               'CEPH_VOLUME_DMCRYPT_SECRET is not set, you must set ' \
+                               'this variable to provide a dmcrypt secret or use --with-tpm ' \
+                               'in order to enroll a tpm2 token.')
+                raise SystemExit(1)
 
         self.objectstore = objectstore.mapping['RAW'][self.args.objectstore](args=self.args)
         self.objectstore.safe_prepare(self.args)
index f8bc2c507934c62b973a4c003a059745693cc20d..faef2ae6231bad6dcf88ea940ff289e388459da6 100644 (file)
@@ -1,7 +1,9 @@
 from . import lvmbluestore
 from . import rawbluestore
+from typing import Any, Dict
 
-mapping = {
+
+mapping: Dict[str, Any] = {
     'LVM': {
         'bluestore': lvmbluestore.LvmBlueStore
     },
index 822f293f3ad94959897e68246784c6a8460faaf2..6ac4cbd9f2b785e666d67121240f55fd567a8f4d 100644 (file)
@@ -2,9 +2,11 @@ import logging
 import os
 import errno
 import time
+import tempfile
 from ceph_volume import conf, terminal, process
 from ceph_volume.util import prepare as prepare_utils
 from ceph_volume.util import system, disk
+from ceph_volume.util import encryption as encryption_utils
 from typing import Dict, Any, List, Optional, TYPE_CHECKING
 
 if TYPE_CHECKING:
@@ -22,21 +24,23 @@ class BaseObjectStore:
         # for the OSD, this needs to be fixed. This could either be a file (!)
         # or a string (!!) or some flags that we would need to compound
         # into a dict so that we can convert to JSON (!!!)
-        self.secrets = {'cephx_secret': prepare_utils.create_key()}
-        self.cephx_secret = self.secrets.get('cephx_secret',
-                                             prepare_utils.create_key())
-        self.encrypted = 0
+        self.secrets: Dict[str, str] = {'cephx_secret': prepare_utils.create_key()}
+        self.cephx_secret: str = self.secrets.get('cephx_secret',
+                                                  prepare_utils.create_key())
+        self.encrypted: int = 0
         self.tags: Dict[str, Any] = {}
         self.osd_id: str = ''
-        self.osd_fsid = ''
-        self.block_lv: Optional["Volume"] = None
-        self.cephx_lockbox_secret = ''
+        self.osd_fsid: str = ''
+        self.cephx_lockbox_secret: str = ''
         self.objectstore: str = ''
         self.osd_mkfs_cmd: List[str] = []
-        self.block_device_path = ''
-        if hasattr(self.args, 'dmcrypt'):
-            if self.args.dmcrypt:
-                self.encrypted = 1
+        self.block_device_path: str = ''
+        self.dmcrypt_key: str = encryption_utils.create_dmcrypt_key()
+        self.with_tpm: int = int(getattr(self.args, 'with_tpm', False))
+        self.method: str = ''
+        if getattr(self.args, 'dmcrypt', False):
+            self.encrypted = 1
+            if not self.with_tpm:
                 self.cephx_lockbox_secret = prepare_utils.create_key()
                 self.secrets['cephx_lockbox_secret'] = \
                     self.cephx_lockbox_secret
@@ -152,3 +156,23 @@ class BaseObjectStore:
 
     def activate(self) -> None:
         raise NotImplementedError()
+
+    def enroll_tpm2(self, device: str) -> None:
+        """
+        Enrolls a device with TPM2 (Trusted Platform Module 2.0) using systemd-cryptenroll.
+        This method creates a temporary file to store the dmcrypt key and uses it to enroll the device.
+
+        Args:
+            device (str): The device path to be enrolled with TPM2.
+        """
+
+        if self.with_tpm:
+            tmp_dir: str = '/rootfs/tmp' if os.environ.get('I_AM_IN_A_CONTAINER', False) else '/tmp'
+            with tempfile.NamedTemporaryFile(mode='w', delete=True, dir=tmp_dir) as temp_file:
+                temp_file.write(self.dmcrypt_key)
+                temp_file.flush()
+                temp_file_name: str = temp_file.name.replace('/rootfs', '', 1)
+                cmd: List[str] = ['systemd-cryptenroll', '--tpm2-device=auto',
+                                  device, '--unlock-key-file', temp_file_name,
+                                  '--tpm2-pcrs', '9+12', '--wipe-slot', 'tpm2']
+                process.call(cmd, run_on_host=True, show_command=True)
index e9b0e9516cb815be2106f92b8dadcae172b49abe..ceaa199a18b7f3e08c1f7185dcfc96f2c48cf465 100644 (file)
@@ -2,10 +2,13 @@ import logging
 import os
 from .baseobjectstore import BaseObjectStore
 from ceph_volume.util import system
-from typing import Optional, TYPE_CHECKING
+from ceph_volume.util.encryption import CephLuks2
+from ceph_volume import process
+from typing import Any, Dict, List, Optional, TYPE_CHECKING
 
 if TYPE_CHECKING:
     import argparse
+    from ceph_volume.api.lvm import Volume
 
 logger = logging.getLogger(__name__)
 
@@ -22,6 +25,7 @@ class BlueStore(BaseObjectStore):
         self.block_device_path: str = ''
         self.wal_device_path: str = ''
         self.db_device_path: str = ''
+        self.block_lv: Volume
 
     def add_objectstore_opts(self) -> None:
         """
@@ -59,3 +63,44 @@ class BlueStore(BaseObjectStore):
             link_path = os.path.join(self.osd_path, link_name)
             if os.path.exists(link_path):
                 os.unlink(os.path.join(self.osd_path, link_name))
+
+
+    def add_label(self, key: str,
+                  value: str,
+                  device: str) -> None:
+        """Add a label to a BlueStore device.
+        Args:
+            key (str): The name of the label being added.
+            value (str): Value of the label being added.
+            device (str): The path of the BlueStore device.
+        Raises:
+            RuntimeError: If `ceph-bluestore-tool` command doesn't success.
+        """
+
+        command: List[str] = ['ceph-bluestore-tool',
+                              'set-label-key',
+                              '-k',
+                              key,
+                              '-v',
+                              value,
+                              '--dev',
+                              device]
+
+        _, err, rc = process.call(command,
+                                  terminal_verbose=True,
+                                  show_command=True)
+        if rc:
+            raise RuntimeError(f"Can't add BlueStore label '{key}' to device {device}: {err}")
+
+    def osd_mkfs(self) -> None:
+        super().osd_mkfs()
+        mapping: Dict[str, Any] = {'raw': ['data', 'block_db', 'block_wal'],
+                                   'lvm': ['ceph.block_device', 'ceph.db_device', 'ceph.wal_device']}
+        if self.args.dmcrypt:
+            for dev_type in mapping[self.method]:
+                if self.method == 'raw':
+                    path = self.args.__dict__.get(dev_type, None)
+                else:
+                    path = self.block_lv.tags.get(dev_type, None)
+                if path is not None:
+                    CephLuks2(path).config_luks2({'subsystem': f'ceph_fsid={self.osd_fsid}'})
index 0fc3c68f8c6d856f88f36d6c1be643a1be4e90fe..47b179bc0e1b89066af58ab26cfe657213ff87b3 100644 (file)
@@ -22,12 +22,12 @@ logger = logging.getLogger(__name__)
 class LvmBlueStore(BlueStore):
     def __init__(self, args: "argparse.Namespace") -> None:
         super().__init__(args)
+        self.method = 'lvm'
         self.tags: Dict[str, Any] = {}
-        self.block_lv: Optional["Volume"] = None
 
     def pre_prepare(self) -> None:
-        if self.encrypted:
-            self.secrets['dmcrypt_key'] = encryption_utils.create_dmcrypt_key()
+        if self.encrypted and not self.with_tpm:
+            self.secrets['dmcrypt_key'] = self.dmcrypt_key
 
         cluster_fsid = self.get_cluster_fsid()
 
@@ -63,6 +63,7 @@ class LvmBlueStore(BlueStore):
         self.tags['ceph.block_uuid'] = self.block_lv.__dict__['lv_uuid']
         self.tags['ceph.cephx_lockbox_secret'] = self.cephx_lockbox_secret
         self.tags['ceph.encrypted'] = self.encrypted
+        self.tags['ceph.with_tpm'] = 1 if self.with_tpm else 0
         self.tags['ceph.vdo'] = api.is_vdo(self.block_lv.__dict__['lv_path'])
 
     def prepare_data_device(self,
@@ -158,7 +159,7 @@ class LvmBlueStore(BlueStore):
         self.block_lv.set_tags(self.tags)  # type: ignore
 
         # 3/ encryption-only operations
-        if self.secrets.get('dmcrypt_key'):
+        if self.encrypted:
             self.prepare_dmcrypt()
 
         # 4/ osd_prepare req
@@ -175,24 +176,18 @@ class LvmBlueStore(BlueStore):
         # done on activation. Format and open ('decrypt' devices) and
         # re-assign the device and journal variables so that the rest of the
         # process can use the mapper paths
-        key = self.secrets['dmcrypt_key']
-
-        self.block_device_path = \
-            self.luks_format_and_open(key,
-                                      self.block_device_path,
-                                      'block',
-                                      self.tags)
-        self.wal_device_path = self.luks_format_and_open(key,
-                                                         self.wal_device_path,
-                                                         'wal',
-                                                         self.tags)
-        self.db_device_path = self.luks_format_and_open(key,
-                                                        self.db_device_path,
-                                                        'db',
-                                                        self.tags)
+
+        device_types = ('block', 'db', 'wal')
+
+        for device_type in device_types:
+            attr_name: str = f'{device_type}_device_path'
+            path: str = self.__dict__[attr_name]
+            if path:
+                self.__dict__[attr_name] = self.luks_format_and_open(path,
+                                                                     device_type,
+                                                                     self.tags)
 
     def luks_format_and_open(self,
-                             key: Optional[str],
                              device: str,
                              device_type: str,
                              tags: Dict[str, Any]) -> str:
@@ -206,14 +201,18 @@ class LvmBlueStore(BlueStore):
         uuid = tags[tag_name]
         # format data device
         encryption_utils.luks_format(
-            key,
+            self.dmcrypt_key,
             device
         )
+
+        if self.with_tpm:
+            self.enroll_tpm2(device)
+
         encryption_utils.luks_open(
-            key,
+            self.dmcrypt_key,
             device,
-            uuid
-        )
+            uuid,
+            self.with_tpm)
 
         return '/dev/mapper/%s' % uuid
 
@@ -346,7 +345,7 @@ class LvmBlueStore(BlueStore):
             raise RuntimeError('could not find a bluestore OSD to activate')
 
         is_encrypted = osd_block_lv.tags.get('ceph.encrypted', '0') == '1'
-        dmcrypt_secret = None
+        dmcrypt_secret = ''
         osd_id = osd_block_lv.tags['ceph.osd_id']
         conf.cluster = osd_block_lv.tags['ceph.cluster_name']
         osd_fsid = osd_block_lv.tags['ceph.osd_fsid']
@@ -368,13 +367,16 @@ class LvmBlueStore(BlueStore):
         if is_encrypted:
             osd_lv_path = '/dev/mapper/%s' % osd_block_lv.__dict__['lv_uuid']
             lockbox_secret = osd_block_lv.tags['ceph.cephx_lockbox_secret']
-            encryption_utils.write_lockbox_keyring(osd_id,
-                                                   osd_fsid,
-                                                   lockbox_secret)
-            dmcrypt_secret = encryption_utils.get_dmcrypt_key(osd_id, osd_fsid)
+            self.with_tpm = bool(osd_block_lv.tags.get('ceph.with_tpm', 0))
+            if not self.with_tpm:
+                encryption_utils.write_lockbox_keyring(osd_id,
+                                                       osd_fsid,
+                                                       lockbox_secret)
+                dmcrypt_secret = encryption_utils.get_dmcrypt_key(osd_id, osd_fsid)
             encryption_utils.luks_open(dmcrypt_secret,
                                        osd_block_lv.__dict__['lv_path'],
-                                       osd_block_lv.__dict__['lv_uuid'])
+                                       osd_block_lv.__dict__['lv_uuid'],
+                                       with_tpm=self.with_tpm)
         else:
             osd_lv_path = osd_block_lv.__dict__['lv_path']
 
index 5ac16617e50febbea1eaa68f557e51d98411d692..859dfb741c654baaac0e8a067bc80e66923f1b67 100644 (file)
@@ -19,19 +19,19 @@ logger = logging.getLogger(__name__)
 class RawBlueStore(BlueStore):
     def __init__(self, args: "argparse.Namespace") -> None:
         super().__init__(args)
-        if hasattr(self.args, 'data'):
-            self.block_device_path = self.args.data
-        if hasattr(self.args, 'block_db'):
-            self.db_device_path = self.args.block_db
-        if hasattr(self.args, 'block_wal'):
-            self.wal_device_path = self.args.block_wal
+        self.method = 'raw'
+        self.devices: List[str] = getattr(args, 'devices', [])
+        self.osd_id = getattr(self.args, 'osd_id', '')
+        self.osd_fsid = getattr(self.args, 'osd_fsid', '')
+        self.block_device_path = getattr(self.args, 'data', '')
+        self.db_device_path = getattr(self.args, 'block_db', '')
+        self.wal_device_path = getattr(self.args, 'block_wal', '')
 
     def prepare_dmcrypt(self) -> None:
         """
         Helper for devices that are encrypted. The operations needed for
         block, db, wal, devices are all the same
         """
-        key = self.secrets['dmcrypt_key']
 
         for device, device_type in [(self.block_device_path, 'block'),
                                     (self.db_device_path, 'db'),
@@ -44,16 +44,19 @@ class RawBlueStore(BlueStore):
                                                          device_type)
                 # format data device
                 encryption_utils.luks_format(
-                    key,
+                    self.dmcrypt_key,
                     device
                 )
+                if self.with_tpm:
+                    self.enroll_tpm2(device)
                 encryption_utils.luks_open(
-                    key,
+                    self.dmcrypt_key,
                     device,
-                    mapping
+                    mapping,
+                    self.with_tpm
                 )
                 self.__dict__[f'{device_type}_device_path'] = \
-                    '/dev/mapper/{}'.format(mapping)
+                    '/dev/mapper/{}'.format(mapping)  # TODO(guits): need to preserve path or find a way to get the parent device from the mapper ?
 
     def safe_prepare(self,
                      args: Optional["argparse.Namespace"] = None) -> None:
@@ -80,25 +83,21 @@ class RawBlueStore(BlueStore):
 
     @decorators.needs_root
     def prepare(self) -> None:
-        if self.encrypted:
-            self.secrets['dmcrypt_key'] = \
-                os.getenv('CEPH_VOLUME_DMCRYPT_SECRET')
         self.osd_fsid = system.generate_uuid()
         crush_device_class = self.args.crush_device_class
+        if self.encrypted and not self.with_tpm:
+            self.dmcrypt_key = os.getenv('CEPH_VOLUME_DMCRYPT_SECRET', '')
+            self.secrets['dmcrypt_key'] = self.dmcrypt_key
         if crush_device_class:
             self.secrets['crush_device_class'] = crush_device_class
 
         tmpfs = not self.args.no_tmpfs
-        if self.args.block_wal:
-            self.wal = self.args.block_wal
-        if self.args.block_db:
-            self.db = self.args.block_db
 
         # reuse a given ID if it exists, otherwise create a new ID
         self.osd_id = prepare_utils.create_id(
             self.osd_fsid, json.dumps(self.secrets))
 
-        if self.secrets.get('dmcrypt_key'):
+        if self.encrypted:
             self.prepare_dmcrypt()
 
         self.prepare_osd_req(tmpfs=tmpfs)
@@ -106,18 +105,12 @@ class RawBlueStore(BlueStore):
         # prepare the osd filesystem
         self.osd_mkfs()
 
-    def _activate(self,
-                  meta: Dict[str, Any],
-                  tmpfs: bool) -> None:
-        # find the osd
-        osd_id = meta['osd_id']
-        osd_uuid = meta['osd_uuid']
-
+    def _activate(self, osd_id: str, osd_fsid: str) -> None:
         # mount on tmpfs the osd directory
         self.osd_path = '/var/lib/ceph/osd/%s-%s' % (conf.cluster, osd_id)
         if not system.path_is_mounted(self.osd_path):
             # mkdir -p and mount as tmpfs
-            prepare_utils.create_osd_path(osd_id, tmpfs=tmpfs)
+            prepare_utils.create_osd_path(osd_id, tmpfs=not self.args.no_tmpfs)
 
         # XXX This needs to be removed once ceph-bluestore-tool can deal with
         # symlinks that exist in the osd dir
@@ -133,49 +126,98 @@ class RawBlueStore(BlueStore):
             'prime-osd-dir',
             '--path', self.osd_path,
             '--no-mon-config',
-            '--dev', meta['device'],
+            '--dev', self.block_device_path,
         ]
         process.run(prime_command)
 
         # always re-do the symlink regardless if it exists, so that the block,
         # block.wal, and block.db devices that may have changed can be mapped
         # correctly every time
-        prepare_utils.link_block(meta['device'], osd_id)
+        prepare_utils.link_block(self.block_device_path, osd_id)
 
-        if 'device_db' in meta:
-            prepare_utils.link_db(meta['device_db'], osd_id, osd_uuid)
+        if self.db_device_path:
+            prepare_utils.link_db(self.db_device_path, osd_id, osd_fsid)
 
-        if 'device_wal' in meta:
-            prepare_utils.link_wal(meta['device_wal'], osd_id, osd_uuid)
+        if self.wal_device_path:
+            prepare_utils.link_wal(self.wal_device_path, osd_id, osd_fsid)
 
         system.chown(self.osd_path)
         terminal.success("ceph-volume raw activate "
                          "successful for osd ID: %s" % osd_id)
 
     @decorators.needs_root
-    def activate(self,
-                 devs: List[str],
-                 start_osd_id: str,
-                 start_osd_uuid: str,
-                 tmpfs: bool) -> None:
-        """
-        :param args: The parsed arguments coming from the CLI
+    def activate(self) -> None:
+        """Activate Ceph OSDs on the system.
+
+        This function activates Ceph Object Storage Daemons (OSDs) on the system.
+        It iterates over all block devices, checking if they have a LUKS2 signature and
+        are encrypted for Ceph. If a device's OSD fsid matches and it is enrolled with TPM2,
+        the function pre-activates it. After collecting the relevant devices, it attempts to
+        activate any OSDs found.
+
+        Raises:
+            RuntimeError: If no matching OSDs are found to activate.
         """
-        assert devs or start_osd_id or start_osd_uuid
-        found = direct_report(devs)
+        assert self.devices or self.osd_id or self.osd_fsid
+
+        activated_any: bool = False
+
+        for d in disk.lsblk_all(abspath=True):
+            device: str = d.get('NAME')
+            luks2 = encryption_utils.CephLuks2(device)
+            if luks2.is_ceph_encrypted:
+                if luks2.is_tpm2_enrolled and self.osd_fsid == luks2.osd_fsid:
+                    self.pre_activate_tpm2(device)
+        found = direct_report(self.devices)
 
-        activated_any = False
         for osd_uuid, meta in found.items():
             osd_id = meta['osd_id']
-            if start_osd_id is not None and str(osd_id) != str(start_osd_id):
+            if self.osd_id is not None and str(osd_id) != str(self.osd_id):
                 continue
-            if start_osd_uuid is not None and osd_uuid != start_osd_uuid:
+            if self.osd_fsid is not None and osd_uuid != self.osd_fsid:
                 continue
-            logger.info('Activating osd.%s uuid %s cluster %s' % (
-                osd_id, osd_uuid, meta['ceph_fsid']))
-            self._activate(meta,
-                           tmpfs=tmpfs)
+            self.block_device_path = meta.get('device')
+            self.db_device_path = meta.get('device_db', '')
+            self.wal_device_path = meta.get('device_wal', '')
+            logger.info(f'Activating osd.{osd_id} uuid {osd_uuid} cluster {meta["ceph_fsid"]}')
+            self._activate(osd_id, osd_uuid)
             activated_any = True
 
         if not activated_any:
             raise RuntimeError('did not find any matching OSD to activate')
+
+    def pre_activate_tpm2(self, device: str) -> None:
+        """Pre-activate a TPM2-encrypted device for Ceph.
+
+        This function pre-activates a TPM2-encrypted device for Ceph by opening the
+        LUKS encryption, checking the BlueStore header, and renaming the device
+        mapper according to the BlueStore mapping type.
+
+        Args:
+            device (str): The path to the device to be pre-activated.
+
+        Raises:
+            RuntimeError: If the device does not have a BlueStore signature.
+        """
+        bs_mapping_type: Dict[str, str] = {'bluefs db': 'db',
+                                           'bluefs wal': 'wal',
+                                           'main': 'block'}
+        self.with_tpm = 1
+        self.temp_mapper: str = f'activating-{os.path.basename(device)}'
+        self.temp_mapper_path: str = f'/dev/mapper/{self.temp_mapper}'
+        encryption_utils.luks_open(
+            '',
+            device,
+            self.temp_mapper,
+            self.with_tpm
+        )
+        bluestore_header: Dict[str, Any] = disk.get_bluestore_header(self.temp_mapper_path)
+        if not bluestore_header:
+            raise RuntimeError(f"{device} doesn't have BlueStore signature.")
+
+        kname: str = disk.get_parent_device_from_mapper(self.temp_mapper_path, abspath=False)
+        device_type = bs_mapping_type[bluestore_header[self.temp_mapper_path]['description']]
+        new_mapper: str = f'ceph-{self.osd_fsid}-{kname}-{device_type}-dmcrypt'
+        self.block_device_path = f'/dev/mapper/{new_mapper}'
+        self.devices.append(self.block_device_path)
+        encryption_utils.rename_mapper(self.temp_mapper, new_mapper)
index fb43da7ab222d4c829ad69cb7462ebaf979ab7dc..ee58081d97da14a6897009d95ee8e37244f3a771 100644 (file)
@@ -1,11 +1,13 @@
 import os
 import pytest
-from mock.mock import patch, PropertyMock, create_autospec
+from mock.mock import patch, PropertyMock, create_autospec, Mock
 from ceph_volume.api import lvm
 from ceph_volume.util import disk
 from ceph_volume.util import device
 from ceph_volume.util.constants import ceph_disk_guids
 from ceph_volume import conf, configuration, objectstore
+from ceph_volume.objectstore.rawbluestore import RawBlueStore
+from typing import Any, Dict, List, Optional, Callable
 
 
 class Capture(object):
@@ -494,6 +496,14 @@ raw_direct_report_data = {
         "osd_id": 9,
         "osd_uuid": "a0e07c5b-bee1-4ea2-ae07-cb89deda9b27",
         "type": "bluestore"
+    },
+    "db32a338-b640-4cbc-af17-f63808b1c36e": {
+        "ceph_fsid": "c301d0aa-288d-11ef-b535-c84bd6975560",
+        "device": "/dev/mapper/ceph-db32a338-b640-4cbc-af17-f63808b1c36e-sdb-block-dmcrypt",
+        "device_db": "/dev/mapper/ceph-db32a338-b640-4cbc-af17-f63808b1c36e-sdc-db-dmcrypt",
+        "osd_id": 0,
+        "osd_uuid": "db32a338-b640-4cbc-af17-f63808b1c36e",
+        "type": "bluestore"
     }
 }
 
@@ -503,4 +513,21 @@ def mock_lvm_direct_report(monkeypatch):
 
 @pytest.fixture
 def mock_raw_direct_report(monkeypatch):
-    monkeypatch.setattr('ceph_volume.objectstore.rawbluestore.direct_report', lambda x: raw_direct_report_data)
\ No newline at end of file
+    monkeypatch.setattr('ceph_volume.objectstore.rawbluestore.direct_report', lambda x: raw_direct_report_data)
+
+@pytest.fixture
+def fake_lsblk_all(monkeypatch: Any) -> Callable:
+    def apply(data: Optional[List[Dict[str, Any]]] = None) -> None:
+        if data is None:
+            devices = []
+        else:
+            devices = data
+        monkeypatch.setattr("ceph_volume.util.device.disk.lsblk_all", lambda *a, **kw: devices)
+    return apply
+
+@pytest.fixture
+def rawbluestore(factory: type[Factory]) -> RawBlueStore:
+    args = factory(devices=['/dev/foo'])
+    with patch('ceph_volume.objectstore.rawbluestore.prepare_utils.create_key', Mock(return_value=['AQCee6ZkzhOrJRAAZWSvNC3KdXOpC2w8ly4AZQ=='])):
+        r = RawBlueStore(args)  # type: ignore
+        return r
index ec301d6eb939a6b960964068607da01589c6312e..c2e909d0146a6856aa7953f8fb84bececcf2c878 100644 (file)
@@ -78,6 +78,7 @@ class TestPrepare(object):
         with pytest.raises(RuntimeError) as error:
             self.p.args = Mock()
             self.p.args.data = '/dev/sdfoo'
+            self.p.args.with_tpm = '0'
             self.p.get_lv = Mock()
             self.p.objectstore = objectstore.lvmbluestore.LvmBlueStore(args=self.p.args)
             self.p.objectstore.safe_prepare()
index 285bc8b5cdf9de41bcc6aea9ed16d8da87f9531e..ac0b1c4fb161d84601cd48cc068ce23a62988d33 100644 (file)
@@ -71,8 +71,8 @@ class TestPrepare(object):
                     osd_fsid='123',
                     secrets=dict(dmcrypt_key='foo'))
         self.p.objectstore.prepare_dmcrypt()
-        m_luks_open.assert_called_with('foo', '/dev/foo', 'ceph-123-foo-block-dmcrypt')
-        m_luks_format.assert_called_with('foo', '/dev/foo')
+        m_luks_open.assert_called_with(self.p.objectstore.dmcrypt_key, '/dev/foo', 'ceph-123-foo-block-dmcrypt', 0)
+        m_luks_format.assert_called_with(self.p.objectstore.dmcrypt_key, '/dev/foo')
         assert self.p.objectstore.__dict__['block_device_path'] == '/dev/mapper/ceph-123-foo-block-dmcrypt'
 
     @patch('ceph_volume.util.encryption.luks_open')
@@ -86,8 +86,8 @@ class TestPrepare(object):
                     osd_fsid='456',
                     secrets=dict(dmcrypt_key='foo'))
         self.p.objectstore.prepare_dmcrypt()
-        m_luks_open.assert_called_with('foo', '/dev/db-foo', 'ceph-456-foo-db-dmcrypt')
-        m_luks_format.assert_called_with('foo', '/dev/db-foo')
+        m_luks_open.assert_called_with(self.p.objectstore.dmcrypt_key, '/dev/db-foo', 'ceph-456-foo-db-dmcrypt', 0)
+        m_luks_format.assert_called_with(self.p.objectstore.dmcrypt_key, '/dev/db-foo')
         assert self.p.objectstore.__dict__['db_device_path'] == '/dev/mapper/ceph-456-foo-db-dmcrypt'
 
     @patch('ceph_volume.util.encryption.luks_open')
@@ -101,8 +101,8 @@ class TestPrepare(object):
                     osd_fsid='789',
                     secrets=dict(dmcrypt_key='foo'))
         self.p.objectstore.prepare_dmcrypt()
-        m_luks_open.assert_called_with('foo', '/dev/wal-foo', 'ceph-789-foo-wal-dmcrypt')
-        m_luks_format.assert_called_with('foo', '/dev/wal-foo')
+        m_luks_open.assert_called_with(self.p.objectstore.dmcrypt_key, '/dev/wal-foo', 'ceph-789-foo-wal-dmcrypt', 0)
+        m_luks_format.assert_called_with(self.p.objectstore.dmcrypt_key, '/dev/wal-foo')
         assert self.p.objectstore.__dict__['wal_device_path'] == '/dev/mapper/ceph-789-foo-wal-dmcrypt'
 
     @patch('ceph_volume.objectstore.rawbluestore.rollback_osd')
index 346e5f455632ec1b8e5336aec738f49e6d215086..9c298640e6b49093a3e05c5a860e07253d584cb5 100644 (file)
@@ -13,7 +13,6 @@ class TestLvmBlueStore:
     @patch('ceph_volume.conf.cluster', 'ceph')
     @patch('ceph_volume.api.lvm.get_single_lv')
     @patch('ceph_volume.objectstore.lvmbluestore.prepare_utils.create_id', Mock(return_value='111'))
-    @patch('ceph_volume.objectstore.lvmbluestore.encryption_utils.create_dmcrypt_key', Mock(return_value='fake-dmcrypt-key'))
     def test_pre_prepare_lv(self, m_get_single_lv, factory):
         args = factory(cluster_fsid='abcd',
                        osd_fsid='abc123',
@@ -26,6 +25,7 @@ class TestLvmBlueStore:
                                               lv_tags='',
                                               lv_uuid='fake-uuid')
         self.lvm_bs.encrypted = True
+        self.lvm_bs.dmcrypt_key = 'fake-dmcrypt-key'
         self.lvm_bs.args = args
         self.lvm_bs.pre_prepare()
         assert self.lvm_bs.secrets['dmcrypt_key'] == 'fake-dmcrypt-key'
@@ -42,22 +42,62 @@ class TestLvmBlueStore:
                                     'ceph.block_uuid': 'fake-uuid',
                                     'ceph.cephx_lockbox_secret': '',
                                     'ceph.encrypted': True,
-                                    'ceph.vdo': '0'}
+                                    'ceph.vdo': '0',
+                                    'ceph.with_tpm': 0}
+
+    @patch('ceph_volume.conf.cluster', 'ceph')
+    @patch('ceph_volume.api.lvm.get_single_lv')
+    @patch('ceph_volume.objectstore.lvmbluestore.prepare_utils.create_id', Mock(return_value='111'))
+    def test_pre_prepare_lv_with_dmcrypt_and_tpm(self, m_get_single_lv, factory):
+        args = factory(cluster_fsid='abcd',
+                       osd_fsid='abc123',
+                       crush_device_class='ssd',
+                       osd_id='111',
+                       data='vg_foo/lv_foo',
+                       dmcrypt=True,
+                       with_tpm=True)
+        m_get_single_lv.return_value = Volume(lv_name='lv_foo',
+                                              lv_path='/fake-path',
+                                              vg_name='vg_foo',
+                                              lv_tags='',
+                                              lv_uuid='fake-uuid')
+        self.lvm_bs.encrypted = True
+        self.lvm_bs.with_tpm = True
+        self.lvm_bs.dmcrypt_key = 'fake-dmcrypt-key-tpm2'
+        self.lvm_bs.args = args
+        self.lvm_bs.pre_prepare()
+        assert 'dmcrypt_key' not in self.lvm_bs.secrets.keys()
+        assert self.lvm_bs.secrets['crush_device_class'] == 'ssd'
+        assert self.lvm_bs.osd_id == '111'
+        assert self.lvm_bs.block_device_path == '/fake-path'
+        assert self.lvm_bs.tags == {'ceph.osd_fsid': 'abc123',
+                                    'ceph.osd_id': '111',
+                                    'ceph.cluster_fsid': 'abcd',
+                                    'ceph.cluster_name': 'ceph',
+                                    'ceph.crush_device_class': 'ssd',
+                                    'ceph.osdspec_affinity': '',
+                                    'ceph.block_device': '/fake-path',
+                                    'ceph.block_uuid': 'fake-uuid',
+                                    'ceph.cephx_lockbox_secret': '',
+                                    'ceph.encrypted': True,
+                                    'ceph.vdo': '0',
+                                    'ceph.with_tpm': 1}
 
     @patch('ceph_volume.objectstore.lvmbluestore.prepare_utils.create_id', Mock(return_value='111'))
-    @patch('ceph_volume.objectstore.lvmbluestore.encryption_utils.create_dmcrypt_key', Mock(return_value='fake-dmcrypt-key'))
     def test_pre_prepare_no_lv(self, factory):
         args = factory(cluster_fsid='abcd',
                        osd_fsid='abc123',
                        crush_device_class='ssd',
                        osd_id='111',
-                       data='/dev/foo')
+                       data='/dev/foo',
+                       dmcrypt_key='fake-dmcrypt-key')
         self.lvm_bs.prepare_data_device = lambda x, y: Volume(lv_name='lv_foo',
                                                               lv_path='/fake-path',
                                                               vg_name='vg_foo',
                                                               lv_tags='',
                                                               lv_uuid='fake-uuid')
         self.lvm_bs.encrypted = True
+        self.lvm_bs.dmcrypt_key = 'fake-dmcrypt-key'
         self.lvm_bs.args = args
         self.lvm_bs.pre_prepare()
         assert self.lvm_bs.secrets['dmcrypt_key'] == 'fake-dmcrypt-key'
@@ -74,7 +114,8 @@ class TestLvmBlueStore:
                                     'ceph.block_uuid': 'fake-uuid',
                                     'ceph.cephx_lockbox_secret': '',
                                     'ceph.encrypted': True,
-                                    'ceph.vdo': '0'}
+                                    'ceph.vdo': '0',
+                                    'ceph.with_tpm': 0}
 
     @patch('ceph_volume.util.disk.is_partition', Mock(return_value=True))
     @patch('ceph_volume.api.lvm.create_lv')
@@ -165,6 +206,7 @@ class TestLvmBlueStore:
                        block_db_size=123,
                        block_wal_slots=1,
                        block_db_slots=1,
+                       with_tpm=False
                        )
         self.lvm_bs.args = args
         self.lvm_bs.pre_prepare = lambda: None
@@ -177,7 +219,7 @@ class TestLvmBlueStore:
         assert self.lvm_bs.wal_device_path == '/dev/foo1'
         assert self.lvm_bs.db_device_path == '/dev/foo2'
         assert self.lvm_bs.block_lv.set_tags.mock_calls == [call({'ceph.type': 'block', 'ceph.vdo': '0', 'ceph.wal_uuid': 'c6798f59-01', 'ceph.wal_device': '/dev/foo1', 'ceph.db_uuid': 'c6798f59-01', 'ceph.db_device': '/dev/foo2'})]
-        assert self.lvm_bs.prepare_dmcrypt.called
+        assert not self.lvm_bs.prepare_dmcrypt.called
         assert self.lvm_bs.osd_mkfs.called
         assert self.lvm_bs.prepare_osd_req.called
 
@@ -185,8 +227,12 @@ class TestLvmBlueStore:
         self.lvm_bs.secrets = {'dmcrypt_key': 'fake-secret'}
         self.lvm_bs.tags = {'ceph.block_uuid': 'block-uuid1',
                             'ceph.db_uuid': 'db-uuid2',
-                            'ceph.wal_uuid': 'wal-uuid3'}
-        self.lvm_bs.luks_format_and_open = lambda *a: f'/dev/mapper/{a[3]["ceph."+a[2]+"_uuid"]}'
+                            'ceph.wal_uuid': 'wal-uuid3',
+                            'ceph.with_tpm': 0}
+        self.lvm_bs.block_device_path = '/dev/sdb'
+        self.lvm_bs.db_device_path = '/dev/sdc'
+        self.lvm_bs.wal_device_path = '/dev/sdb'
+        self.lvm_bs.luks_format_and_open = lambda *a: f'/dev/mapper/{a[2]["ceph."+a[1]+"_uuid"]}'
         self.lvm_bs.prepare_dmcrypt()
         assert self.lvm_bs.block_device_path == '/dev/mapper/block-uuid1'
         assert self.lvm_bs.db_device_path == '/dev/mapper/db-uuid2'
@@ -195,15 +241,24 @@ class TestLvmBlueStore:
     @patch('ceph_volume.objectstore.lvmbluestore.encryption_utils.luks_open')
     @patch('ceph_volume.objectstore.lvmbluestore.encryption_utils.luks_format')
     def test_luks_format_and_open(self, m_luks_format, m_luks_open):
-        result = self.lvm_bs.luks_format_and_open('key',
-                                                  '/dev/foo',
+        result = self.lvm_bs.luks_format_and_open('/dev/foo',
+                                                  'block',
+                                                  {'ceph.block_uuid': 'block-uuid1'})
+        assert result == '/dev/mapper/block-uuid1'
+
+    @patch('ceph_volume.objectstore.lvmbluestore.LvmBlueStore.enroll_tpm2', Mock(return_value=MagicMock()))
+    @patch('ceph_volume.objectstore.lvmbluestore.encryption_utils.luks_open')
+    @patch('ceph_volume.objectstore.lvmbluestore.encryption_utils.luks_format')
+    def test_luks_format_and_open_with_tpm(self, m_luks_format, m_luks_open):
+        self.lvm_bs.with_tpm = True
+        result = self.lvm_bs.luks_format_and_open('/dev/foo',
                                                   'block',
                                                   {'ceph.block_uuid': 'block-uuid1'})
         assert result == '/dev/mapper/block-uuid1'
+        self.lvm_bs.enroll_tpm2.assert_called_once()
 
     def test_luks_format_and_open_not_device(self):
-        result = self.lvm_bs.luks_format_and_open('key',
-                                                  '',
+        result = self.lvm_bs.luks_format_and_open('',
                                                   'block',
                                                   {})
         assert result == ''
index 204dcdb6f2fd6487436101afb89bf5e9d0901a51..f4f50b06f8a2972c50ae8c84b6490afca46891f6 100644 (file)
@@ -26,6 +26,26 @@ class TestRawBlueStore:
         assert self.raw_bs.db_device_path == "/dev/mapper/ceph--foo0-db-dmcrypt"
         assert self.raw_bs.wal_device_path == "/dev/mapper/ceph--foo0-wal-dmcrypt"
 
+    @patch('ceph_volume.objectstore.rawbluestore.RawBlueStore.enroll_tpm2', Mock(return_value=MagicMock()))
+    def test_prepare_dmcrypt_with_tpm(self,
+                                      device_info,
+                                      fake_call,
+                                      key_size):
+        self.raw_bs.block_device_path = '/dev/foo0'
+        self.raw_bs.db_device_path = '/dev/foo1'
+        self.raw_bs.wal_device_path = '/dev/foo2'
+        self.raw_bs.with_tpm = 1
+        lsblk = {"TYPE": "disk",
+                 "NAME": "foo0",
+                 'KNAME': 'foo0'}
+        device_info(lsblk=lsblk)
+        self.raw_bs.prepare_dmcrypt()
+        assert 'dmcrypt_key' not in self.raw_bs.secrets.keys()
+        assert self.raw_bs.block_device_path == "/dev/mapper/ceph--foo0-block-dmcrypt"
+        assert self.raw_bs.db_device_path == "/dev/mapper/ceph--foo0-db-dmcrypt"
+        assert self.raw_bs.wal_device_path == "/dev/mapper/ceph--foo0-wal-dmcrypt"
+        assert self.raw_bs.enroll_tpm2.mock_calls == [call('/dev/foo0'), call('/dev/foo1'), call('/dev/foo2')]
+
     @patch('ceph_volume.objectstore.rawbluestore.rollback_osd')
     @patch('ceph_volume.objectstore.rawbluestore.RawBlueStore.prepare')
     def test_safe_prepare_raises_exception(self,
@@ -48,13 +68,10 @@ class TestRawBlueStore:
                           capsys):
         args = factory(dmcrypt=True,
                        data='/dev/foo')
-        # self.raw_bs.args = args
         self.raw_bs.safe_prepare(args)
-        stdout, stderr = capsys.readouterr()
+        _, stderr = capsys.readouterr()
         assert "prepare successful for: /dev/foo" in stderr
 
-    # @patch('ceph_volume.objectstore.rawbluestore.prepare_utils.create_id')
-    # @patch('ceph_volume.objectstore.rawbluestore.system.generate_uuid', return_value='fake-uuid')
     @patch.dict('os.environ', {'CEPH_VOLUME_DMCRYPT_SECRET': 'dmcrypt-key'})
     @patch('ceph_volume.objectstore.rawbluestore.prepare_utils.create_id')
     @patch('ceph_volume.objectstore.rawbluestore.system.generate_uuid')
@@ -92,26 +109,27 @@ class TestRawBlueStore:
                        m_link_block,
                        m_link_db,
                        m_link_wal,
-                       monkeypatch):
-        meta = dict(osd_id='1',
-                    osd_uuid='fake-uuid',
-                    device='/dev/foo',
-                    device_db='/dev/foo1',
-                    device_wal='/dev/foo2')
+                       monkeypatch,
+                       factory):
+        args = factory(no_tmpfs=False)
+        self.raw_bs.args = args
+        self.raw_bs.block_device_path = '/dev/sda'
+        self.raw_bs.db_device_path = '/dev/sdb'
+        self.raw_bs.wal_device_path = '/dev/sdc'
         m_run.return_value = MagicMock()
         m_exists.side_effect = lambda path: True
         m_create_osd_path.return_value = MagicMock()
         m_unlink.return_value = MagicMock()
         monkeypatch.setattr(system, 'chown', lambda path: 0)
         monkeypatch.setattr(system, 'path_is_mounted', lambda path: 0)
-        self.raw_bs._activate(meta, True)
+        self.raw_bs._activate('1', True)
         calls = [call('/var/lib/ceph/osd/ceph-1/block'),
                  call('/var/lib/ceph/osd/ceph-1/block.db'),
                  call('/var/lib/ceph/osd/ceph-1/block.wal')]
         assert m_run.mock_calls == [call(['ceph-bluestore-tool',
                                          'prime-osd-dir',
                                          '--path', '/var/lib/ceph/osd/ceph-1',
-                                         '--no-mon-config', '--dev', '/dev/foo'])]
+                                         '--no-mon-config', '--dev', '/dev/sda'])]
         assert m_unlink.mock_calls == calls
         assert m_exists.mock_calls == calls
         assert m_create_osd_path.mock_calls == [call('1', tmpfs=True)]
@@ -120,40 +138,81 @@ class TestRawBlueStore:
                                        is_root,
                                        mock_raw_direct_report):
         with pytest.raises(RuntimeError) as error:
-            self.raw_bs.activate([],
-                                '123',
-                                'fake-uuid',
-                                True)
+            self.raw_bs.osd_id = '1'
+            self.raw_bs.activate()
         assert str(error.value) == 'did not find any matching OSD to activate'
 
-    def test_activate_osd_id(self,
-                             is_root,
-                             mock_raw_direct_report):
+    def test_activate_osd_id_and_fsid(self,
+                                      is_root,
+                                      mock_raw_direct_report):
         self.raw_bs._activate = MagicMock()
-        self.raw_bs.activate([],
-                            '8',
-                            '824f7edf-371f-4b75-9231-4ab62a32d5c0',
-                            True)
+        self.raw_bs.osd_id = '8'
+        self.raw_bs.osd_fsid = '824f7edf-371f-4b75-9231-4ab62a32d5c0'
+        self.raw_bs.activate()
         self.raw_bs._activate.mock_calls == [call({'ceph_fsid': '7dccab18-14cf-11ee-837b-5254008f8ca5',
                                                    'device': '/dev/mapper/ceph--40bc7bd7--4aee--483e--ba95--89a64bc8a4fd-osd--block--824f7edf--371f--4b75--9231--4ab62a32d5c0',
                                                    'device_db': '/dev/mapper/ceph--73d6d4db--6528--48f2--a4e2--1c82bc87a9ac-osd--db--b82d920d--be3c--4e4d--ba64--18f7e8445892',
                                                    'osd_id': 8,
                                                    'osd_uuid': '824f7edf-371f-4b75-9231-4ab62a32d5c0',
                                                    'type': 'bluestore'},
-                                                  tmpfs=True)]
+                                                   tmpfs=True)]
 
-    def test_activate_osd_fsid(self,
-                               is_root,
-                               mock_raw_direct_report):
-        self.raw_bs._activate = MagicMock()
-        with pytest.raises(RuntimeError):
-            self.raw_bs.activate([],
-                                '8',
-                                'a0e07c5b-bee1-4ea2-ae07-cb89deda9b27',
-                                True)
-        self.raw_bs._activate.mock_calls == [call({'ceph_fsid': '7dccab18-14cf-11ee-837b-5254008f8ca5',
-                                                   'device': '/dev/mapper/ceph--e34cc3f5--a70d--49df--82b3--46bcbd63d4b0-osd--block--a0e07c5b--bee1--4ea2--ae07--cb89deda9b27',
-                                                   'osd_id': 9,
-                                                   'osd_uuid': 'a0e07c5b-bee1-4ea2-ae07-cb89deda9b27',
-                                                   'type': 'bluestore'},
-                                                  tmpfs=True)]
\ No newline at end of file
+    @patch('ceph_volume.objectstore.rawbluestore.encryption_utils.rename_mapper', Mock(return_value=MagicMock()))
+    @patch('ceph_volume.util.disk.get_bluestore_header')
+    @patch('ceph_volume.objectstore.rawbluestore.encryption_utils.luks_open', Mock(return_value=MagicMock()))
+    def test_activate_dmcrypt_tpm(self, m_bs_header, rawbluestore, fake_lsblk_all, mock_raw_direct_report, is_root) -> None:
+        m_bs_header.return_value = {
+            "/dev/mapper/activating-sdb": {
+            "osd_uuid": "db32a338-b640-4cbc-af17-f63808b1c36e",
+            "size": 20000572178432,
+            "btime": "2024-06-13T12:16:57.607442+0000",
+            "description": "main",
+            "bfm_blocks": "4882952192",
+            "bfm_blocks_per_key": "128",
+            "bfm_bytes_per_block": "4096",
+            "bfm_size": "20000572178432",
+            "bluefs": "1",
+            "ceph_fsid": "c301d0aa-288d-11ef-b535-c84bd6975560",
+            "ceph_version_when_created": "ceph version 19.0.0-4242-gf2f7cc60 (f2f7cc609cdbae767486cf2fe6872a4789adffb2) squid (dev)",
+            "created_at": "2024-06-13T12:17:20.122565Z",
+            "elastic_shared_blobs": "1",
+            "kv_backend": "rocksdb",
+            "magic": "ceph osd volume v026",
+            "mkfs_done": "yes",
+            "osd_key": "AQAk42pmt7tqFxAAHlaETFm33yFtEuoQAh/cpQ==",
+            "ready": "ready",
+            "whoami": "0"}
+        }
+        mock_luks2_1 = Mock()
+        mock_luks2_1.is_ceph_encrypted = True
+        mock_luks2_1.is_tpm2_enrolled = True
+        mock_luks2_1.osd_fsid = 'db32a338-b640-4cbc-af17-f63808b1c36e'
+
+        mock_luks2_2 = Mock()
+        mock_luks2_2.is_ceph_encrypted = True
+        mock_luks2_2.is_tpm2_enrolled = False
+        mock_luks2_2.osd_fsid = 'db32a338-b640-4cbc-af17-f63808b1c36e'
+
+        mock_luks2_3 = Mock()
+        mock_luks2_3.is_ceph_encrypted = False
+        mock_luks2_3.is_tpm2_enrolled = False
+        mock_luks2_3.osd_fsid = ''
+
+        mock_luks2_4 = Mock()
+        mock_luks2_4.is_ceph_encrypted = True
+        mock_luks2_4.is_tpm2_enrolled = True
+        mock_luks2_4.osd_fsid = 'abcd'
+        with patch('ceph_volume.objectstore.rawbluestore.encryption_utils.CephLuks2', side_effect=[mock_luks2_1,
+                                                                                                   mock_luks2_2,
+                                                                                                   mock_luks2_3,
+                                                                                                   mock_luks2_4]):
+            fake_lsblk_all([{'NAME': '/dev/sdb', 'FSTYPE': 'crypto_LUKS'},
+                            {'NAME': '/dev/sdc', 'FSTYPE': 'crypto_LUKS'},
+                            {'NAME': '/dev/sdd', 'FSTYPE': ''}])
+            rawbluestore.osd_fsid = 'db32a338-b640-4cbc-af17-f63808b1c36e'
+            rawbluestore.osd_id = '0'
+            rawbluestore._activate = MagicMock()
+            rawbluestore.activate()
+            assert rawbluestore._activate.mock_calls == [call(0, 'db32a338-b640-4cbc-af17-f63808b1c36e')]
+            assert rawbluestore.block_device_path == '/dev/mapper/ceph-db32a338-b640-4cbc-af17-f63808b1c36e-sdb-block-dmcrypt'
+            assert rawbluestore.db_device_path == '/dev/mapper/ceph-db32a338-b640-4cbc-af17-f63808b1c36e-sdc-db-dmcrypt'
diff --git a/src/ceph-volume/ceph_volume/tests/test_ceph_volume.py b/src/ceph-volume/ceph_volume/tests/test_ceph_volume.py
new file mode 100644 (file)
index 0000000..0336e2c
--- /dev/null
@@ -0,0 +1,26 @@
+import os
+from ceph_volume import AllowLoopDevices, allow_loop_devices
+from typing import Any
+
+
+class TestAllowLoopDevsWarning:
+    def setup_method(self) -> None:
+        AllowLoopDevices.allow = False
+        AllowLoopDevices.warned = False
+        self.teardown_method()
+
+    def teardown_method(self) -> None:
+        AllowLoopDevices.allow = False
+        AllowLoopDevices.warned = False
+        if os.environ.get('CEPH_VOLUME_ALLOW_LOOP_DEVICES'):
+            os.environ.pop('CEPH_VOLUME_ALLOW_LOOP_DEVICES')
+
+    def test_loop_dev_warning(self, fake_call: Any, caplog: Any) -> None:
+        AllowLoopDevices.warned = False
+        assert allow_loop_devices() is False
+        assert not caplog.records
+        os.environ['CEPH_VOLUME_ALLOW_LOOP_DEVICES'] = "y"
+        assert allow_loop_devices() is True
+        log = caplog.records[0]
+        assert log.levelname == "WARNING"
+        assert "will never be supported in production" in log.message
index 94f3d3566e624b24dc1bbb484187c3e0a14f896c..7a4727a9aa7568e5752d4ec528eed6ba867a82ea 100644 (file)
@@ -1,4 +1,3 @@
-import os
 import pytest
 from ceph_volume.util import disk
 from mock.mock import patch, MagicMock
@@ -551,24 +550,6 @@ class TestSizeSpecificFormatting(object):
         assert result == "1027.00 TB"
 
 
-class TestAllowLoopDevsWarning(object):
-    def setup_method(self):
-        disk.AllowLoopDevices.allow = False
-        disk.AllowLoopDevices.warned = False
-        if os.environ.get('CEPH_VOLUME_ALLOW_LOOP_DEVICES'):
-            os.environ.pop('CEPH_VOLUME_ALLOW_LOOP_DEVICES')
-
-    def test_loop_dev_warning(self, fake_call, caplog):
-        disk.AllowLoopDevices.warned = False
-        assert disk.allow_loop_devices() is False
-        assert not caplog.records
-        os.environ['CEPH_VOLUME_ALLOW_LOOP_DEVICES'] = "y"
-        assert disk.allow_loop_devices() is True
-        log = caplog.records[0]
-        assert log.levelname == "WARNING"
-        assert "will never be supported in production" in log.message
-
-
 class TestHasBlueStoreLabel(object):
     def test_device_path_is_a_path(self, fake_filesystem):
         device_path = '/var/lib/ceph/osd/ceph-0'
index 745230659afb845fed0d9d3871455796258bf0d5..553193adf6a92a7a08540a3e3def07d64d2be761 100644 (file)
@@ -1,7 +1,9 @@
 from ceph_volume.util import encryption
-from mock.mock import patch, Mock
+from mock.mock import call, patch, Mock, MagicMock
+from typing import Any
 import base64
 import pytest
+import json
 
 
 class TestNoWorkqueue:
@@ -176,3 +178,113 @@ class TestLuksOpen(object):
         ]
         encryption.luks_open('abcd', '/dev/foo', '/dev/bar')
         assert m_call.call_args[0][0] == expected
+
+
+class TestCephLuks2:
+    @patch.object(encryption.CephLuks2, 'get_osd_fsid', Mock(return_value='abcd-1234'))
+    @patch.object(encryption.CephLuks2, 'is_ceph_encrypted', Mock(return_value=True))
+    def test_init_ceph_encrypted(self) -> None:
+        assert encryption.CephLuks2('/dev/foo').osd_fsid == 'abcd-1234'
+
+    @patch.object(encryption.CephLuks2, 'get_osd_fsid', Mock(return_value=''))
+    @patch.object(encryption.CephLuks2, 'is_ceph_encrypted', Mock(return_value=False))
+    def test_init_not_ceph_encrypted(self) -> None:
+        assert encryption.CephLuks2('/dev/foo').osd_fsid == ''
+
+    def test_has_luks2_signature(self) -> None:
+        with patch('ceph_volume.util.encryption._dd_read', return_value='LUKS'):
+            assert encryption.CephLuks2('/dev/foo').has_luks2_signature
+
+    @patch('ceph_volume.util.encryption._dd_read', side_effect=Exception('foo'))
+    def test_has_luks2_signature_raises_exception(self, m_dd_read: Any) -> None:
+        with pytest.raises(RuntimeError):
+            encryption.CephLuks2('/dev/foo').has_luks2_signature
+
+    @patch.object(encryption.CephLuks2, 'get_subsystem', Mock(return_value='ceph_fsid=abcd'))
+    @patch.object(encryption.CephLuks2, 'has_luks2_signature', Mock(return_value=True))
+    def test_is_ceph_encrypted(self) -> None:
+        assert encryption.CephLuks2('/dev/foo').is_ceph_encrypted
+
+    @patch.object(encryption.CephLuks2, 'get_label', Mock(return_value=''))
+    @patch.object(encryption.CephLuks2, 'has_luks2_signature', Mock(return_value=True))
+    def test_is_not_ceph_encrypted(self) -> None:
+        assert not encryption.CephLuks2('/dev/foo').is_ceph_encrypted
+
+    @patch('ceph_volume.util.encryption.process.call', Mock(return_value=MagicMock()))
+    def test_config_luks2_invalid_config(self) -> None:
+        with pytest.raises(RuntimeError):
+            encryption.CephLuks2('/dev/foo').config_luks2({'subsystem': 'ceph_fsid=1234-abcd', 'label': 'foo', 'foo': 'bar'})
+
+    @patch('ceph_volume.util.encryption.process.call', Mock(return_value=MagicMock()))
+    def test_config_luks2_invalid_config_keys(self) -> None:
+        with pytest.raises(RuntimeError):
+            encryption.CephLuks2('/dev/foo').config_luks2({'fake': 'fake-value', 'subsystem': 'ceph_fsid=1234-abcd'})
+
+    @patch('ceph_volume.util.encryption.process.call')
+    def test_config_luks2_ok(self, m_call: Any) -> None:
+        m_call.return_value = ('', '', 0)
+        encryption.CephLuks2('/dev/foo').config_luks2({'label': 'foo', 'subsystem': 'ceph_fsid=1234-abcd'})
+        assert m_call.mock_calls == [call(['cryptsetup', 'config', '/dev/foo', '--label', 'foo', '--subsystem', 'ceph_fsid=1234-abcd'], verbose_on_failure=False)]
+
+    @patch('ceph_volume.util.encryption.process.call')
+    def test_config_luks2_raises_exception(self, m_call: Any) -> None:
+        m_call.return_value = ('', '', 1)
+        with pytest.raises(RuntimeError):
+            encryption.CephLuks2('/dev/foo').config_luks2({'label': 'foo', 'subsystem': 'ceph_fsid=1234-abcd'})
+
+    def test_get_label(self) -> None:
+        with patch('ceph_volume.util.encryption._dd_read', return_value='fake-luks2-label'):
+            label: str = encryption.CephLuks2('/dev/foo').get_label()
+            assert label == 'fake-luks2-label'
+
+    def test_get_label_raises_exception(self) -> None:
+        with patch('ceph_volume.util.encryption._dd_read', side_effect=Exception('fake-error')):
+            with pytest.raises(RuntimeError):
+                encryption.CephLuks2('/dev/foo').get_label()
+
+    @patch.object(encryption.CephLuks2, 'get_subsystem', Mock(return_value='ceph_fsid=abcd'))
+    def test_get_osd_fsid(self) -> None:
+        assert encryption.CephLuks2('/dev/foo').get_osd_fsid() == 'abcd'
+
+    @patch.object(encryption.CephLuks2, 'get_label', Mock(return_value='ceph'))
+    def test_get_osd_fsid_error(self) -> None:
+        result: str = encryption.CephLuks2('/dev/foo').get_osd_fsid()
+        assert result == ''
+
+    def test_get_subsystem(self) -> None:
+        with patch('ceph_volume.util.encryption._dd_read', return_value='fake-luks2-subsystem'):
+            assert encryption.CephLuks2('/dev/foo').get_subsystem() == 'fake-luks2-subsystem'
+
+    def test_get_subsystem_raises_exception(self) -> None:
+        with patch('ceph_volume.util.encryption._dd_read', side_effect=Exception('fake-error')):
+            with pytest.raises(RuntimeError):
+                encryption.CephLuks2('/dev/foo').get_subsystem()
+
+    def test_get_json_area(self) -> None:
+        mock_json_data = '{"tokens": {"1": {"type": "systemd-tpm2"}}}'
+        with patch('ceph_volume.util.encryption._dd_read', return_value=mock_json_data):
+            assert encryption.CephLuks2('/dev/foo').get_json_area() == json.loads(mock_json_data)
+
+    def test_get_json_area_invalid(self) -> None:
+        with patch('ceph_volume.util.encryption._dd_read', return_value='invalid-json-data'):
+            with pytest.raises(RuntimeError):
+                encryption.CephLuks2('/dev/foo').get_json_area()
+
+    def test_get_json_area_exception_caught(self) -> None:
+        with patch('ceph_volume.util.encryption._dd_read', side_effect=OSError):
+            with pytest.raises(OSError):
+                encryption.CephLuks2('/dev/foo').get_json_area()
+
+    @patch('ceph_volume.util.encryption.lsblk', Mock(return_value={'FSTYPE': 'crypto_LUKS'}))
+    @patch.object(encryption.CephLuks2, 'get_json_area', Mock(return_value={"tokens": {"1": {"type": "systemd-tpm2"}}}))
+    def test_is_tpm2_enrolled_true(self) -> None:
+        assert encryption.CephLuks2('/dev/foo').is_tpm2_enrolled
+
+    @patch('ceph_volume.util.encryption.lsblk', Mock(return_value={'FSTYPE': 'whatever'}))
+    def test_is_tpm2_enrolled_false_not_a_luks_device(self) -> None:
+        assert not encryption.CephLuks2('/dev/foo').is_tpm2_enrolled
+
+    @patch('ceph_volume.util.encryption.lsblk', Mock(return_value={'FSTYPE': 'crypto_LUKS'}))
+    @patch.object(encryption.CephLuks2, 'get_json_area', Mock(return_value={"whatever": "fake-value"}))
+    def test_is_tpm2_enrolled_false_not_enrolled_with_tpm2(self) -> None:
+        assert not encryption.CephLuks2('/dev/foo').is_tpm2_enrolled
index 4bda56581c46484ea7d3e20dd6e6d2c1c3cbfa6b..d1f53bdddc78c93f3470d190e6c0b3b1ebbac15d 100644 (file)
@@ -130,28 +130,35 @@ class TestOsdMkfsBluestore(object):
         o.osd_mkfs()
         assert '--keyfile' in fake_call.calls[2]['args'][0]
 
-    def test_keyring_is_not_added(self, fake_call, monkeypatch):
+    def test_keyring_is_not_added(self, fake_call, monkeypatch, factory):
+        args = factory(dmcrypt=False)
         monkeypatch.setattr(system, 'chown', lambda path: True)
         o = objectstore.bluestore.BlueStore([])
+        o.args = args
         o.osd_id = '1'
         o.osd_fsid = 'asdf'
         o.osd_mkfs()
         assert '--keyfile' not in fake_call.calls[0]['args'][0]
 
-    def test_wal_is_added(self, fake_call, monkeypatch, objectstore_bluestore):
+    def test_wal_is_added(self, fake_call, monkeypatch, objectstore_bluestore, factory):
+        args = factory(dmcrypt=False)
         monkeypatch.setattr(system, 'chown', lambda path: True)
         bs = objectstore_bluestore(objecstore='bluestore',
                         osd_id='1',
                         osd_fid='asdf',
                         wal_device_path='/dev/smm1',
-                        cephx_secret='foo',)
+                        cephx_secret='foo',
+                        dmcrypt=False)
+        bs.args = args
         bs.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):
+    def test_db_is_added(self, fake_call, monkeypatch, factory):
+        args = factory(dmcrypt=False)
         monkeypatch.setattr(system, 'chown', lambda path: True)
         bs = objectstore.bluestore.BlueStore([])
+        bs.args = args
         bs.db_device_path = '/dev/smm2'
         bs.osd_mkfs()
         assert '--bluestore-block-db-path' in fake_call.calls[2]['args'][0]
index 8299586547700b7765e629d5f2fde950c3a03b5c..3a595d331df9b3c7c5ac4798a967a618c6b5462d 100644 (file)
@@ -3,12 +3,11 @@
 import logging
 import os
 from functools import total_ordering
-from ceph_volume import sys_info
+from ceph_volume import sys_info, allow_loop_devices
 from ceph_volume.api import lvm
 from ceph_volume.util import disk, system
 from ceph_volume.util.lsmdisk import LSMDisk
 from ceph_volume.util.constants import ceph_disk_guids
-from ceph_volume.util.disk import allow_loop_devices
 
 
 logger = logging.getLogger(__name__)
index 95d69da8d5e174edb9b548c4b82aae81c9afbbcc..3104dead053be77ae8b284213d41eb1169a9ea59 100644 (file)
@@ -3,7 +3,8 @@ import os
 import re
 import stat
 import time
-from ceph_volume import process
+import json
+from ceph_volume import process, allow_loop_devices
 from ceph_volume.api import lvm
 from ceph_volume.util.system import get_file_contents
 from typing import Dict, List, Any
@@ -727,31 +728,6 @@ def is_mapper_device(device_name):
     return device_name.startswith(('/dev/mapper', '/dev/dm-'))
 
 
-class AllowLoopDevices(object):
-    allow = False
-    warned = False
-
-    @classmethod
-    def __call__(cls):
-        val = os.environ.get("CEPH_VOLUME_ALLOW_LOOP_DEVICES", "false").lower()
-        if val not in ("false", 'no', '0'):
-            cls.allow = True
-            if not cls.warned:
-                logger.warning(
-                    "CEPH_VOLUME_ALLOW_LOOP_DEVICES is set in your "
-                    "environment, so we will allow the use of unattached loop"
-                    " devices as disks. This feature is intended for "
-                    "development purposes only and will never be supported in"
-                    " production. Issues filed based on this behavior will "
-                    "likely be ignored."
-                )
-                cls.warned = True
-        return cls.allow
-
-
-allow_loop_devices = AllowLoopDevices()
-
-
 def get_block_devs_sysfs(_sys_block_path: str = '/sys/block', _sys_dev_block_path: str = '/sys/dev/block', device: str = '') -> List[List[str]]:
     def holder_inner_loop() -> bool:
         for holder in holders:
@@ -963,3 +939,168 @@ def get_lvm_mappers(sys_block_path: str = '/sys/block') -> List[str]:
                     result.append(f'/dev/mapper/{name.strip()}')
                     result.append(f'/dev/{device}')
     return result
+
+def _dd_read(device: str, count: int, skip: int = 0) -> str:
+    """Read bytes from a device
+
+    Args:
+        device (str): The device to read bytes from.
+        count (int): The number of bytes to read.
+        skip (int, optional): The number of bytes to skip at the beginning. Defaults to 0.
+
+    Returns:
+        str: A string containing the read bytes.
+    """
+    result: str = ''
+    try:
+        with open(device, 'rb') as b:
+            b.seek(skip)
+            data: bytes = b.read(count)
+            result = data.decode('utf-8').replace('\x00', '')
+    except OSError:
+        logger.warning(f"Can't read from {device}")
+        pass
+    except UnicodeDecodeError:
+        pass
+    except Exception as e:
+        logger.error(f"An error occurred while reading from {device}: {e}")
+        raise
+
+    return result
+
+def _dd_write(device: str, data: str, skip: int = 0) -> None:
+    """Write bytes to a device
+
+    Args:
+        device (str): The device to write bytes to.
+        data (str): The data to write to the device.
+        skip (int, optional): The number of bytes to skip at the beginning. Defaults to 0.
+
+    Raises:
+        OSError: If there is an error opening or writing to the device.
+        Exception: If any other error occurs during the write operation.
+    """
+    try:
+        with open(device, 'r+b') as b:
+            b.seek(skip)
+            b.write(data.encode('utf-8'))
+    except OSError:
+        logger.warning(f"Can't write to {device}")
+        raise
+    except Exception as e:
+        logger.error(f"An error occurred while writing to {device}: {e}")
+        raise
+
+def get_bluestore_header(device: str) -> Dict[str, Any]:
+    """Retrieve BlueStore header information from a given device.
+
+    This function retrieves BlueStore header information from the specified 'device'.
+    It first checks if the device exists. If the device does not exist, a RuntimeError
+    is raised. Then, it calls the 'ceph-bluestore-tool' command to show the label
+    information of the device. If the command execution is successful, it parses the
+    JSON output containing the BlueStore header information and returns it as a dictionary.
+
+    Args:
+        device (str): The path to the device.
+
+    Returns:
+        Dict[str, Any]: A dictionary containing BlueStore header information.
+    """
+    data: Dict[str, Any] = {}
+
+    if os.path.exists(device):
+        out, err, rc = process.call([
+            'ceph-bluestore-tool', 'show-label',
+            '--dev', device], verbose_on_failure=False)
+        if rc:
+            logger.debug(f'device {device} is not BlueStore; ceph-bluestore-tool failed to get info from device: {out}\n{err}')
+        else:
+            data = json.loads(''.join(out))
+    else:
+        logger.warning(f'device {device} not found.')
+    return data
+
+def bluestore_info(device: str, bluestore_labels: Dict[str, Any]) -> Dict[str, Any]:
+    """Build a dict representation of a BlueStore header
+
+    Args:
+        device (str): The path of the BlueStore device.
+        bluestore_labels (Dict[str, Any]): Plain text output from `ceph-bluestore-tool show-label`
+
+    Returns:
+        Dict[str, Any]: Generated dict representation of the BlueStore header
+    """
+    result: Dict[str, Any] = {}
+    result['osd_uuid'] = bluestore_labels[device]['osd_uuid']
+    if bluestore_labels[device]['description'] == 'main':
+        whoami = bluestore_labels[device]['whoami']
+        result.update({
+            'type': bluestore_labels[device].get('type', 'bluestore'),
+            'osd_id': int(whoami),
+            'ceph_fsid': bluestore_labels[device]['ceph_fsid'],
+            'device': device,
+        })
+        if bluestore_labels[device].get('db_device_uuid', ''):
+            result['db_device_uuid'] = bluestore_labels[device].get('db_device_uuid')
+        if bluestore_labels[device].get('wal_device_uuid', ''):
+            result['wal_device_uuid'] = bluestore_labels[device].get('wal_device_uuid')
+    elif bluestore_labels[device]['description'] == 'bluefs db':
+        result['device_db'] = device
+    elif bluestore_labels[device]['description'] == 'bluefs wal':
+        result['device_wal'] = device
+    return result
+
+def get_block_device_holders(sys_block: str = '/sys/block') -> Dict[str, Any]:
+    """Get a dictionary of device mappers with their corresponding parent devices.
+
+    This function retrieves information about device mappers and their parent devices
+    from the '/sys/block' directory. It iterates through each directory within 'sys_block',
+    and for each directory, it checks if a 'holders' directory exists. If so, it lists
+    the contents of the 'holders' directory and constructs a dictionary where the keys
+    are the device mappers and the values are their corresponding parent devices.
+
+    Args:
+        sys_block (str, optional): The path to the '/sys/block' directory. Defaults to '/sys/block'.
+
+    Returns:
+        Dict[str, Any]: A dictionary where keys are device mappers (e.g., '/dev/mapper/...') and
+        values are their corresponding parent devices (e.g., '/dev/sdX').
+    """
+    result: Dict[str, Any] = {}
+    for b in os.listdir(sys_block):
+        path: str = os.path.join(sys_block, b, 'holders')
+        if os.path.exists(path):
+            for h in os.listdir(path):
+                result[f'/dev/{h}'] = f'/dev/{b}'
+
+    return result
+
+def get_parent_device_from_mapper(mapper: str, abspath: bool = True) -> str:
+    """Get the parent device corresponding to a given device mapper.
+
+    This function retrieves the parent device corresponding to a given device mapper
+    from the dictionary returned by the 'get_block_device_holders' function. It first
+    checks if the specified 'mapper' exists. If it does, it resolves the real path of
+    the mapper using 'os.path.realpath'. Then, it attempts to retrieve the parent device
+    from the dictionary. If the mapper is not found in the dictionary, an empty string
+    is returned.
+
+    Args:
+        mapper (str): The path to the device mapper.
+        abspath (bool, optional): If True (default), returns the absolute path of the parent device.
+                                  If False, returns only the basename of the parent device.
+
+    Returns:
+        str: The parent device corresponding to the given device mapper, or an empty string
+        if the mapper is not found in the dictionary of device mappers.
+    """
+    result: str = ''
+    if os.path.exists(mapper):
+        _mapper: str = os.path.realpath(mapper)
+        try:
+            result = get_block_device_holders()[_mapper]
+            if not abspath:
+                result = os.path.basename(result)
+        except KeyError:
+            pass
+    return result
index 15a31315645bb131024420ce9c5f8027ea9e552c..82e5f401f9377a19cf580e317624ca1660fd937b 100644 (file)
@@ -2,12 +2,14 @@ import base64
 import os
 import logging
 import re
+import json
 from ceph_volume import process, conf, terminal
 from ceph_volume.util import constants, system
 from ceph_volume.util.device import Device
 from .prepare import write_keyring
-from .disk import lsblk, device_family, get_part_entry_type
+from .disk import lsblk, device_family, get_part_entry_type, _dd_read
 from packaging import version
+from typing import Any, Dict, List
 
 logger = logging.getLogger(__name__)
 mlogger = terminal.MultiLogger(__name__)
@@ -82,7 +84,7 @@ def get_key_size_from_conf():
 
     return key_size
 
-def create_dmcrypt_key():
+def create_dmcrypt_key() -> str:
     """
     Create the secret dm-crypt key (KEK) used to encrypt/decrypt the Volume Key.
     """
@@ -91,7 +93,7 @@ def create_dmcrypt_key():
     return key
 
 
-def luks_format(key, device):
+def luks_format(key: str, device: str) -> None:
     """
     Decrypt (open) an encrypted device, previously prepared with cryptsetup
 
@@ -140,7 +142,47 @@ def plain_open(key, device, mapping):
     process.call(command, stdin=key, terminal_verbose=True, show_command=True)
 
 
-def luks_open(key, device, mapping):
+def luks_close(mapping: str) -> None:
+    """Close a LUKS2 mapper device.
+
+    Args:
+        mapping (str): the name of the mapper to be closed.
+    """
+    command: List[str] = ['cryptsetup',
+                          'luksClose',
+                          mapping]
+
+    process.call(command,
+                 terminal_verbose=True,
+                 show_command=True)
+
+
+def rename_mapper(current: str, new: str) -> None:
+    """Rename a mapper
+
+    Args:
+        old (str): current name
+        new (str): new name
+    """
+
+    command: List[str] = [
+        'dmsetup',
+        'rename',
+        current,
+        new
+    ]
+
+    _, err, rc = process.call(command,
+                              terminal_verbose=True,
+                              show_command=True)
+    if rc:
+        raise RuntimeError(f"Can't rename mapper '{current}' to '{new}': {err}")
+
+
+def luks_open(key: str,
+              device: str,
+              mapping: str,
+              with_tpm: int = 0) -> None:
     """
     Decrypt (open) an encrypted device, previously prepared with cryptsetup
 
@@ -150,23 +192,38 @@ def luks_open(key, device, mapping):
     :param device: absolute path to device
     :param mapping: mapping name used to correlate device. Usually a UUID
     """
-    command = [
-        'cryptsetup',
-        '--key-size',
-        get_key_size_from_conf(),
-        '--key-file',
-        '-',
-        '--allow-discards',  # allow discards (aka TRIM) requests for device
-        'luksOpen',
-        device,
-        mapping,
-    ]
-
-    if bypass_workqueue(device):
-        command.extend(['--perf-no_read_workqueue',
-                        '--perf-no_write_workqueue'])
-
-    process.call(command, stdin=key, terminal_verbose=True, show_command=True)
+    command: List[str] = []
+    if with_tpm:
+        command = ['/usr/lib/systemd/systemd-cryptsetup',
+                   'attach',
+                   mapping,
+                   device,
+                   '-',
+                   'tpm2-device=auto,discard']
+        if bypass_workqueue(device):
+            command[-1] += ',no-read-workqueue,no-write-workqueue'
+    else:
+        command = [
+            'cryptsetup',
+            '--key-size',
+            get_key_size_from_conf(),
+            '--key-file',
+            '-',
+            '--allow-discards',  # allow discards (aka TRIM) requests for device
+            'luksOpen',
+            device,
+            mapping,
+        ]
+
+        if bypass_workqueue(device):
+            command.extend(['--perf-no_read_workqueue',
+                            '--perf-no_write_workqueue'])
+
+    process.call(command,
+                 run_on_host=with_tpm,
+                 stdin=key,
+                 terminal_verbose=True,
+                 show_command=True)
 
 
 def dmcrypt_close(mapping, skip_path_check=False):
@@ -355,3 +412,160 @@ def prepare_dmcrypt(key, device, mapping):
         mapping
     )
     return '/dev/mapper/%s' % mapping
+
+
+class CephLuks2:
+    def __init__(self, device: str) -> None:
+        self.device: str = device
+        self.osd_fsid: str = ''
+        if self.is_ceph_encrypted:
+            self.osd_fsid = self.get_osd_fsid()
+
+    @property
+    def has_luks2_signature(self) -> bool:
+        try:
+            return _dd_read(self.device, 4) == 'LUKS'
+        except Exception as e:
+            raise RuntimeError(e)
+
+    @property
+    def is_ceph_encrypted(self) -> bool:
+        """Check whether a device is used for a Ceph encrypted OSD
+
+        Args:
+            device (str): The path of the device being checked.
+
+        Returns:
+            bool: `True` if the device is used by an encrypted Ceph OSD, else `False`.
+        """
+        result: bool = False
+        try:
+            result = self.has_luks2_signature and 'ceph_fsid=' in self.get_subsystem()
+        except RuntimeError:
+            pass
+        return result
+
+    def config_luks2(self, config: Dict[str, str]) -> None:
+        """Set the subsystem of a LUKS2 device
+
+        Args:
+            config (str): The config to apply to the LUKS2 device.
+
+        Raises:
+            RuntimeError: If it can't set LUKS2 configuration.
+        """
+        if not (0 < len(config) <= 2):
+            raise RuntimeError(f'Invalid config for LUKS2 device {self.device}')
+
+        valid_keys = ['label', 'subsystem']
+        if not all(key in valid_keys for key in config.keys()):
+            raise RuntimeError(f'LUKS2 config for device {self.device} can only be "label" and/or "subsystem".')
+
+        command: List[str] = ['cryptsetup', 'config',
+                              self.device]
+        for k, v in config.items():
+                command.extend([f'--{k}', v])
+        _, err, rc = process.call(command, verbose_on_failure=False)
+        if rc:
+            raise RuntimeError(f"Can't set luks2 config to {self.device}:\n{err}")
+
+    def get_label(self) -> str:
+        """Get the label of a LUKS2 device
+
+        Args:
+            device (str): The device to get the LUKS label from.
+
+        Returns:
+            str: The LUKS2 label of the device.
+        """
+        result: str = ''
+        try:
+            result = _dd_read(self.device, 48, 24)
+        except Exception:
+            raise RuntimeError(f"Can't get luks2 label from {self.device}")
+        return result
+
+    def get_osd_fsid(self) -> str:
+        """Get the osd fsid.
+
+        Returns:
+            str: The OSD fsid
+        """
+
+        result: str = ''
+        try:
+            subsystem = self.get_subsystem()
+            result = subsystem.split('=')[1]
+        except IndexError:
+            logger.debug(f"LUKS2 device {self.device} doesn't have ceph osd fsid detail. Please check LUKS2 label for this device.")
+        return result
+
+    def get_subsystem(self) -> str:
+        """Get the subsystem of a LUKS2 device
+
+        Args:
+            device (str): The device to get the LUKS subsystem from.
+
+        Returns:
+            str: The LUKS2 subsystem of the device.
+        """
+        result: str = ''
+        try:
+            result = _dd_read(self.device, 48, 208)
+        except Exception as e:
+            raise RuntimeError(f"Can't get luks2 label from {self.device}:\n{e}")
+        return result
+
+    def get_json_area(self) -> Dict[str, Any]:
+        """Retrieve the LUKS2 JSON configuration area from a given device.
+
+        This function reads the LUKS2 JSON configuration area from the specified 'device'.
+        It first checks if the device contains a LUKS2 signature. If not, an empty dictionary
+        is returned. If a LUKS2 signature is found, it reads the JSON configuration area
+        starting from byte offset 4096 (4 KB) and extracts the configuration data.
+
+        Args:
+            device (str): The path to the device.
+
+        Raises:
+            RuntimeError: If the LUKS2 JSON area on the device is invalid or cannot be decoded.
+
+        Returns:
+            Dict[str, Any]: A dictionary containing the extracted LUKS2 JSON configuration data.
+        """
+        result: Dict[str, Any] = {}
+        try:
+            data: str = _dd_read(self.device, 12288, 4096)
+            result = json.loads(data)
+        except json.JSONDecodeError:
+            msg: str = f"LUKS2 json area for device {self.device} seems invalid."
+            raise RuntimeError(msg)
+        except Exception:
+            raise
+
+        return result
+
+    @property
+    def is_tpm2_enrolled(self) -> bool:
+        """Check if a given device is enrolled with TPM2.
+
+        This function checks if the specified 'device' is enrolled with TPM2.
+        It first determines if the device is a LUKS encrypted volume by checking
+        its filesystem type using lsblk. If the filesystem type is 'crypto_LUKS',
+        it extracts the LUKS2 JSON configuration area from the device using the
+        'get_luks2_json_area' function. If the JSON area contains a 'systemd-tpm2'
+        token, it indicates that the device is enrolled with TPM2.
+
+        Args:
+            device (str): The path to the device.
+
+        Returns:
+            bool: True if the device is enrolled with TPM2, False otherwise.
+        """
+        if lsblk(self.device).get('FSTYPE', '') == 'crypto_LUKS':
+            json_area: Dict[str, Any] = self.get_json_area()
+            if 'tokens' in json_area.keys():
+                for token in json_area['tokens'].keys():
+                    if json_area['tokens'][token].get('type', '') == 'systemd-tpm2':
+                        return True
+        return False
index cf24fc0efa7589050607664b384c9d1f5a88bd41..c68ee01a7288fa422447ae3307c876deda070385 100644 (file)
@@ -166,7 +166,7 @@ class DriveGroupSpec(ServiceSpec):
     """
 
     _supported_features = [
-        "encrypted", "block_wal_size", "osds_per_device",
+        "encrypted", "tpm2", "block_wal_size", "osds_per_device",
         "db_slots", "wal_slots", "block_db_size", "placement", "service_id", "service_type",
         "data_devices", "db_devices", "wal_devices", "journal_devices",
         "data_directories", "osds_per_device", "objectstore", "osd_id_claims",
@@ -185,6 +185,7 @@ class DriveGroupSpec(ServiceSpec):
                  osds_per_device=None,  # type: Optional[int]
                  objectstore='bluestore',  # type: str
                  encrypted=False,  # type: bool
+                 tpm2=False,  # type: bool
                  db_slots=None,  # type: Optional[int]
                  wal_slots=None,  # type: Optional[int]
                  osd_id_claims=None,  # type: Optional[Dict[str, List[str]]]
@@ -248,6 +249,9 @@ class DriveGroupSpec(ServiceSpec):
         #: ``true`` or ``false``
         self.encrypted = encrypted
 
+        #: ``true`` or ``false``
+        self.tpm2 = tpm2
+
         #: How many OSDs per DB device
         self.db_slots = db_slots
 
index dd91b33e986ba196d0573d659a04c37b724e1313..49fb17da725e40b9f07115944e132518403f9cc0 100644 (file)
@@ -132,6 +132,9 @@ class to_ceph_volume(object):
             if self.spec.encrypted:
                 cmds[i] += " --dmcrypt"
 
+            if self.spec.tpm2:
+                cmds[i] += " --with-tpm"
+
             if self.spec.osds_per_device:
                 cmds[i] += " --osds-per-device {}".format(self.spec.osds_per_device)