diff --git a/src/pybind/mgr/cephadm/module.py b/src/pybind/mgr/cephadm/module.py index e3d90f13aebd24..c7330933a4d564 100644 --- a/src/pybind/mgr/cephadm/module.py +++ b/src/pybind/mgr/cephadm/module.py @@ -2535,10 +2535,43 @@ def _apply(self, spec: GenericSpec) -> str: return self._apply_service_spec(cast(ServiceSpec, spec)) + def _get_candidate_hosts(self, placement: PlacementSpec) -> List[str]: + """Return a list of candidate hosts according to the placement specification.""" + all_hosts = self.cache.get_schedulable_hosts() + draining_hosts = [dh.hostname for dh in self.cache.get_draining_hosts()] + candidates = [] + if placement.hosts: + candidates = [h.hostname for h in placement.hosts if h.hostname in placement.hosts] + elif placement.label: + candidates = [x.hostname for x in [h for h in all_hosts if placement.label in h.labels]] + elif placement.host_pattern: + candidates = [x for x in placement.filter_matching_hostspecs(all_hosts)] + elif (placement.count is not None or placement.count_per_host is not None): + candidates = [x.hostname for x in all_hosts] + return [h for h in candidates if h not in draining_hosts] + + def _validate_one_shot_placement_spec(self, spec: PlacementSpec) -> None: + """Validate placement specification for TunedProfileSpec and ClientKeyringSpec.""" + if spec.count is not None: + raise OrchestratorError("Placement 'count' field is no supported for this specification.") + if spec.count_per_host is not None: + raise OrchestratorError("Placement 'count_per_host' field is no supported for this specification.") + if spec.hosts: + all_hosts = [h.hostname for h in self.inventory.all_specs()] + invalid_hosts = [h.hostname for h in spec.hosts if h.hostname not in all_hosts] + if invalid_hosts: + raise OrchestratorError(f"Found invalid host(s) in placement section: {invalid_hosts}. " + f"Please check 'ceph orch host ls' for available hosts.") + elif not self._get_candidate_hosts(spec): + raise OrchestratorError("Invalid placement specification. No host(s) matched placement spec. " + "Please check 'ceph orch host ls' for available hosts." + "Note: draining hosts are excluded from the candidate list.") + @handle_orch_error def apply_tuned_profiles(self, specs: List[TunedProfileSpec], no_overwrite: bool = False) -> str: outs = [] for spec in specs: + self._validate_one_shot_placement_spec(spec.placement) if no_overwrite and self.tuned_profiles.exists(spec.profile_name): outs.append(f"Tuned profile '{spec.profile_name}' already exists (--no-overwrite was passed)") else: diff --git a/src/python-common/ceph/deployment/hostspec.py b/src/python-common/ceph/deployment/hostspec.py index cb7e4de3484248..9edd90ceea5cac 100644 --- a/src/python-common/ceph/deployment/hostspec.py +++ b/src/python-common/ceph/deployment/hostspec.py @@ -128,8 +128,10 @@ def __str__(self) -> str: return self.hostname def __eq__(self, other: Any) -> bool: + if not isinstance(other, HostSpec): + return NotImplemented # Let's omit `status` for the moment, as it is still the very same host. return self.hostname == other.hostname and \ - self.addr == other.addr and \ - sorted(self.labels) == sorted(other.labels) and \ - self.location == other.location + self.addr == other.addr and \ + sorted(self.labels) == sorted(other.labels) and \ + self.location == other.location