]>
Commit | Line | Data |
---|---|---|
2a845540 TL |
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'} |