From 43bf8f21ecc1c032621e0a25099f7cf7f02f934f Mon Sep 17 00:00:00 2001 From: Kushal Deb Date: Tue, 5 May 2026 11:00:12 +0530 Subject: [PATCH] mgr/cephadm: allow nvmeof group assignment for NVMe-oF services NVMe-oF services created in older releases may have a service_id that does not include the gateway group name, because those services could be created without a group. The current validation requires an NVMe-oF service_id to end with the configured group name when spec.group is set. This is correct for new services, but it blocks the documented upgrade flow for existing legacy services where the user exports the existing spec, adds spec.group, and applies it back. Allow this narrow update path when the service already exists, the stored spec has no group, and the incoming spec keeps the same service_id while adding a non-empty group. Keep the existing validation for new services and for services that already have a group, so duplicate or misleading group configurations are still rejected. Signed-off-by: Kushal Deb --- src/pybind/mgr/cephadm/module.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/pybind/mgr/cephadm/module.py b/src/pybind/mgr/cephadm/module.py index e6704e8a023c..8658c64e2232 100644 --- a/src/pybind/mgr/cephadm/module.py +++ b/src/pybind/mgr/cephadm/module.py @@ -3875,6 +3875,30 @@ Then run the following: ) return cert_warning + def _is_adding_nvmeof_group_to_existing_service(self, nvmeof_spec: NvmeofServiceSpec) -> bool: + """ + Check whether this spec is assigning a group to an existing NVMe-oF + service that does not have a group yet. + + This allows an existing service to keep its current service_id while adding + a group for the first time. New services and services that already have a + group must still follow the normal service_id/group validation. + """ + existing_spec = self.spec_store.active_specs.get(nvmeof_spec.service_name()) + if existing_spec is None: + return False + + if existing_spec.service_type != 'nvmeof': + return False + + existing_nvmeof_spec = cast(NvmeofServiceSpec, existing_spec) + + return ( + existing_nvmeof_spec.service_id == nvmeof_spec.service_id + and not existing_nvmeof_spec.group + and bool(nvmeof_spec.group) + ) + def _apply_service_spec(self, spec: ServiceSpec) -> str: if spec.placement.is_empty(): # fill in default placement @@ -3932,11 +3956,13 @@ Then run the following: except OrchestratorError as e: self.log.debug(f"{e}") raise - nvmeof_spec = cast(NvmeofServiceSpec, spec) assert nvmeof_spec.service_id is not None # for mypy if nvmeof_spec.group and not nvmeof_spec.service_id.endswith(nvmeof_spec.group): - raise OrchestratorError("The 'nvmeof' service id/name must end with '.'. Found " - f"group name '{nvmeof_spec.group}' and service id '{nvmeof_spec.service_id}'") + if not self._is_adding_nvmeof_group_to_existing_service(nvmeof_spec): + raise OrchestratorError( + "The 'nvmeof' service id/name must end with '.'. Found " + f"group name '{nvmeof_spec.group}' and service id '{nvmeof_spec.service_id}'" + ) for sspec in [s.spec for s in self.spec_store.get_by_service_type('nvmeof')]: nspec = cast(NvmeofServiceSpec, sspec) if nvmeof_spec.group == nspec.group and nvmeof_spec.service_id != nspec.service_id: -- 2.47.3