diff options
Diffstat (limited to 'src/pybind/mgr/cephadm/tests/test_tuned_profiles.py')
-rw-r--r-- | src/pybind/mgr/cephadm/tests/test_tuned_profiles.py | 256 |
1 files changed, 256 insertions, 0 deletions
diff --git a/src/pybind/mgr/cephadm/tests/test_tuned_profiles.py b/src/pybind/mgr/cephadm/tests/test_tuned_profiles.py new file mode 100644 index 000000000..66feaee31 --- /dev/null +++ b/src/pybind/mgr/cephadm/tests/test_tuned_profiles.py @@ -0,0 +1,256 @@ +import pytest +import json +from tests import mock +from cephadm.tuned_profiles import TunedProfileUtils, SYSCTL_DIR +from cephadm.inventory import TunedProfileStore +from ceph.utils import datetime_now +from ceph.deployment.service_spec import TunedProfileSpec, PlacementSpec +from cephadm.ssh import SSHManager +from orchestrator import HostSpec + +from typing import List, Dict + + +class SaveError(Exception): + pass + + +class FakeCache: + def __init__(self, + hosts, + schedulable_hosts, + unreachable_hosts): + self.hosts = hosts + self.unreachable_hosts = [HostSpec(h) for h in unreachable_hosts] + self.schedulable_hosts = [HostSpec(h) for h in schedulable_hosts] + self.last_tuned_profile_update = {} + + def get_hosts(self): + return self.hosts + + def get_schedulable_hosts(self): + return self.schedulable_hosts + + def get_unreachable_hosts(self): + return self.unreachable_hosts + + def get_draining_hosts(self): + return [] + + def is_host_unreachable(self, hostname: str): + return hostname in [h.hostname for h in self.get_unreachable_hosts()] + + def is_host_schedulable(self, hostname: str): + return hostname in [h.hostname for h in self.get_schedulable_hosts()] + + def is_host_draining(self, hostname: str): + return hostname in [h.hostname for h in self.get_draining_hosts()] + + @property + def networks(self): + return {h: {'a': {'b': ['c']}} for h in self.hosts} + + def host_needs_tuned_profile_update(self, host, profile_name): + return profile_name == 'p2' + + +class FakeMgr: + def __init__(self, + hosts: List[str], + schedulable_hosts: List[str], + unreachable_hosts: List[str], + profiles: Dict[str, TunedProfileSpec]): + self.cache = FakeCache(hosts, schedulable_hosts, unreachable_hosts) + self.tuned_profiles = TunedProfileStore(self) + self.tuned_profiles.profiles = profiles + self.ssh = SSHManager(self) + self.offline_hosts = [] + self.log_refresh_metadata = False + + def set_store(self, what: str, value: str): + raise SaveError(f'{what}: {value}') + + def get_store(self, what: str): + if what == 'tuned_profiles': + return json.dumps({'x': TunedProfileSpec('x', + PlacementSpec(hosts=['x']), + {'x': 'x'}).to_json(), + 'y': TunedProfileSpec('y', + PlacementSpec(hosts=['y']), + {'y': 'y'}).to_json()}) + return '' + + +class TestTunedProfiles: + tspec1 = TunedProfileSpec('p1', + PlacementSpec(hosts=['a', 'b', 'c']), + {'setting1': 'value1', + 'setting2': 'value2', + 'setting with space': 'value with space'}) + tspec2 = TunedProfileSpec('p2', + PlacementSpec(hosts=['a', 'c']), + {'something': 'something_else', + 'high': '5'}) + tspec3 = TunedProfileSpec('p3', + PlacementSpec(hosts=['c']), + {'wow': 'wow2', + 'setting with space': 'value with space', + 'down': 'low'}) + + def profiles_to_calls(self, tp: TunedProfileUtils, profiles: List[TunedProfileSpec]) -> List[Dict[str, str]]: + # this function takes a list of tuned profiles and returns a mapping from + # profile names to the string that will be written to the actual config file on the host. + res = [] + for p in profiles: + p_str = tp._profile_to_str(p) + res.append({p.profile_name: p_str}) + return res + + @mock.patch("cephadm.tuned_profiles.TunedProfileUtils._remove_stray_tuned_profiles") + @mock.patch("cephadm.tuned_profiles.TunedProfileUtils._write_tuned_profiles") + def test_write_all_tuned_profiles(self, _write_profiles, _rm_profiles): + profiles = {'p1': self.tspec1, 'p2': self.tspec2, 'p3': self.tspec3} + mgr = FakeMgr(['a', 'b', 'c'], + ['a', 'b', 'c'], + [], + profiles) + tp = TunedProfileUtils(mgr) + tp._write_all_tuned_profiles() + # need to check that _write_tuned_profiles is correctly called with the + # profiles that match the tuned profile placements and with the correct + # strings that should be generated from the settings the profiles have. + # the _profiles_to_calls helper allows us to generated the input we + # should check against + calls = [ + mock.call('a', self.profiles_to_calls(tp, [self.tspec1, self.tspec2])), + mock.call('b', self.profiles_to_calls(tp, [self.tspec1])), + mock.call('c', self.profiles_to_calls(tp, [self.tspec1, self.tspec2, self.tspec3])) + ] + _write_profiles.assert_has_calls(calls, any_order=True) + + @mock.patch('cephadm.ssh.SSHManager.check_execute_command') + def test_rm_stray_tuned_profiles(self, _check_execute_command): + profiles = {'p1': self.tspec1, 'p2': self.tspec2, 'p3': self.tspec3} + # for this test, going to use host "a" and put 4 cephadm generated + # profiles "p1" "p2", "p3" and "who" only two of which should be there ("p1", "p2") + # as well as a file not generated by cephadm. Only the "p3" and "who" + # profiles should be removed from the host. This should total to 4 + # calls to check_execute_command, 1 "ls", 2 "rm", and 1 "sysctl --system" + _check_execute_command.return_value = '\n'.join(['p1-cephadm-tuned-profile.conf', + 'p2-cephadm-tuned-profile.conf', + 'p3-cephadm-tuned-profile.conf', + 'who-cephadm-tuned-profile.conf', + 'dont-touch-me']) + mgr = FakeMgr(['a', 'b', 'c'], + ['a', 'b', 'c'], + [], + profiles) + tp = TunedProfileUtils(mgr) + tp._remove_stray_tuned_profiles('a', self.profiles_to_calls(tp, [self.tspec1, self.tspec2])) + calls = [ + mock.call('a', ['ls', SYSCTL_DIR], log_command=False), + mock.call('a', ['rm', '-f', f'{SYSCTL_DIR}/p3-cephadm-tuned-profile.conf']), + mock.call('a', ['rm', '-f', f'{SYSCTL_DIR}/who-cephadm-tuned-profile.conf']), + mock.call('a', ['sysctl', '--system']) + ] + _check_execute_command.assert_has_calls(calls, any_order=True) + + @mock.patch('cephadm.ssh.SSHManager.check_execute_command') + @mock.patch('cephadm.ssh.SSHManager.write_remote_file') + def test_write_tuned_profiles(self, _write_remote_file, _check_execute_command): + profiles = {'p1': self.tspec1, 'p2': self.tspec2, 'p3': self.tspec3} + # for this test we will use host "a" and have it so host_needs_tuned_profile_update + # returns True for p2 and False for p1 (see FakeCache class). So we should see + # 2 ssh calls, one to write p2, one to run sysctl --system + _check_execute_command.return_value = 'success' + _write_remote_file.return_value = 'success' + mgr = FakeMgr(['a', 'b', 'c'], + ['a', 'b', 'c'], + [], + profiles) + tp = TunedProfileUtils(mgr) + tp._write_tuned_profiles('a', self.profiles_to_calls(tp, [self.tspec1, self.tspec2])) + _check_execute_command.assert_called_with('a', ['sysctl', '--system']) + _write_remote_file.assert_called_with( + 'a', f'{SYSCTL_DIR}/p2-cephadm-tuned-profile.conf', tp._profile_to_str(self.tspec2).encode('utf-8')) + + def test_dont_write_to_unreachable_hosts(self): + profiles = {'p1': self.tspec1, 'p2': self.tspec2, 'p3': self.tspec3} + + # list host "a" and "b" as hosts that exist, "a" will be + # a normal, schedulable host and "b" is considered unreachable + mgr = FakeMgr(['a', 'b'], + ['a'], + ['b'], + profiles) + tp = TunedProfileUtils(mgr) + + assert 'a' not in tp.mgr.cache.last_tuned_profile_update + assert 'b' not in tp.mgr.cache.last_tuned_profile_update + + # with an online host, should proceed as normal. Providing + # no actual profiles here though so the only actual action taken + # is updating the entry in the last_tuned_profile_update dict + tp._write_tuned_profiles('a', {}) + assert 'a' in tp.mgr.cache.last_tuned_profile_update + + # trying to write to an unreachable host should be a no-op + # and return immediately. No entry for 'b' should be added + # to the last_tuned_profile_update dict + tp._write_tuned_profiles('b', {}) + assert 'b' not in tp.mgr.cache.last_tuned_profile_update + + def test_store(self): + mgr = FakeMgr(['a', 'b', 'c'], + ['a', 'b', 'c'], + [], + {}) + tps = TunedProfileStore(mgr) + save_str_p1 = 'tuned_profiles: ' + json.dumps({'p1': self.tspec1.to_json()}) + tspec1_updated = self.tspec1.copy() + tspec1_updated.settings.update({'new-setting': 'new-value'}) + save_str_p1_updated = 'tuned_profiles: ' + json.dumps({'p1': tspec1_updated.to_json()}) + save_str_p1_updated_p2 = 'tuned_profiles: ' + \ + json.dumps({'p1': tspec1_updated.to_json(), 'p2': self.tspec2.to_json()}) + tspec2_updated = self.tspec2.copy() + tspec2_updated.settings.pop('something') + save_str_p1_updated_p2_updated = 'tuned_profiles: ' + \ + json.dumps({'p1': tspec1_updated.to_json(), 'p2': tspec2_updated.to_json()}) + save_str_p2_updated = 'tuned_profiles: ' + json.dumps({'p2': tspec2_updated.to_json()}) + with pytest.raises(SaveError) as e: + tps.add_profile(self.tspec1) + assert str(e.value) == save_str_p1 + assert 'p1' in tps + with pytest.raises(SaveError) as e: + tps.add_setting('p1', 'new-setting', 'new-value') + assert str(e.value) == save_str_p1_updated + assert 'new-setting' in tps.list_profiles()[0].settings + with pytest.raises(SaveError) as e: + tps.add_profile(self.tspec2) + assert str(e.value) == save_str_p1_updated_p2 + assert 'p2' in tps + assert 'something' in tps.list_profiles()[1].settings + with pytest.raises(SaveError) as e: + tps.rm_setting('p2', 'something') + assert 'something' not in tps.list_profiles()[1].settings + assert str(e.value) == save_str_p1_updated_p2_updated + with pytest.raises(SaveError) as e: + tps.rm_profile('p1') + assert str(e.value) == save_str_p2_updated + assert 'p1' not in tps + assert 'p2' in tps + assert len(tps.list_profiles()) == 1 + assert tps.list_profiles()[0].profile_name == 'p2' + + cur_last_updated = tps.last_updated('p2') + new_last_updated = datetime_now() + assert cur_last_updated != new_last_updated + tps.set_last_updated('p2', new_last_updated) + assert tps.last_updated('p2') == new_last_updated + + # check FakeMgr get_store func to see what is expected to be found in Key Store here + tps.load() + assert 'x' in tps + assert 'y' in tps + assert [p for p in tps.list_profiles() if p.profile_name == 'x'][0].settings == {'x': 'x'} + assert [p for p in tps.list_profiles() if p.profile_name == 'y'][0].settings == {'y': 'y'} |