From: Shweta Bhosale Date: Tue, 11 Nov 2025 07:47:31 +0000 (+0530) Subject: mgr/nfs: Cephadm support for NFS-Ganesha TLS configuration adding new option xprtsec X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=741e3e063fb892ac66c8231a1ab80c8bc4f833c0;p=ceph-ci.git mgr/nfs: Cephadm support for NFS-Ganesha TLS configuration adding new option xprtsec Fixes: https://tracker.ceph.com/issues/73774 Signed-off-by: Shweta Bhosale Resolves: rhbz#2413723 Conflicts: src/pybind/mgr/nfs/export.py src/pybind/mgr/nfs/ganesha_conf.py src/pybind/mgr/nfs/module.py --- diff --git a/doc/mgr/nfs.rst b/doc/mgr/nfs.rst index 485200e1f5c..2fc65e967dd 100644 --- a/doc/mgr/nfs.rst +++ b/doc/mgr/nfs.rst @@ -290,7 +290,7 @@ Create CephFS Export .. prompt:: bash # - ceph nfs export create cephfs --cluster-id --pseudo-path --fsname [--readonly] [--path=/path/in/cephfs] [--client_addr ...] [--squash ] [--sectype ...] [--cmount_path ] + ceph nfs export create cephfs --cluster-id --pseudo-path --fsname [--readonly] [--path=/path/in/cephfs] [--client_addr ...] [--squash ] [--sectype ...] [--cmount_path ] [--xprtsec ] This creates export RADOS objects containing the export block, where @@ -318,13 +318,16 @@ value is ``no_root_squash``. See the `NFS-Ganesha Export Sample`_ for permissible values. ```` specifies which authentication methods will be used when -connecting to the export. Valid values include "krb5p", "krb5i", "krb5", "sys", "tls", "mtls" -and "none". More than one value can be supplied. The flag may be specified +connecting to the export. Valid values include ``krb5p``, ``krb5i``, ``krb5``, ``sys`` +and ``none``. More than one value can be supplied. The flag may be specified multiple times (example: ``--sectype=krb5p --sectype=krb5i``) or multiple values may be separated by a comma (example: ``--sectype krb5p,krb5i``). The server will negotatiate a supported security type with the client preferring the supplied methods left-to-right. +```` defines how traffic is secured at the transport layer. +Valid values are ``tls``, ``mtls`` and ``none``. + ```` specifies the path within the CephFS to mount this export on. It is allowed to be any complete path hierarchy between ``/`` and the ``EXPORT {path}``. (i.e. if ``EXPORT { Path }`` parameter is ``/foo/bar`` then cmount_path could be ``/``, ``/foo`` or ``/foo/bar``). @@ -355,7 +358,7 @@ To export a *bucket*: .. prompt:: bash # - ceph nfs export create rgw --cluster-id --pseudo-path --bucket [--user-id ] [--readonly] [--client_addr ...] [--squash ] [--sectype ...] + ceph nfs export create rgw --cluster-id --pseudo-path --bucket [--user-id ] [--readonly] [--client_addr ...] [--squash ] [--sectype ...] [--xprtsec ] For example, to export ``mybucket`` via NFS cluster ``mynfs`` at the pseudo-path ``/bucketdata`` to any host in the ``192.168.10.0/24`` network @@ -392,8 +395,8 @@ default value is ``no_root_squash``. See the `NFS-Ganesha Export Sample`_ for permissible values. ```` specifies which authentication methods will be used when -connecting to the export. Valid values include "krb5p", "krb5i", "krb5", -"sys", and "none". More than one value can be supplied. The flag may be +connecting to the export. Valid values include ``krb5p``, ``krb5i``, ``krb5``, +``sys`` and ``none``. More than one value can be supplied. The flag may be specified multiple times (example: ``--sectype=krb5p --sectype=krb5i``) or multiple values may be separated by a comma (example: ``--sectype krb5p,krb5i``). The server will negotatiate a supported security type with the @@ -403,6 +406,10 @@ client preferring the supplied methods left-to-right. function on servers that are configured to support Kerberos. Setting up NFS-Ganesha to support Kerberos is outside the scope of this document. +```` defines how traffic is secured at the transport layer. +Valid values are ``tls``, ``mtls`` and ``none``. + + RGW user export ^^^^^^^^^^^^^^^ diff --git a/src/pybind/mgr/nfs/export.py b/src/pybind/mgr/nfs/export.py index 42d0a1e9cdf..3525bd4639e 100644 --- a/src/pybind/mgr/nfs/export.py +++ b/src/pybind/mgr/nfs/export.py @@ -675,6 +675,7 @@ class ExportMgr: access_type: str, clients: list = [], sectype: Optional[List[str]] = None, + xprtsec: Optional[str] = None, cmount_path: Optional[str] = "/", earmark_resolver: Optional[CephFSEarmarkResolver] = None, kmip_key_id: Optional[str] = None @@ -702,7 +703,8 @@ class ExportMgr: }, "clients": clients, "sectype": sectype, - "kmip_key_id": kmip_key_id + "kmip_key_id": kmip_key_id, + "XprtSec": xprtsec, }, earmark_resolver ) @@ -729,7 +731,8 @@ class ExportMgr: user_id: Optional[str] = None, clients: list = [], sectype: Optional[List[str]] = None, - kmip_key_id: Optional[str] = None) -> Dict[str, Any]: + kmip_key_id: Optional[str] = None, + xprtsec: Optional[str] = None) -> Dict[str, Any]: pseudo_path = normalize_path(pseudo_path) if not bucket and not user_id: @@ -750,7 +753,8 @@ class ExportMgr: }, "clients": clients, "sectype": sectype, - "kmip_key_id": kmip_key_id + "kmip_key_id": kmip_key_id, + "XprtSec": xprtsec, } ) log.debug("creating rgw export %s", export) diff --git a/src/pybind/mgr/nfs/ganesha_conf.py b/src/pybind/mgr/nfs/ganesha_conf.py index bd59d8a0b65..20c590ce66a 100644 --- a/src/pybind/mgr/nfs/ganesha_conf.py +++ b/src/pybind/mgr/nfs/ganesha_conf.py @@ -48,12 +48,19 @@ def _validate_access_type(access_type: str) -> None: def _validate_sec_type(sec_type: str) -> None: - valid_sec_types = ["none", "sys", "krb5", "krb5i", "krb5p", "tls", "mtls"] + valid_sec_types = ["none", "sys", "krb5", "krb5i", "krb5p"] if not isinstance(sec_type, str) or sec_type not in valid_sec_types: raise NFSInvalidOperation( f"SecType {sec_type} invalid, valid types are {valid_sec_types}") +def _validate_xprtsec_type(xprtsec: str) -> None: + valid_xprtsec_types = ['none', 'tls', 'mtls'] + if not isinstance(xprtsec, str) or xprtsec not in valid_xprtsec_types: + raise NFSInvalidOperation( + f"XprtSec {xprtsec} invalid, valid types are {valid_xprtsec_types}") + + class GaneshaConfParser: def __init__(self, raw_config: str): self.pos = 0 @@ -357,7 +364,8 @@ class Export: clients: Optional[List[Client]] = None, sectype: Optional[List[str]] = None, qos_block: Optional[QOS] = None, - kmip_key_id: Optional[str] = None) -> None: + kmip_key_id: Optional[str] = None, + xprtsec: Optional[str] = None) -> None: self.export_id = export_id self.path = path self.fsal = fsal @@ -373,6 +381,7 @@ class Export: self.sectype = sectype self.qos_block = qos_block self.kmip_key_id = kmip_key_id + self.xprtsec = xprtsec @classmethod def from_export_block(cls, export_block: RawBlock, cluster_id: str) -> 'Export': @@ -406,6 +415,9 @@ class Export: # https://github.com/ceph/go-ceph/issues/1097 if sectype is not None and not isinstance(sectype, list): sectype = [sectype] + + xprtsec = export_block.values.get('XprtSec') + return cls(export_block.values['export_id'], export_block.values['path'], cluster_id, @@ -420,8 +432,8 @@ class Export: for client in client_blocks], sectype=sectype, qos_block=qos_block, - kmip_key_id=export_block.values.get('kmip_key_id') - ) + kmip_key_id=export_block.values.get('kmip_key_id'), + xprtsec=xprtsec) def to_export_block(self) -> RawBlock: # if kmip_key_id is present, it should be first line of export block @@ -442,6 +454,8 @@ class Export: }) if self.sectype: values['SecType'] = self.sectype + if self.xprtsec: + values['XprtSec'] = self.xprtsec result = RawBlock("EXPORT", values=values) result.blocks = [ self.fsal.to_fsal_block() @@ -472,8 +486,8 @@ class Export: [Client.from_dict(client) for client in ex_dict.get('clients', [])], sectype=ex_dict.get("sectype"), qos_block=qos_block, - kmip_key_id=ex_dict.get('kmip_key_id') - ) + kmip_key_id=ex_dict.get('kmip_key_id'), + xprtsec=ex_dict.get('XprtSec')) def to_dict(self) -> Dict[str, Any]: values = { @@ -495,6 +509,8 @@ class Export: values['qos_block'] = self.qos_block.to_dict() if self.kmip_key_id: values['kmip_key_id'] = self.kmip_key_id + if self.xprtsec: + values['XprtSec'] = self.xprtsec return values def validate(self, mgr: 'Module') -> None: @@ -537,6 +553,8 @@ class Export: for st in (self.sectype or []): _validate_sec_type(st) + if self.xprtsec: + _validate_xprtsec_type(self.xprtsec) def __eq__(self, other: Any) -> bool: if not isinstance(other, Export): diff --git a/src/pybind/mgr/nfs/module.py b/src/pybind/mgr/nfs/module.py index 3733543226d..b888b320be2 100644 --- a/src/pybind/mgr/nfs/module.py +++ b/src/pybind/mgr/nfs/module.py @@ -43,7 +43,8 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule): sectype: Optional[List[str]] = None, cmount_path: Optional[str] = "/", skip_notify_nfs_server: bool = False, - kmip_key_id: Optional[str] = None + kmip_key_id: Optional[str] = None, + xprtsec: Optional[str] = None ) -> Dict[str, Any]: """Create a CephFS export""" self.export_mgr.skip_notify_nfs_server = skip_notify_nfs_server @@ -58,6 +59,7 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule): squash=squash, addr=client_addr, sectype=sectype, + xprtsec=xprtsec, cmount_path=cmount_path, earmark_resolver=earmark_resolver, kmip_key_id=kmip_key_id @@ -76,7 +78,8 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule): squash: str = 'none', sectype: Optional[List[str]] = None, skip_notify_nfs_server: bool = False, - kmip_key_id: Optional[str] = None + kmip_key_id: Optional[str] = None, + xprtsec: Optional[str] = None, ) -> Dict[str, Any]: """Create an RGW export""" self.export_mgr.skip_notify_nfs_server = skip_notify_nfs_server @@ -90,7 +93,8 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule): squash=squash, addr=client_addr, sectype=sectype, - kmip_key_id=kmip_key_id + kmip_key_id=kmip_key_id, + xprtsec=xprtsec ) @CLICommand('nfs export rm', perm='rw') diff --git a/src/pybind/mgr/nfs/tests/test_nfs.py b/src/pybind/mgr/nfs/tests/test_nfs.py index d73d0f254b0..b6479908194 100644 --- a/src/pybind/mgr/nfs/tests/test_nfs.py +++ b/src/pybind/mgr/nfs/tests/test_nfs.py @@ -888,6 +888,7 @@ NFS_CORE_PARAM { assert info["export_id"] == 2 assert info["path"] == "bucket" assert "sectype" not in info + assert 'XprtSec' not in info r = conf.apply_export(self.cluster_id, json.dumps({ 'export_id': 2, @@ -904,7 +905,8 @@ NFS_CORE_PARAM { 'access_type': None, 'squash': None }], - 'sectype': ["krb5p", "krb5i", "sys", "mtls", "tls"], + 'sectype': ["krb5p", "krb5i", "sys"], + 'XprtSec': 'tls', 'fsal': { 'name': 'RGW', 'user_id': 'nfs.foo.bucket', @@ -918,7 +920,8 @@ NFS_CORE_PARAM { info = conf._get_export_dict(self.cluster_id, "/rgw/bucket") assert info["export_id"] == 2 assert info["path"] == "bucket" - assert info["sectype"] == ["krb5p", "krb5i", "sys", "mtls", "tls"] + assert info["sectype"] == ["krb5p", "krb5i", "sys"] + assert info['XprtSec'] == 'tls' def test_update_export_with_ganesha_conf(self): self._do_mock_test(self._do_test_update_export_with_ganesha_conf) @@ -1105,7 +1108,8 @@ NFS_CORE_PARAM { pseudo_path='/mybucket', read_only=False, squash='root', - addr=["192.168.0.0/16"] + addr=["192.168.0.0/16"], + xprtsec='tls' ) assert r["bind"] == "/mybucket" @@ -1129,6 +1133,7 @@ NFS_CORE_PARAM { assert export.clients[0].access_type == 'rw' assert export.clients[0].addresses == ["192.168.0.0/16"] assert export.cluster_id == self.cluster_id + assert export.xprtsec == 'tls' def test_create_export_rgw_bucket_user(self): self._do_mock_test(self._do_test_create_export_rgw_bucket_user)