From: Guillaume Abrioux Date: Mon, 1 Sep 2025 13:13:43 +0000 (+0000) Subject: ceph-volume: support additional dmcrypt params X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=63babbcc0951b518f5e1ed1eb4c1c10b668179bc;p=ceph.git ceph-volume: support additional dmcrypt params Add support for passing extra options to dmcrypt commands. This makes it possible to provide custom parameters to `cryptsetup luksFormat` and `cryptsetup luksOpen`, using the same syntax as the native cryptsetup CLI. Fixes: https://tracker.ceph.com/issues/72801 Signed-off-by: Guillaume Abrioux --- diff --git a/src/ceph-volume/ceph_volume/devices/lvm/batch.py b/src/ceph-volume/ceph_volume/devices/lvm/batch.py index 8f766923bd1..9ed46d7afc3 100644 --- a/src/ceph-volume/ceph_volume/devices/lvm/batch.py +++ b/src/ceph-volume/ceph_volume/devices/lvm/batch.py @@ -278,6 +278,18 @@ class Batch(object): help='Reuse existing OSD ids', type=arg_validators.valid_osd_id ) + parser.add_argument( + '--dmcrypt-format-opts', + type=str, + default=None, + help="Additional cryptsetup luksFormat options (use the same syntax as the cryptsetup CLI)", + ) + parser.add_argument( + '--dmcrypt-open-opts', + type=str, + default=None, + help="Additional cryptsetup luksOpen options (use the same syntax as the cryptsetup CLI)", + ) self.args = parser.parse_args(argv) if self.args.bluestore: self.args.objectstore = 'bluestore' @@ -379,6 +391,8 @@ class Batch(object): 'with_tpm', 'crush_device_class', 'no_systemd', + 'dmcrypt_format_opts', + 'dmcrypt_open_opts', ] defaults.update({arg: getattr(self.args, arg) for arg in global_args}) for osd in plan: diff --git a/src/ceph-volume/ceph_volume/devices/lvm/common.py b/src/ceph-volume/ceph_volume/devices/lvm/common.py index a74931155c2..fa39ac6c962 100644 --- a/src/ceph-volume/ceph_volume/devices/lvm/common.py +++ b/src/ceph-volume/ceph_volume/devices/lvm/common.py @@ -84,6 +84,14 @@ common_args: Dict[str, Any] = { 'action': arg_validators.DmcryptAction, 'help': 'Enable device encryption via dm-crypt', }, + '--dmcrypt-format-opts': { + 'default': None, + 'type': Optional[str], + }, + '--dmcrypt-open-opts': { + 'default': None, + 'type': Optional[str], + }, '--with-tpm': { 'dest': 'with_tpm', 'help': 'Whether encrypted OSDs should be enrolled with TPM.', diff --git a/src/ceph-volume/ceph_volume/devices/lvm/migrate.py b/src/ceph-volume/ceph_volume/devices/lvm/migrate.py index 83ed16845e7..1e19d9afd3e 100644 --- a/src/ceph-volume/ceph_volume/devices/lvm/migrate.py +++ b/src/ceph-volume/ceph_volume/devices/lvm/migrate.py @@ -339,7 +339,11 @@ class Migrate(object): secret = encryption_utils.get_dmcrypt_key(osd_id, osd_fsid) mlogger.info(' preparing dmcrypt for {}, uuid {}'.format(target_lv.lv_path, target_lv.lv_uuid)) target_path = encryption_utils.prepare_dmcrypt( - key=secret, device=target_path, mapping=target_lv.lv_uuid) + key=secret, + device=target_path, + mapping=target_lv.lv_uuid, + format_options=self.args.dmcrypt_format_opts, + open_options=self.args.dmcrypt_open_opts) try: # we need to update lvm tags for all the remaining volumes # and clear for ones which to be removed @@ -511,6 +515,18 @@ class Migrate(object): action='store_true', help='Skip checking OSD systemd unit', ) + parser.add_argument( + '--dmcrypt-format-opts', + type=str, + default=None, + help="Additional cryptsetup luksFormat options (use the same syntax as the cryptsetup CLI)", + ) + parser.add_argument( + '--dmcrypt-open-opts', + type=str, + default=None, + help="Additional cryptsetup luksOpen options (use the same syntax as the cryptsetup CLI)", + ) return parser def main(self): @@ -602,6 +618,18 @@ class NewVolume(object): action='store_true', help='Skip checking OSD systemd unit', ) + parser.add_argument( + '--dmcrypt-format-opts', + type=str, + default=None, + help="Additional cryptsetup luksFormat options (use the same syntax as the cryptsetup CLI)", + ) + parser.add_argument( + '--dmcrypt-open-opts', + type=str, + default=None, + help="Additional cryptsetup luksOpen options (use the same syntax as the cryptsetup CLI)", + ) return parser @decorators.needs_root @@ -617,7 +645,12 @@ class NewVolume(object): secret = encryption_utils.get_dmcrypt_key(osd_id, osd_fsid) mlogger.info(' preparing dmcrypt for {}, uuid {}'.format(target_lv.lv_path, target_lv.lv_uuid)) target_path = encryption_utils.prepare_dmcrypt( - key=secret, device=target_path, mapping=target_lv.lv_uuid) + key=secret, + device=target_path, + mapping=target_lv.lv_uuid, + format_options=self.args.dmcrypt_format_opts, + open_options=self.args.dmcrypt_open_opts + ) try: tag_tracker.update_tags_when_lv_create(self.create_type) diff --git a/src/ceph-volume/ceph_volume/objectstore/lvm.py b/src/ceph-volume/ceph_volume/objectstore/lvm.py index 204f2b954e1..e1c78c9483b 100644 --- a/src/ceph-volume/ceph_volume/objectstore/lvm.py +++ b/src/ceph-volume/ceph_volume/objectstore/lvm.py @@ -181,7 +181,8 @@ class Lvm(BaseObjectStore): # format data device encryption_utils.luks_format( self.dmcrypt_key, - device + device, + self.args.dmcrypt_format_opts, ) if self.with_tpm: @@ -191,7 +192,8 @@ class Lvm(BaseObjectStore): self.dmcrypt_key, device, uuid, - self.with_tpm) + self.with_tpm, + self.args.dmcrypt_open_opts) return '/dev/mapper/%s' % uuid diff --git a/src/ceph-volume/ceph_volume/tests/objectstore/test_lvm.py b/src/ceph-volume/ceph_volume/tests/objectstore/test_lvm.py index f23bfd0876e..3493f9930eb 100644 --- a/src/ceph-volume/ceph_volume/tests/objectstore/test_lvm.py +++ b/src/ceph-volume/ceph_volume/tests/objectstore/test_lvm.py @@ -10,7 +10,8 @@ from typing import Callable class TestLvm: @patch('ceph_volume.objectstore.lvm.prepare_utils.create_key', Mock(return_value=['AQCee6ZkzhOrJRAAZWSvNC3KdXOpC2w8ly4AZQ=='])) def setup_method(self, m_create_key): - self.lvm = Lvm([]) + args = Namespace(dmcrypt_format_opts=None, dmcrypt_open_opts=None) + self.lvm = Lvm(args) @patch('ceph_volume.conf.cluster', 'ceph') @patch('ceph_volume.api.lvm.get_single_lv') @@ -28,6 +29,7 @@ class TestLvm: lv_tags='', lv_uuid='fake-uuid') self.lvm.encrypted = True + self.lvm.with_tpm = 0 self.lvm.dmcrypt_key = 'fake-dmcrypt-key' self.lvm.args = args self.lvm.objectstore = 'seastore' @@ -108,6 +110,7 @@ class TestLvm: lv_uuid='fake-uuid') self.lvm.encrypted = True self.lvm.dmcrypt_key = 'fake-dmcrypt-key' + self.lvm.with_tpm = 0 self.lvm.args = args self.lvm.objectstore = 'seastore' self.lvm.pre_prepare() diff --git a/src/ceph-volume/ceph_volume/tests/util/test_encryption.py b/src/ceph-volume/ceph_volume/tests/util/test_encryption.py index 3e5284d9af8..5c3d47a6478 100644 --- a/src/ceph-volume/ceph_volume/tests/util/test_encryption.py +++ b/src/ceph-volume/ceph_volume/tests/util/test_encryption.py @@ -125,6 +125,23 @@ class TestLuksFormat(object): encryption.luks_format('abcd', '/dev/foo') assert m_call.call_args[0][0] == expected + @patch('ceph_volume.util.encryption.process.call') + def test_luks_format_with_extra_option(self, m_call, conf_ceph_stub): + conf_ceph_stub('[global]\nfsid=abcd') + expected = [ + 'cryptsetup', + '--batch-mode', + '--key-size', + '512', + '--key-file', + '-', + 'luksFormat', + '--fake-custom-opt1', + '/dev/foo' + ] + encryption.luks_format('abcd', '/dev/foo', '--fake-custom-opt1') + assert m_call.call_args[0][0] == expected + @patch('ceph_volume.util.encryption.process.call') def test_luks_format_command_with_custom_size(self, m_call, conf_ceph_stub): conf_ceph_stub('[global]\nfsid=abcd\n[osd]\nosd_dmcrypt_key_size=256') @@ -179,6 +196,25 @@ class TestLuksOpen(object): encryption.luks_open('abcd', '/dev/foo', '/dev/bar') assert m_call.call_args[0][0] == expected + @patch('ceph_volume.util.encryption.bypass_workqueue', return_value=False) + @patch('ceph_volume.util.encryption.process.call') + def test_luks_format_with_extra_option(self, m_call, m_bypass_workqueue, conf_ceph_stub): + conf_ceph_stub('[global]\nfsid=abcd') + expected = [ + 'cryptsetup', + '--key-size', + '512', + '--key-file', + '-', + '--allow-discards', + 'luksOpen', + '--fake-custom-opt1', + '/dev/foo', + '/dev/bar' + ] + encryption.luks_open('abcd', '/dev/foo', '/dev/bar', options='--fake-custom-opt1') + assert m_call.call_args[0][0] == expected + @patch('ceph_volume.util.encryption.bypass_workqueue', return_value=False) @patch('ceph_volume.util.encryption.process.call') def test_luks_open_command_with_tpm(self, m_call, m_bypass_workqueue, conf_ceph_stub): diff --git a/src/ceph-volume/ceph_volume/util/encryption.py b/src/ceph-volume/ceph_volume/util/encryption.py index 5de77d21a9a..659163a0b06 100644 --- a/src/ceph-volume/ceph_volume/util/encryption.py +++ b/src/ceph-volume/ceph_volume/util/encryption.py @@ -3,13 +3,14 @@ import os import logging import re import json +import shlex 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, _dd_read from packaging import version -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional logger = logging.getLogger(__name__) mlogger = terminal.MultiLogger(__name__) @@ -93,23 +94,27 @@ def create_dmcrypt_key() -> str: return key -def luks_format(key: str, device: str) -> None: +def luks_format(key: str, device: str, options: Optional[str] = None) -> None: """ Decrypt (open) an encrypted device, previously prepared with cryptsetup :param key: dmcrypt secret key, will be used for decrypting :param device: Absolute path to device + :param options: A list of additional cryptsetup args """ - command = [ + extra_args: List[str] = [] + base_cmd: List[str] = [ 'cryptsetup', '--batch-mode', # do not prompt - '--key-size', - get_key_size_from_conf(), - '--key-file', # misnomer, should be key - '-', # because we indicate stdin for the key here - 'luksFormat', - device, + '--key-size', get_key_size_from_conf(), + '--key-file', '-', ] + + if options is not None: + extra_args: List[str] = shlex.split(options) + + command: List[str] = base_cmd + ["luksFormat"] + extra_args + [device] + process.call(command, stdin=key, terminal_verbose=True, show_command=True) @@ -182,7 +187,8 @@ def rename_mapper(current: str, new: str) -> None: def luks_open(key: str, device: str, mapping: str, - with_tpm: int = 0) -> None: + with_tpm: int = 0, + options: Optional[str] = None) -> None: """ Decrypt (open) an encrypted device, previously prepared with cryptsetup @@ -192,8 +198,10 @@ def luks_open(key: str, :param device: absolute path to device :param mapping: mapping name used to correlate device. Usually a UUID :param with_tpm: whether to use tpm2 token enrollment. + :param options: A list of additional cryptsetup args """ command: List[str] = [] + extra_args: List[str] = [] if with_tpm: command = ['/usr/lib/systemd/systemd-cryptsetup', 'attach', @@ -204,17 +212,20 @@ def luks_open(key: str, 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, + base_cmd: List[str] = [ + "cryptsetup", + "--key-size", str(get_key_size_from_conf()), + "--key-file", "-", + "--allow-discards", ] + if options is not None: + extra_args = shlex.split(options) + command: List[str] = ( + base_cmd + + ["luksOpen"] + + extra_args + + [device, mapping] + ) if bypass_workqueue(device): command.extend(['--perf-no_read_workqueue', @@ -395,7 +406,11 @@ def legacy_encrypted(device): break return metadata -def prepare_dmcrypt(key, device, mapping): +def prepare_dmcrypt(key: str, + device: str, + mapping: str, + format_options: Optional[str] = None, + open_options: Optional[str] = None): """ Helper for devices that are encrypted. The operations needed for block, db, wal, or data/journal devices are all the same @@ -405,12 +420,14 @@ def prepare_dmcrypt(key, device, mapping): # format data device luks_format( key, - device + device, + format_options ) luks_open( key, device, - mapping + mapping, + options=open_options ) return '/dev/mapper/%s' % mapping