From: Shweta Bhosale Date: Tue, 4 Nov 2025 15:18:34 +0000 (+0530) Subject: mgr/nfs: Add parameter to nfs cluster create to pass ingress placement X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=e9b85dfa74895c1769d382a3d006faa2bba56a02;p=ceph.git mgr/nfs: Add parameter to nfs cluster create to pass ingress placement Fixes: https://tracker.ceph.com/issues/73718 Signed-off-by: Shweta Bhosale --- diff --git a/doc/mgr/nfs.rst b/doc/mgr/nfs.rst index 4ef7a9581b9..bf2005ec695 100644 --- a/doc/mgr/nfs.rst +++ b/doc/mgr/nfs.rst @@ -31,7 +31,7 @@ Create NFS Ganesha Cluster .. prompt:: bash # - ceph nfs cluster create [] [--ingress] [--virtual_ip ] [--ingress-mode {default|keepalive-only|haproxy-standard|haproxy-protocol}] [--port ] [--enable-rdma] [--rdma_port ] [-i ] [--enable-nfsv3] + ceph nfs cluster create [] [--ingress] [--virtual_ip ] [--ingress-mode {default|keepalive-only|haproxy-standard|haproxy-protocol}] [--ingress-placement ] [--port ] [--enable-rdma] [--rdma_port ] [--enable-nfsv3] [-i ] This creates a common recovery pool for all NFS Ganesha daemons, new user based on ``cluster_id``, and a common NFS Ganesha config RADOS object. @@ -111,6 +111,18 @@ of the details of NFS redirecting traffic on the virtual IP to the appropriate backend NFS servers, and redeploying NFS servers when they fail. +By default, the *ingress* service follows the same placement as the NFS +Ganesha daemons (the optional ```` argument). To schedule +keepalived and HAProxy on a different set of hosts, pass +``--ingress-placement`` with a separate placement string. For example, +to run three NFS daemons on ``host1`` and ``host2`` while colocating +ingress on three dedicated nodes:: + + ceph nfs cluster create mynfs "2 host1 host2" --ingress --virtual_ip 192.168.1.100/24 --ingress-placement "3 host3 host4 host5" + +If ``--ingress-placement`` is omitted, both services share the NFS +placement. + An optional ``--ingress-mode`` parameter can be provided to choose how the *ingress* service is configured: diff --git a/src/pybind/mgr/nfs/cluster.py b/src/pybind/mgr/nfs/cluster.py index d67ca4cb1e3..f022cd69ed1 100644 --- a/src/pybind/mgr/nfs/cluster.py +++ b/src/pybind/mgr/nfs/cluster.py @@ -172,6 +172,7 @@ class NFSCluster: tls_ciphers: Optional[str] = None, enable_rdma: bool = False, rdma_port: Optional[int] = None, + ingress_placement: Optional[str] = None, ) -> None: if not port: port = 2049 # default nfs port @@ -182,9 +183,12 @@ class NFSCluster: ingress_mode = IngressType.default ingress_mode = ingress_mode.canonicalize() pspec = PlacementSpec.from_string(placement) + ingress_pspec = PlacementSpec.from_string(ingress_placement) if ingress_placement else None if ingress_mode == IngressType.keepalive_only: # enforce count=1 for nfs over keepalive only pspec.count = 1 + if ingress_pspec: + ingress_pspec.count = 1 ganesha_port = 10000 + port # semi-arbitrary, fix me someday frontend_port: Optional[int] = port @@ -223,7 +227,7 @@ class NFSCluster: ispec = IngressSpec(service_type='ingress', service_id='nfs.' + cluster_id, backend_service='nfs.' + cluster_id, - placement=pspec, + placement=ingress_pspec or pspec, frontend_port=frontend_port, monitor_port=7000 + port, # semi-arbitrary, fix me someday virtual_ip=virtual_ip, @@ -283,6 +287,7 @@ class NFSCluster: tls_ciphers: Optional[str] = None, enable_rdma: bool = False, rdma_port: Optional[int] = None, + ingress_placement: Optional[str] = None, ) -> None: try: if virtual_ip: @@ -323,7 +328,8 @@ class NFSCluster: tls_min_version=tls_min_version, tls_ciphers=tls_ciphers, enable_rdma=enable_rdma, - rdma_port=rdma_port + rdma_port=rdma_port, + ingress_placement=ingress_placement ) return raise NonFatalError(f"{cluster_id} cluster already exists") diff --git a/src/pybind/mgr/nfs/module.py b/src/pybind/mgr/nfs/module.py index 0f4ed118d94..79549038cde 100644 --- a/src/pybind/mgr/nfs/module.py +++ b/src/pybind/mgr/nfs/module.py @@ -165,6 +165,7 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule): enable_rdma: bool = False, rdma_port: Optional[int] = None, enable_nfsv3: bool = False, + ingress_placement: Optional[str] = None, inbuf: Optional[str] = None) -> None: """Create an NFS Cluster""" cluster_qos_config = None @@ -196,7 +197,8 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule): tls_min_version=tls_min_version, tls_ciphers=tls_ciphers, enable_rdma=enable_rdma, - rdma_port=rdma_port) + rdma_port=rdma_port, + ingress_placement=ingress_placement) @NFSCLICommand('nfs cluster rm', perm='rw') @object_format.EmptyResponder() diff --git a/src/pybind/mgr/nfs/tests/test_nfs.py b/src/pybind/mgr/nfs/tests/test_nfs.py index 0349e3e905b..d1b6c7d6195 100644 --- a/src/pybind/mgr/nfs/tests/test_nfs.py +++ b/src/pybind/mgr/nfs/tests/test_nfs.py @@ -11,7 +11,7 @@ from mgr_module import MgrModule, NFS_POOL_NAME from rados import ObjectNotFound -from ceph.deployment.service_spec import NFSServiceSpec +from ceph.deployment.service_spec import NFSServiceSpec, PlacementSpec from ceph.utils import with_units_to_int, bytes_to_human from nfs import Module from nfs.export import ExportMgr, normalize_path @@ -1798,6 +1798,30 @@ EXPORT { self._do_mock_test(self._do_test_export_qos_bw_ops, qos_type, clust_bw_params, clust_ops_params, export_bw_params, export_ops_params) +class TestNFSClusterIngressPlacement: + cluster_id = 'mynfs' + virtual_ip = '192.168.1.100/24' + nfs_placement = '2 host1 host2' + ingress_placement = '3 host3 host4 host5' + + def test_create_nfs_cluster_passes_ingress_placement(self): + mgr = MagicMock() + cluster = NFSCluster(mgr) + with mock.patch('nfs.cluster.create_ganesha_pool'), \ + mock.patch.object(cluster, 'create_empty_rados_obj'), \ + mock.patch('nfs.cluster.available_clusters', return_value=[]), \ + mock.patch.object(cluster, '_call_orch_apply_nfs') as mock_apply: + cluster.create_nfs_cluster( + cluster_id=self.cluster_id, + placement='host1', + virtual_ip=self.virtual_ip, + ingress=True, + ingress_placement=self.ingress_placement, + ) + mock_apply.assert_called_once() + assert mock_apply.call_args.kwargs['ingress_placement'] == self.ingress_placement + + @pytest.mark.parametrize( "path,expected", [