]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/cephadm/tests/test_tuned_profiles.py
1521b01105231d6b92cbc4404f5e91f076c11832
[ceph.git] / ceph / src / pybind / mgr / cephadm / tests / test_tuned_profiles.py
1 import pytest
2 import json
3 from tests import mock
4 from cephadm.tuned_profiles import TunedProfileUtils, SYSCTL_DIR
5 from cephadm.inventory import TunedProfileStore
6 from ceph.utils import datetime_now
7 from ceph.deployment.service_spec import TunedProfileSpec, PlacementSpec
8 from cephadm.ssh import SSHManager
9 from orchestrator import HostSpec
10
11 from typing import List, Dict
12
13
14 class SaveError(Exception):
15 pass
16
17
18 class FakeCache:
19 def __init__(self,
20 hosts,
21 schedulable_hosts,
22 unreachable_hosts):
23 self.hosts = hosts
24 self.unreachable_hosts = [HostSpec(h) for h in unreachable_hosts]
25 self.schedulable_hosts = [HostSpec(h) for h in schedulable_hosts]
26 self.last_tuned_profile_update = {}
27
28 def get_hosts(self):
29 return self.hosts
30
31 def get_schedulable_hosts(self):
32 return self.schedulable_hosts
33
34 def get_unreachable_hosts(self):
35 return self.unreachable_hosts
36
37 def get_draining_hosts(self):
38 return []
39
40 @property
41 def networks(self):
42 return {h: {'a': {'b': ['c']}} for h in self.hosts}
43
44 def host_needs_tuned_profile_update(self, host, profile_name):
45 return profile_name == 'p2'
46
47
48 class FakeMgr:
49 def __init__(self,
50 hosts: List[str],
51 schedulable_hosts: List[str],
52 unreachable_hosts: List[str],
53 profiles: Dict[str, TunedProfileSpec]):
54 self.cache = FakeCache(hosts, schedulable_hosts, unreachable_hosts)
55 self.tuned_profiles = TunedProfileStore(self)
56 self.tuned_profiles.profiles = profiles
57 self.ssh = SSHManager(self)
58 self.offline_hosts = []
59
60 def set_store(self, what: str, value: str):
61 raise SaveError(f'{what}: {value}')
62
63 def get_store(self, what: str):
64 if what == 'tuned_profiles':
65 return json.dumps({'x': TunedProfileSpec('x',
66 PlacementSpec(hosts=['x']),
67 {'x': 'x'}).to_json(),
68 'y': TunedProfileSpec('y',
69 PlacementSpec(hosts=['y']),
70 {'y': 'y'}).to_json()})
71 return ''
72
73
74 class TestTunedProfiles:
75 tspec1 = TunedProfileSpec('p1',
76 PlacementSpec(hosts=['a', 'b', 'c']),
77 {'setting1': 'value1',
78 'setting2': 'value2',
79 'setting with space': 'value with space'})
80 tspec2 = TunedProfileSpec('p2',
81 PlacementSpec(hosts=['a', 'c']),
82 {'something': 'something_else',
83 'high': '5'})
84 tspec3 = TunedProfileSpec('p3',
85 PlacementSpec(hosts=['c']),
86 {'wow': 'wow2',
87 'setting with space': 'value with space',
88 'down': 'low'})
89
90 def profiles_to_calls(self, tp: TunedProfileUtils, profiles: List[TunedProfileSpec]) -> List[Dict[str, str]]:
91 # this function takes a list of tuned profiles and returns a mapping from
92 # profile names to the string that will be written to the actual config file on the host.
93 res = []
94 for p in profiles:
95 p_str = tp._profile_to_str(p)
96 res.append({p.profile_name: p_str})
97 return res
98
99 @mock.patch("cephadm.tuned_profiles.TunedProfileUtils._remove_stray_tuned_profiles")
100 @mock.patch("cephadm.tuned_profiles.TunedProfileUtils._write_tuned_profiles")
101 def test_write_all_tuned_profiles(self, _write_profiles, _rm_profiles):
102 profiles = {'p1': self.tspec1, 'p2': self.tspec2, 'p3': self.tspec3}
103 mgr = FakeMgr(['a', 'b', 'c'],
104 ['a', 'b', 'c'],
105 [],
106 profiles)
107 tp = TunedProfileUtils(mgr)
108 tp._write_all_tuned_profiles()
109 # need to check that _write_tuned_profiles is correctly called with the
110 # profiles that match the tuned profile placements and with the correct
111 # strings that should be generated from the settings the profiles have.
112 # the _profiles_to_calls helper allows us to generated the input we
113 # should check against
114 calls = [
115 mock.call('a', self.profiles_to_calls(tp, [self.tspec1, self.tspec2])),
116 mock.call('b', self.profiles_to_calls(tp, [self.tspec1])),
117 mock.call('c', self.profiles_to_calls(tp, [self.tspec1, self.tspec2, self.tspec3]))
118 ]
119 _write_profiles.assert_has_calls(calls, any_order=True)
120
121 @mock.patch('cephadm.ssh.SSHManager.check_execute_command')
122 def test_rm_stray_tuned_profiles(self, _check_execute_command):
123 profiles = {'p1': self.tspec1, 'p2': self.tspec2, 'p3': self.tspec3}
124 # for this test, going to use host "a" and put 4 cephadm generated
125 # profiles "p1" "p2", "p3" and "who" only two of which should be there ("p1", "p2")
126 # as well as a file not generated by cephadm. Only the "p3" and "who"
127 # profiles should be removed from the host. This should total to 4
128 # calls to check_execute_command, 1 "ls", 2 "rm", and 1 "sysctl --system"
129 _check_execute_command.return_value = '\n'.join(['p1-cephadm-tuned-profile.conf',
130 'p2-cephadm-tuned-profile.conf',
131 'p3-cephadm-tuned-profile.conf',
132 'who-cephadm-tuned-profile.conf',
133 'dont-touch-me'])
134 mgr = FakeMgr(['a', 'b', 'c'],
135 ['a', 'b', 'c'],
136 [],
137 profiles)
138 tp = TunedProfileUtils(mgr)
139 tp._remove_stray_tuned_profiles('a', self.profiles_to_calls(tp, [self.tspec1, self.tspec2]))
140 calls = [
141 mock.call('a', ['ls', SYSCTL_DIR]),
142 mock.call('a', ['rm', '-f', f'{SYSCTL_DIR}/p3-cephadm-tuned-profile.conf']),
143 mock.call('a', ['rm', '-f', f'{SYSCTL_DIR}/who-cephadm-tuned-profile.conf']),
144 mock.call('a', ['sysctl', '--system'])
145 ]
146 _check_execute_command.assert_has_calls(calls, any_order=True)
147
148 @mock.patch('cephadm.ssh.SSHManager.check_execute_command')
149 @mock.patch('cephadm.ssh.SSHManager.write_remote_file')
150 def test_write_tuned_profiles(self, _write_remote_file, _check_execute_command):
151 profiles = {'p1': self.tspec1, 'p2': self.tspec2, 'p3': self.tspec3}
152 # for this test we will use host "a" and have it so host_needs_tuned_profile_update
153 # returns True for p2 and False for p1 (see FakeCache class). So we should see
154 # 2 ssh calls, one to write p2, one to run sysctl --system
155 _check_execute_command.return_value = 'success'
156 _write_remote_file.return_value = 'success'
157 mgr = FakeMgr(['a', 'b', 'c'],
158 ['a', 'b', 'c'],
159 [],
160 profiles)
161 tp = TunedProfileUtils(mgr)
162 tp._write_tuned_profiles('a', self.profiles_to_calls(tp, [self.tspec1, self.tspec2]))
163 _check_execute_command.assert_called_with('a', ['sysctl', '--system'])
164 _write_remote_file.assert_called_with(
165 'a', f'{SYSCTL_DIR}/p2-cephadm-tuned-profile.conf', tp._profile_to_str(self.tspec2).encode('utf-8'))
166
167 def test_store(self):
168 mgr = FakeMgr(['a', 'b', 'c'],
169 ['a', 'b', 'c'],
170 [],
171 {})
172 tps = TunedProfileStore(mgr)
173 save_str_p1 = 'tuned_profiles: ' + json.dumps({'p1': self.tspec1.to_json()})
174 tspec1_updated = self.tspec1.copy()
175 tspec1_updated.settings.update({'new-setting': 'new-value'})
176 save_str_p1_updated = 'tuned_profiles: ' + json.dumps({'p1': tspec1_updated.to_json()})
177 save_str_p1_updated_p2 = 'tuned_profiles: ' + \
178 json.dumps({'p1': tspec1_updated.to_json(), 'p2': self.tspec2.to_json()})
179 tspec2_updated = self.tspec2.copy()
180 tspec2_updated.settings.pop('something')
181 save_str_p1_updated_p2_updated = 'tuned_profiles: ' + \
182 json.dumps({'p1': tspec1_updated.to_json(), 'p2': tspec2_updated.to_json()})
183 save_str_p2_updated = 'tuned_profiles: ' + json.dumps({'p2': tspec2_updated.to_json()})
184 with pytest.raises(SaveError) as e:
185 tps.add_profile(self.tspec1)
186 assert str(e.value) == save_str_p1
187 assert 'p1' in tps
188 with pytest.raises(SaveError) as e:
189 tps.add_setting('p1', 'new-setting', 'new-value')
190 assert str(e.value) == save_str_p1_updated
191 assert 'new-setting' in tps.list_profiles()[0].settings
192 with pytest.raises(SaveError) as e:
193 tps.add_profile(self.tspec2)
194 assert str(e.value) == save_str_p1_updated_p2
195 assert 'p2' in tps
196 assert 'something' in tps.list_profiles()[1].settings
197 with pytest.raises(SaveError) as e:
198 tps.rm_setting('p2', 'something')
199 assert 'something' not in tps.list_profiles()[1].settings
200 assert str(e.value) == save_str_p1_updated_p2_updated
201 with pytest.raises(SaveError) as e:
202 tps.rm_profile('p1')
203 assert str(e.value) == save_str_p2_updated
204 assert 'p1' not in tps
205 assert 'p2' in tps
206 assert len(tps.list_profiles()) == 1
207 assert tps.list_profiles()[0].profile_name == 'p2'
208
209 cur_last_updated = tps.last_updated('p2')
210 new_last_updated = datetime_now()
211 assert cur_last_updated != new_last_updated
212 tps.set_last_updated('p2', new_last_updated)
213 assert tps.last_updated('p2') == new_last_updated
214
215 # check FakeMgr get_store func to see what is expected to be found in Key Store here
216 tps.load()
217 assert 'x' in tps
218 assert 'y' in tps
219 assert [p for p in tps.list_profiles() if p.profile_name == 'x'][0].settings == {'x': 'x'}
220 assert [p for p in tps.list_profiles() if p.profile_name == 'y'][0].settings == {'y': 'y'}