def __init__(self, ctx: CephadmContext):
self.ctx: CephadmContext = ctx
self.cpu_model: str = 'Unknown'
+ self.sysctl_options: Dict[str, str] = self._populate_sysctl_options()
self.cpu_count: int = 0
self.cpu_cores: int = 0
self.cpu_threads: int = 0
self._block_devices = self._get_block_devs()
self._device_list = self._get_device_info()
+ def _populate_sysctl_options(self) -> Dict[str, str]:
+ sysctl_options = {}
+ out, _, _ = call_throws(self.ctx, ['sysctl', '-a'], verbosity=CallVerbosity.QUIET_UNLESS_ERROR)
+ if out:
+ for line in out.splitlines():
+ option, value = line.split('=')
+ sysctl_options[option.strip()] = value.strip()
+ return sysctl_options
+
def _discover_enclosures(self) -> Dict[str, Enclosure]:
"""Build a dictionary of discovered scsi enclosures
else:
# for OSDs, we still need to update config, just not carry out the full
# prepare_create function
- daemon_spec.final_config, daemon_spec.deps = self.osd_service.generate_config(daemon_spec)
+ daemon_spec.final_config, daemon_spec.deps = self.osd_service.generate_config(
+ daemon_spec)
return self.wait_async(CephadmServe(self)._create_daemon(daemon_spec, reconfig=(action == 'reconfig')))
actions = {
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.\n"
+ "Please check 'ceph orch host ls' for available hosts.\n"
+ "Note: draining hosts are excluded from the candidate list.")
+
+ def _validate_tunedprofile_settings(self, spec: TunedProfileSpec) -> Dict[str, List[str]]:
+ candidate_hosts = spec.placement.filter_matching_hostspecs(self.inventory.all_specs())
+ invalid_options: Dict[str, List[str]] = {}
+ for host in candidate_hosts:
+ host_sysctl_options = self.cache.get_facts(host).get('sysctl_options', {})
+ invalid_options[host] = []
+ for option in spec.settings:
+ if option not in host_sysctl_options:
+ invalid_options[host].append(option)
+ return invalid_options
+
+ def _validate_tuned_profile_spec(self, spec: TunedProfileSpec) -> None:
+ if not spec.settings:
+ raise OrchestratorError("Invalid spec: settings section cannot be empty.")
+ self._validate_one_shot_placement_spec(spec.placement)
+ invalid_options = self._validate_tunedprofile_settings(spec)
+ if any(e for e in invalid_options.values()):
+ raise OrchestratorError(
+ f'Failed to apply tuned profile. Invalid sysctl option(s) for host(s) detected: {invalid_options}')
+
@handle_orch_error
def apply_tuned_profiles(self, specs: List[TunedProfileSpec], no_overwrite: bool = False) -> str:
outs = []
for spec in specs:
+ self._validate_tuned_profile_spec(spec)
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)")
+ outs.append(
+ f"Tuned profile '{spec.profile_name}' already exists (--no-overwrite was passed)")
else:
+ # done, let's save the specs
self.tuned_profiles.add_profile(spec)
outs.append(f'Saved tuned profile {spec.profile_name}')
self._kick_serve_loop()
'host': daemon_spec.host,
'device_enhanced_scan': str(self.mgr.device_enhanced_scan)}
- listener_cert, listener_key = agent.ssl_certs.generate_cert(self.mgr.inventory.get_addr(daemon_spec.host))
+ listener_cert, listener_key = agent.ssl_certs.generate_cert(
+ self.mgr.inventory.get_addr(daemon_spec.host))
config = {
'agent.json': json.dumps(cfg),
'keyring': daemon_spec.keyring,
with with_host(cephadm_module, 'test1', refresh_hosts=False, rm_with_force=True):
with with_host(cephadm_module, 'test2', refresh_hosts=False, rm_with_force=False):
cephadm_module.inventory.add_label('test2', '_admin')
+
+ @pytest.mark.parametrize("facts, settings, expected_value",
+ [
+ # All options are available on all hosts
+ (
+ {
+ "host1":
+ {
+ "sysctl_options":
+ {
+ 'opt1': 'val1',
+ 'opt2': 'val2',
+ }
+ },
+ "host2":
+ {
+ "sysctl_options":
+ {
+ 'opt1': '',
+ 'opt2': '',
+ }
+ },
+ },
+ {'opt1', 'opt2'}, # settings
+ {'host1': [], 'host2': []} # expected_value
+ ),
+ # opt1 is missing on host 1, opt2 is missing on host2
+ ({
+ "host1":
+ {
+ "sysctl_options":
+ {
+ 'opt2': '',
+ 'optX': '',
+ }
+ },
+ "host2":
+ {
+ "sysctl_options":
+ {
+ 'opt1': '',
+ 'opt3': '',
+ 'opt4': '',
+ }
+ },
+ },
+ {'opt1', 'opt2'}, # settings
+ {'host1': ['opt1'], 'host2': ['opt2']} # expected_value
+ ),
+ # All options are missing on all hosts
+ ({
+ "host1":
+ {
+ "sysctl_options":
+ {
+ }
+ },
+ "host2":
+ {
+ "sysctl_options":
+ {
+ }
+ },
+ },
+ {'opt1', 'opt2'}, # settings
+ {'host1': ['opt1', 'opt2'], 'host2': [
+ 'opt1', 'opt2']} # expected_value
+ ),
+ ]
+ )
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+ def test_tuned_profiles_settings_validation(self, facts, settings, expected_value, cephadm_module):
+ with with_host(cephadm_module, 'test'):
+ spec = mock.Mock()
+ spec.settings = sorted(settings)
+ spec.placement.filter_matching_hostspecs = mock.Mock()
+ spec.placement.filter_matching_hostspecs.return_value = ['host1', 'host2']
+ cephadm_module.cache.facts = facts
+ assert cephadm_module._validate_tunedprofile_settings(spec) == expected_value
+
+ @mock.patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('[]'))
+ def test_tuned_profiles_validation(self, cephadm_module):
+ with with_host(cephadm_module, 'test'):
+
+ with pytest.raises(OrchestratorError, match="^Invalid placement specification.+"):
+ spec = mock.Mock()
+ spec.settings = {'a': 'b'}
+ spec.placement = PlacementSpec(hosts=[])
+ cephadm_module._validate_tuned_profile_spec(spec)
+
+ with pytest.raises(OrchestratorError, match="Invalid spec: settings section cannot be empty."):
+ spec = mock.Mock()
+ spec.settings = {}
+ spec.placement = PlacementSpec(hosts=['host1', 'host2'])
+ cephadm_module._validate_tuned_profile_spec(spec)
+
+ with pytest.raises(OrchestratorError, match="^Placement 'count' field is no supported .+"):
+ spec = mock.Mock()
+ spec.settings = {'a': 'b'}
+ spec.placement = PlacementSpec(count=1)
+ cephadm_module._validate_tuned_profile_spec(spec)
+
+ with pytest.raises(OrchestratorError, match="^Placement 'count_per_host' field is no supported .+"):
+ spec = mock.Mock()
+ spec.settings = {'a': 'b'}
+ spec.placement = PlacementSpec(count_per_host=1, label='foo')
+ cephadm_module._validate_tuned_profile_spec(spec)
+
+ with pytest.raises(OrchestratorError, match="^Found invalid host"):
+ spec = mock.Mock()
+ spec.settings = {'a': 'b'}
+ spec.placement = PlacementSpec(hosts=['host1', 'host2'])
+ cephadm_module.inventory = mock.Mock()
+ cephadm_module.inventory.all_specs = mock.Mock(
+ return_value=[mock.Mock().hostname, mock.Mock().hostname])
+ cephadm_module._validate_tuned_profile_spec(spec)
def __eq__(self, other: Any) -> bool:
# Let's omit `status` for the moment, as it is still the very same host.
+ if not isinstance(other, HostSpec):
+ return NotImplemented
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