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
11 from typing
import List
, Dict
14 class SaveError(Exception):
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
= {}
31 def get_schedulable_hosts(self
):
32 return self
.schedulable_hosts
34 def get_unreachable_hosts(self
):
35 return self
.unreachable_hosts
37 def get_draining_hosts(self
):
42 return {h
: {'a': {'b': ['c']}} for h
in self
.hosts
}
44 def host_needs_tuned_profile_update(self
, host
, profile_name
):
45 return profile_name
== 'p2'
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
= []
60 def set_store(self
, what
: str, value
: str):
61 raise SaveError(f
'{what}: {value}')
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()})
74 class TestTunedProfiles
:
75 tspec1
= TunedProfileSpec('p1',
76 PlacementSpec(hosts
=['a', 'b', 'c']),
77 {'setting1': 'value1',
79 'setting with space': 'value with space'})
80 tspec2
= TunedProfileSpec('p2',
81 PlacementSpec(hosts
=['a', 'c']),
82 {'something': 'something_else',
84 tspec3
= TunedProfileSpec('p3',
85 PlacementSpec(hosts
=['c']),
87 'setting with space': 'value with space',
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.
95 p_str
= tp
._profile
_to
_str
(p
)
96 res
.append({p
.profile_name
: p_str
})
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'],
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
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
]))
119 _write_profiles
.assert_has_calls(calls
, any_order
=True)
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',
134 mgr
= FakeMgr(['a', 'b', 'c'],
138 tp
= TunedProfileUtils(mgr
)
139 tp
._remove
_stray
_tuned
_profiles
('a', self
.profiles_to_calls(tp
, [self
.tspec1
, self
.tspec2
]))
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'])
146 _check_execute_command
.assert_has_calls(calls
, any_order
=True)
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'],
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'))
167 def test_store(self
):
168 mgr
= FakeMgr(['a', 'b', 'c'],
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
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
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
:
203 assert str(e
.value
) == save_str_p2_updated
204 assert 'p1' not in tps
206 assert len(tps
.list_profiles()) == 1
207 assert tps
.list_profiles()[0].profile_name
== 'p2'
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
215 # check FakeMgr get_store func to see what is expected to be found in Key Store here
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'}