Merge ~dashmage/charm-sysconfig:systemd-reboot-notification into charm-sysconfig:master
- Git
- lp:~dashmage/charm-sysconfig
- systemd-reboot-notification
- Merge into master
Status: | Merged |
---|---|
Approved by: | Eric Chen |
Approved revision: | 8f715537bb7c9c9925be6aefb2dc9e725be82fe1 |
Merged at revision: | ec090fc3fd17648d455b838b57e284b11e28ec9f |
Proposed branch: | ~dashmage/charm-sysconfig:systemd-reboot-notification |
Merge into: | charm-sysconfig:master |
Diff against target: |
439 lines (+201/-85) 4 files modified
src/lib/lib_sysconfig.py (+43/-6) src/tests/functional/conftest.py (+101/-0) src/tests/functional/test_deploy.py (+18/-78) src/tests/unit/test_lib.py (+39/-1) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Andrea Ieri | Approve | ||
🤖 prod-jenkaas-bootstack (community) | continuous-integration | Approve | |
Eric Chen | Approve | ||
BootStack Reviewers | Pending | ||
Review via email: mp+450413@code.launchpad.net |
Commit message
Check for any actual changes before updating systemd conf file.
Description of the change
Currently when the install hook handler is executed, the systemd conf file rendered by the charm is installed on the machine. This change causes a false positive reboot notification to appear even where there are no changes performed by the charm during initial deployment.
In order to fix this, the systemd conf file is only updated during the install hook and config-changed hook after checking whether there's a logical change to the file. This comparison is done through the use of the configparser module.
Ashley James (dashmage) wrote : | # |
Ashley James (dashmage) wrote : | # |
```
$ juju deploy ./sysconfig-
$ juju config sysconfig-new cpu-affinity-
$ juju relate ubuntu sysconfig-new
```
Relating the subordinate sysconfig to the ubuntu principal *after* setting the config doesn't trigger the reboot notification. Only once the relation is joined and then config changed, would the if condition logic in the config-changed hook be executed and the reboot notification be displayed.
Andrea Ieri (aieri) wrote (last edit ): | # |
A few comments:
* setting config options at deploy time or subsequently *must* yield the same behavior, both in terms of messages and on-disk configuration
* the config-changed hook is run right after the install one (see[0]), although whether sysconfig correctly fires the config_changed() function when the config-changed hook is run is a separate issue
* the way I see it, being careful about when we do or we do not run the system.conf update function is very fragile. We should rather make update_
1. render the template, preferably in memory, or in a tempfile
2. load the rendered template in a configparser object
3. load system.conf in another configparser object
4. compare the two configparser objects; if they are different, mv the tempfile into place and trigger the reboot-required notification
I have done a very quick check with two semantically identical systemd.conf files (only the order of the keys changed) and yes, something like this does work:
```
c_old = configparser.
c_new = configparser.
c_old.read(
c_new.read(
c_old == c_new # returns True if the configs are semantically equal
```
[0] https:/
Ashley James (dashmage) wrote : | # |
Thanks for your suggestion @aieri.
> We should rather make update_
I think this is a good idea. I've implemented a new method that performs this check through configparser.
The only problem I see is the earlier issue I raised with setting the config during the time of deployment. After some testing, it looks like the config-changed event handler code is not being executed at all when set with "juju deploy --config x=y". So the reboot notification is not raised in this scenario.
Andrea Ieri (aieri) wrote : | # |
Here is a preliminary quick review. Please see the comments in-line. Additionally, yes, having the charm be able to apply config options during deployment is mandatory. I have no idea why the config_changed function isn't being called. You could run debug hooks on a stable revision and compare to your build.
Eric Chen (eric-chen) wrote : | # |
The naming between grub and systemd seems very different. It's really confusing and hard to understand the logic. Can we align the naming and logic?
in src/lib/
Grub:
- _assemble_
- update_grub_file: generate the temporary grub-file
- check_update_grub: compare final grub-file with the temporary grub-file
- update-grub: generate the final grub file
- check_grub_reboot: check if we need to reboot the server
Sysconfig
- _assemble_
- systemd_
- update_
- clear_systemd_
Ashley James (dashmage) wrote (last edit ): | # |
> The naming between grub and systemd seems very different. It's really
> confusing and hard to understand the logic. Can we align the naming and logic?
>
> in src/lib/
>
> Grub:
> - _assemble_
> - update_grub_file: generate the temporary grub-file
> - check_update_grub: compare final grub-file with the temporary grub-file
> - update-grub: generate the final grub file
> - check_grub_reboot: check if we need to reboot the server
>
> Sysconfig
> - _assemble_
> - systemd_
> systemd context
> - update_
> - clear_systemd_
We can't strictly align the naming/logic between grub and systemd. This is because for grub, the charm doesn't update the final config file (/boot/
Although I do agree with your point that the systemd method naming could be better to make it more understandable. Here is what I propose:
Systemd
- _assemble_
- update_
- check_update_
- check_systemd_
I've also added a comment to clarify the if condition check in the update_status handler.
Let me know if these changes look good to you @eric-chen.
Eric Chen (eric-chen) wrote : | # |
Other than the naming, an inline comment for the logic.
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
FAILED: Continuous integration, rev:8404b07fade
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Eric Chen (eric-chen) wrote : | # |
nitpick about the comment, other than that, LGTM
Eric Chen (eric-chen) wrote (last edit ): | # |
One more thing, we should add functional test
1. In a clean server, no reboot notification after simple deployment `juju deploy charm-sysconfig`
2. trigger reboot notificaiton for `juju deploy charm-sysconfig --config x=y`
3. trigger reboot notification after config changed `juju config xxxx`
Ashley James (dashmage) wrote : | # |
Updated functional tests. Couple of points to note:
* trigger reboot notification after config changed `juju config xxxx`: This case was already present in the existing functional tests.
* I've modified some of the older code pertaining to the deployment method - changed using "juju deploy" directly to using libjuju to perform the deployment. The reasoning for doing this earlier was that libjuju fails with deployment with JAAS according to https:/
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
PASSED: Continuous integration, rev:5055be39579
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Andrea Ieri (aieri) wrote : | # |
Overall lgtm, I have a few optional suggestions and a question about the test.
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
FAILED: Continuous integration, rev:bc456f2da21
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
FAILED: Continuous integration, rev:b0fd90864a6
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Ashley James (dashmage) wrote (last edit ): | # |
@aieri: Addressed your comments. Only one interesting behaviour about your nitpick with the ConfigParser initialization which I've responded inline.
In short, these 2 code snippets are not functionally the same which I didn't know before either.
```
config = ConfigParser()
config.
```
is not equivalent to
```
config = ConfigParser(
```
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
PASSED: Continuous integration, rev:058376cf6f0
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Andrea Ieri (aieri) wrote : | # |
> @aieri: Addressed your comments. Only one interesting behaviour about your
> nitpick with the ConfigParser initialization which I've responded inline.
>
> In short, these 2 code snippets are not functionally the same which I didn't
> know before either.
>
> ```
> config = ConfigParser()
> config.
> ```
>
> is not equivalent to
>
> ```
> config = ConfigParser(
> ```
I'm struggling to see your inline reply... where is it? It's neither attached to commit 5055be3 nor 058376c
Ashley James (dashmage) wrote : | # |
So I can see my inline responses attached to commit 5055be3. Not quite sure why it's not visible to others. In any case, let me paste those responses on this comment.
> [very nit-picky] since you're not dealing with long lines, you could skip lines 59 and 60 with
```
existing_config = ConfigParser(
new_config = ConfigParser(
```
I had initially made this change and couldn't figure out why the code wasn't working anymore. Turns out that
```
config = ConfigParser()
config.
```
is not equivalent to
```
config = ConfigParser(
```
The first code snippet correctly returns a ConfigParser object whereas the second one (where we directly call the read method of a newly created ConfigParser object) returns a list object with the name of the file (['config.txt']).
Even the python docs (https:/
Similarly for the `read_string` method, if we directly call it like so `ConfigParser(
So we do need to declare the ConfigParser object explicitly before any usage. I'm surprised this caveat isn't mentioned anywhere in the docs.
-----
> there are only two places where you call update_
(...)
This makes a lot of sense and the code is much cleaner. Thanks for the suggestion!
-----
> I am confused... where is the assertion? TBH I'm confused by the old test too, how did it work?
(...)
The old test didn't seem like it tests anything but just gets sysconfig deployed and ensures that the unit has the right status at the end.
My intention with the second sysconfig app was to test for the deployment scenario where we perform "juju deploy xyz --config x=y" and config options were defined at deployment time itself and not after that through an explicit "juju config" command. I added the second sysconfig deployment in the same test in order to have them both deploy parallelly and not have execution be blocked (with the model.block_until call) waiting for one sysconfig app to finish deploying before the other one starts.
I've now added a try-except block checking if the timeout has reached in case the app status is not what's expected.
-----
> nit: do you need to serialize adding the relation and setting the enable-container config? If you don't, you could let both tasks run asynchronously: just create them and gather both at the same time.
Good idea, let me do that.
Andrea Ieri (aieri) wrote : | # |
TIL! I had run some small tests on my side but did not catch that `config = ConfigParser(
Regarding the functional tests:
* perhaps the idea was that the block_until would eventually time out if the app doesn't deploy correctly, and thus fail the test. But it's a weird way to write a test, using an explicit assertion would be better
* I understand your desire to start building two apps in parallel to save time, but tests should really have a narrow scope and only test one thing at a time. What you could perhaps do is create the apps as fixtures (potentially in parallel), and run the checks you need in the test code.
* I also have the suspicion that the tests that come after test_sysconfig_
Ashley James (dashmage) wrote : | # |
Thanks for the latest round of suggestions!
I've pushed a new commit with changes to move the deployment work over to conftest.py and also enable running each of the tests independently. The deployments happen parallelly by setting the deployment fixtures to "autouse" so they both deploy at the start of the test. Both the deployments have separate tests which check whether the timeout exception has been raised while blocking the model and checking for a particular status.
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
FAILED: Continuous integration, rev:658ea4d68fa
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
PASSED: Continuous integration, rev:8f715537bb7
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Andrea Ieri (aieri) wrote : | # |
thank you, this is a lot cleaner!
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Change successfully merged at revision ec090fc3fd17648
Preview Diff
1 | diff --git a/src/lib/lib_sysconfig.py b/src/lib/lib_sysconfig.py |
2 | index 83b5dd6..63ebec9 100644 |
3 | --- a/src/lib/lib_sysconfig.py |
4 | +++ b/src/lib/lib_sysconfig.py |
5 | @@ -7,6 +7,7 @@ import hashlib |
6 | import os |
7 | import re |
8 | import subprocess |
9 | +from configparser import ConfigParser |
10 | from datetime import datetime, timedelta, timezone |
11 | |
12 | import charmhelpers.core.sysctl as sysctl |
13 | @@ -469,7 +470,7 @@ class SysConfigHelper: |
14 | |
15 | return valid |
16 | |
17 | - def _assemble_context(self): |
18 | + def _assemble_grub_context(self): |
19 | context = {} |
20 | |
21 | # The isolcpus boot option can be used to isolate one or more CPUs at |
22 | @@ -519,14 +520,14 @@ class SysConfigHelper: |
23 | |
24 | Will call update-grub if update-grub config is set to True. |
25 | """ |
26 | - context = self._assemble_context() |
27 | + context = self._assemble_grub_context() |
28 | self._render_boot_resource(GRUB_CONF_TMPL, GRUB_CONF, context) |
29 | hookenv.log("grub configuration updated") |
30 | self._update_grub() |
31 | |
32 | - def update_systemd_system_file(self): |
33 | - """Update /etc/systemd/system.conf according to charm configuration.""" |
34 | + def _assemble_systemd_context(self): |
35 | context = {} |
36 | + |
37 | if self.cpu_affinity_range: |
38 | context["cpu_affinity_range"] = self.cpu_affinity_range |
39 | |
40 | @@ -538,9 +539,45 @@ class SysConfigHelper: |
41 | context["systemd_config_flags"] = parse_config_flags( |
42 | self.config_flags.get("systemd", "") |
43 | ) |
44 | + return context |
45 | |
46 | - self._render_boot_resource(SYSTEMD_SYSTEM_TMPL, SYSTEMD_SYSTEM, context) |
47 | - hookenv.log("systemd configuration updated") |
48 | + def _systemd_update_available(self, context): |
49 | + """Compare the systemd system.conf file with the one rendered by the charm. |
50 | + |
51 | + This method renders the systemd conf file in memory along with the configured |
52 | + values (if any) and then compares with the existing system.conf file. |
53 | + |
54 | + Returns True in case there are changes present between the files. |
55 | + """ |
56 | + hookenv.log( |
57 | + "Checking for changes to /etc/systemd/system.conf.", |
58 | + hookenv.DEBUG, |
59 | + ) |
60 | + |
61 | + new_config = ConfigParser() |
62 | + existing_config = ConfigParser() |
63 | + |
64 | + render_output = render( |
65 | + source=SYSTEMD_SYSTEM_TMPL, |
66 | + templates_dir="templates", |
67 | + target=None, |
68 | + context=context, |
69 | + ) |
70 | + |
71 | + existing_config.read(SYSTEMD_SYSTEM) |
72 | + new_config.read_string(render_output) |
73 | + |
74 | + return existing_config != new_config |
75 | + |
76 | + def update_systemd_system_file(self): |
77 | + """Update /etc/systemd/system.conf according to charm configuration.""" |
78 | + context = self._assemble_systemd_context() |
79 | + # update systemd config only if there's an actual change |
80 | + if self._systemd_update_available(context): |
81 | + self._render_boot_resource(SYSTEMD_SYSTEM_TMPL, SYSTEMD_SYSTEM, context) |
82 | + hookenv.log("systemd configuration updated") |
83 | + else: |
84 | + hookenv.log("no systemd configuration changes, not rendering file") |
85 | |
86 | def update_systemd_resolved(self): |
87 | """Update /etc/systemd/resolved.conf according to charm configuration.""" |
88 | diff --git a/src/tests/functional/conftest.py b/src/tests/functional/conftest.py |
89 | index 672e1c3..317b398 100644 |
90 | --- a/src/tests/functional/conftest.py |
91 | +++ b/src/tests/functional/conftest.py |
92 | @@ -17,6 +17,13 @@ import pytest_asyncio |
93 | from juju.controller import Controller |
94 | from juju_tools import JujuTools |
95 | |
96 | +charm_location = os.getenv("CHARM_LOCATION", "..").rstrip("/") |
97 | +charm_name = os.getenv("CHARM_NAME", "sysconfig") |
98 | +series = ["jammy", "focal", "bionic"] |
99 | +sources = [("local", "{}/{}.charm".format(charm_location, charm_name))] |
100 | + |
101 | +PRINCIPAL_APP_NAME = "ubuntu-{}" |
102 | + |
103 | |
104 | @pytest_asyncio.fixture(scope="module") |
105 | def event_loop(): |
106 | @@ -62,6 +69,100 @@ async def model(controller): |
107 | await asyncio.sleep(1) |
108 | |
109 | |
110 | +@pytest_asyncio.fixture(scope="module", params=series) |
111 | +def series(request): |
112 | + """Return ubuntu version (i.e. xenial) in use in the test.""" |
113 | + return request.param |
114 | + |
115 | + |
116 | +@pytest_asyncio.fixture(scope="module", params=sources, ids=[s[0] for s in sources]) |
117 | +def source(request): |
118 | + """Return source of the charm under test (i.e. local, cs).""" |
119 | + return request.param |
120 | + |
121 | + |
122 | +@pytest_asyncio.fixture(scope="module", autouse=True) |
123 | +async def app(model, series, source, request): |
124 | + """Deploy sysconfig app along with a principal ubuntu unit.""" |
125 | + channel = "stable" |
126 | + sysconfig_app_name = "sysconfig-{}-{}".format(series, source[0]) |
127 | + principal_app_name = PRINCIPAL_APP_NAME.format(series) |
128 | + |
129 | + # uncomment if app is already deployed while re-testing on same model |
130 | + # sysconfig_app = model.applications.get(sysconfig_app_name) |
131 | + # if sysconfig_app: |
132 | + # return sysconfig_app |
133 | + |
134 | + await model.deploy( |
135 | + "ubuntu", application_name=principal_app_name, series=series, channel=channel |
136 | + ) |
137 | + |
138 | + # If series is 'xfail' force install to allow testing against versions not in |
139 | + # metadata.yaml |
140 | + force = True if request.node.get_closest_marker("xfail") else False |
141 | + |
142 | + sysconfig_app = await model.deploy( |
143 | + source[1], |
144 | + application_name=sysconfig_app_name, |
145 | + series=series, |
146 | + force=force, |
147 | + num_units=0, |
148 | + ) |
149 | + await asyncio.gather( |
150 | + sysconfig_app.add_relation( |
151 | + "juju-info", "{}:juju-info".format(principal_app_name) |
152 | + ), |
153 | + sysconfig_app.set_config({"enable-container": "true"}), |
154 | + ) |
155 | + |
156 | + return sysconfig_app |
157 | + |
158 | + |
159 | +@pytest_asyncio.fixture(scope="module", autouse=True) |
160 | +async def app_with_config(model, series, source): |
161 | + """Deploy sysconfig app + config along with a principal ubuntu unit.""" |
162 | + channel = "stable" |
163 | + sysconfig_app_with_config_name = "sysconfig-{}-{}-with-config".format( |
164 | + series, source[0] |
165 | + ) |
166 | + principal_app_name = PRINCIPAL_APP_NAME.format(series) |
167 | + principal_app_with_config_name = principal_app_name + "-with-config" |
168 | + |
169 | + # uncomment if app is already deployed while re-testing on same model |
170 | + # sysconfig_app_with_config = model.applications.get(sysconfig_app_with_config_name) |
171 | + # if sysconfig_app_with_config: |
172 | + # return sysconfig_app_with_config |
173 | + |
174 | + await model.deploy( |
175 | + "ubuntu", |
176 | + application_name=principal_app_with_config_name, |
177 | + series=series, |
178 | + channel=channel, |
179 | + ) |
180 | + |
181 | + config = { |
182 | + "isolcpus": "1,2,3,4", |
183 | + "enable-pti": "on", |
184 | + "systemd-config-flags": "LogLevel=warning,DumpCore=no", |
185 | + "governor": "powersave", |
186 | + } |
187 | + sysconfig_app_with_config = await model.deploy( |
188 | + source[1], |
189 | + application_name=sysconfig_app_with_config_name, |
190 | + series=series, |
191 | + num_units=0, |
192 | + config=config, |
193 | + ) |
194 | + await asyncio.gather( |
195 | + sysconfig_app_with_config.add_relation( |
196 | + "juju-info", "{}:juju-info".format(principal_app_with_config_name) |
197 | + ), |
198 | + sysconfig_app_with_config.set_config({"enable-container": "true"}), |
199 | + ) |
200 | + |
201 | + return sysconfig_app_with_config |
202 | + |
203 | + |
204 | @pytest_asyncio.fixture(scope="module") |
205 | async def jujutools(controller, model): |
206 | """Return JujuTools instance.""" |
207 | diff --git a/src/tests/functional/test_deploy.py b/src/tests/functional/test_deploy.py |
208 | index 01b5f78..46e1dcc 100644 |
209 | --- a/src/tests/functional/test_deploy.py |
210 | +++ b/src/tests/functional/test_deploy.py |
211 | @@ -1,25 +1,15 @@ |
212 | """Functional tests for sysconfig charm.""" |
213 | |
214 | import asyncio |
215 | -import os |
216 | import re |
217 | -import subprocess |
218 | |
219 | import pytest |
220 | -import pytest_asyncio |
221 | import tenacity |
222 | import websockets |
223 | |
224 | # Treat all tests as coroutines |
225 | pytestmark = pytest.mark.asyncio |
226 | |
227 | -charm_location = os.getenv("CHARM_LOCATION", "..").rstrip("/") |
228 | -charm_name = os.getenv("CHARM_NAME", "sysconfig") |
229 | - |
230 | -series = ["jammy", "focal", "bionic"] |
231 | - |
232 | -sources = [("local", "{}/{}.charm".format(charm_location, charm_name))] |
233 | - |
234 | TIMEOUT = 600 |
235 | MODEL_ACTIVE_TIMEOUT = 10 |
236 | GRUB_DEFAULT = "Advanced options for Ubuntu>Ubuntu, with Linux {}" |
237 | @@ -30,83 +20,33 @@ RETRY = tenacity.retry( |
238 | stop=tenacity.stop_after_attempt(4), |
239 | ) |
240 | |
241 | + |
242 | # Uncomment for re-using the current model, useful for debugging functional tests |
243 | -# @pytest.fixture(scope='module') |
244 | +# @pytest_asyncio.fixture(scope="module") |
245 | # async def model(): |
246 | # from juju.model import Model |
247 | # model = Model() |
248 | -# await model.connect_current() |
249 | +# await model.connect() |
250 | # yield model |
251 | # await model.disconnect() |
252 | |
253 | |
254 | -# Custom fixtures |
255 | -@pytest_asyncio.fixture(params=series) |
256 | -def series(request): |
257 | - """Return ubuntu version (i.e. xenial) in use in the test.""" |
258 | - return request.param |
259 | - |
260 | - |
261 | -@pytest_asyncio.fixture(params=sources, ids=[s[0] for s in sources]) |
262 | -def source(request): |
263 | - """Return source of the charm under test (i.e. local, cs).""" |
264 | - return request.param |
265 | - |
266 | - |
267 | -@pytest_asyncio.fixture |
268 | -async def app(model, series, source): |
269 | - """Return application of the charm under test.""" |
270 | - app_name = "sysconfig-{}-{}".format(series, source[0]) |
271 | - return await model._wait_for_new("application", app_name) |
272 | - |
273 | - |
274 | -# Tests |
275 | - |
276 | - |
277 | -async def test_sysconfig_deploy(model, series, source, request): |
278 | - """Deploys the sysconfig charm as a subordinate of ubuntu.""" |
279 | - channel = "stable" |
280 | - sysconfig_app_name = "sysconfig-{}-{}".format(series, source[0]) |
281 | - principal_app_name = PRINCIPAL_APP_NAME.format(series) |
282 | - |
283 | - ubuntu_app = await model.deploy( |
284 | - "ubuntu", application_name=principal_app_name, series=series, channel=channel |
285 | - ) |
286 | +async def test_app_deploy(model, app): |
287 | + """Verify if the sysconfig app has been successfully deployed.""" |
288 | + try: |
289 | + await model.block_until(lambda: app.status == "active", timeout=TIMEOUT) |
290 | + except asyncio.exceptions.TimeoutError: |
291 | + assert False, "Sysconfig app should have active status." |
292 | |
293 | - await model.block_until(lambda: ubuntu_app.status == "active", timeout=TIMEOUT) |
294 | - |
295 | - # Using subprocess b/c libjuju fails with JAAS |
296 | - # https://github.com/juju/python-libjuju/issues/221 |
297 | - cmd = [ |
298 | - "juju", |
299 | - "deploy", |
300 | - source[1], |
301 | - "-m", |
302 | - model.info.name, |
303 | - "--series", |
304 | - series, |
305 | - sysconfig_app_name, |
306 | - ] |
307 | |
308 | - if request.node.get_closest_marker("xfail"): |
309 | - # If series is 'xfail' force install to allow testing against versions not in |
310 | - # metadata.yaml |
311 | - cmd.append("--force") |
312 | - subprocess.check_call(cmd) |
313 | - |
314 | - # This is pretty horrible, but we can't deploy via libjuju |
315 | - while True: |
316 | - try: |
317 | - sysconfig_app = model.applications[sysconfig_app_name] |
318 | - break |
319 | - except KeyError: |
320 | - await asyncio.sleep(5) |
321 | - |
322 | - await sysconfig_app.add_relation( |
323 | - "juju-info", "{}:juju-info".format(principal_app_name) |
324 | - ) |
325 | - await sysconfig_app.set_config({"enable-container": "true"}) |
326 | - await model.block_until(lambda: sysconfig_app.status == "blocked", timeout=TIMEOUT) |
327 | +async def test_app_with_config_deploy(model, app_with_config): |
328 | + """Check if status of sysconfig app is "blocked" if deployed along with config.""" |
329 | + try: |
330 | + await model.block_until( |
331 | + lambda: app_with_config.status == "blocked", timeout=TIMEOUT |
332 | + ) |
333 | + except asyncio.exceptions.TimeoutError: |
334 | + assert False, "Sysconfig app with config should have blocked status." |
335 | |
336 | |
337 | async def test_cpufrequtils_intalled(app, jujutools): |
338 | @@ -420,7 +360,7 @@ async def test_set_sysctl(app, model, jujutools, sysctl): |
339 | |
340 | |
341 | async def test_uninstall(app, model, jujutools, series): |
342 | - """Tests unistall the unit removing the subordinate relation.""" |
343 | + """Test uninstall of the unit by removing the subordinate relation.""" |
344 | # Apply systemd and cpufrequtils configuration to test that is deleted |
345 | # after removing the relation with ubuntu |
346 | await app.set_config( |
347 | diff --git a/src/tests/unit/test_lib.py b/src/tests/unit/test_lib.py |
348 | index b2b5354..1fdf204 100644 |
349 | --- a/src/tests/unit/test_lib.py |
350 | +++ b/src/tests/unit/test_lib.py |
351 | @@ -4,6 +4,7 @@ import subprocess |
352 | import unittest.mock as mock |
353 | from datetime import datetime, timedelta, timezone |
354 | from tempfile import NamedTemporaryFile |
355 | +from unittest.mock import Mock |
356 | |
357 | import lib_sysconfig |
358 | import pytest |
359 | @@ -540,7 +541,7 @@ class TestLib: |
360 | @mock.patch("lib_sysconfig.hookenv.log") |
361 | @mock.patch("lib_sysconfig.render") |
362 | def test_update_systemd_system_file(self, render, log, config): |
363 | - """Update /etc/default/grub.d/90-sysconfig.cfg and update-grub false. |
364 | + """Update config and see whether /etc/systemd/system.conf is rendered. |
365 | |
366 | Expect file is rendered with correct config and updated-grub is not called. |
367 | """ |
368 | @@ -559,6 +560,10 @@ class TestLib: |
369 | } |
370 | |
371 | sysh = lib_sysconfig.SysConfigHelper() |
372 | + update_available = Mock() |
373 | + update_available.return_value = True |
374 | + sysh._systemd_update_available = update_available |
375 | + |
376 | sysh.update_systemd_system_file() |
377 | render.assert_called_with( |
378 | source=lib_sysconfig.SYSTEMD_SYSTEM_TMPL, |
379 | @@ -566,6 +571,27 @@ class TestLib: |
380 | templates_dir="templates", |
381 | context=expected, |
382 | ) |
383 | + log.assert_called_with("systemd configuration updated") |
384 | + |
385 | + @mock.patch("lib_sysconfig.hookenv.config") |
386 | + @mock.patch("lib_sysconfig.hookenv.log") |
387 | + @mock.patch("lib_sysconfig.render") |
388 | + def test_update_systemd_system_file_no_change(self, render, log, config): |
389 | + """Test whether /etc/systemd/system.conf is rendered if there are no changes.""" |
390 | + config.return_value = { |
391 | + "cpu-affinity-range": "", |
392 | + "reservation": "off", |
393 | + "systemd-config-flags": "", |
394 | + } |
395 | + |
396 | + sysh = lib_sysconfig.SysConfigHelper() |
397 | + update_available = Mock() |
398 | + update_available.return_value = False |
399 | + sysh._systemd_update_available = update_available |
400 | + |
401 | + sysh.update_systemd_system_file() |
402 | + render.assert_not_called() |
403 | + log.assert_called_with("no systemd configuration changes, not rendering file") |
404 | |
405 | @mock.patch("lib_sysconfig.hookenv.config") |
406 | @mock.patch("lib_sysconfig.hookenv.log") |
407 | @@ -591,6 +617,10 @@ class TestLib: |
408 | } |
409 | |
410 | sysh = lib_sysconfig.SysConfigHelper() |
411 | + update_available = Mock() |
412 | + update_available.return_value = True |
413 | + sysh._systemd_update_available = update_available |
414 | + |
415 | sysh.update_systemd_system_file() |
416 | render.assert_called_with( |
417 | source=lib_sysconfig.SYSTEMD_SYSTEM_TMPL, |
418 | @@ -623,6 +653,10 @@ class TestLib: |
419 | } |
420 | |
421 | sysh = lib_sysconfig.SysConfigHelper() |
422 | + update_available = Mock() |
423 | + update_available.return_value = True |
424 | + sysh._systemd_update_available = update_available |
425 | + |
426 | sysh.update_systemd_system_file() |
427 | render.assert_called_with( |
428 | source=lib_sysconfig.SYSTEMD_SYSTEM_TMPL, |
429 | @@ -655,6 +689,10 @@ class TestLib: |
430 | } |
431 | |
432 | sysh = lib_sysconfig.SysConfigHelper() |
433 | + update_available = Mock() |
434 | + update_available.return_value = True |
435 | + sysh._systemd_update_available = update_available |
436 | + |
437 | sysh.update_systemd_system_file() |
438 | render.assert_called_with( |
439 | source=lib_sysconfig.SYSTEMD_SYSTEM_TMPL, |
One problem that pops up while testing these changes is when providing the config options directly during deploy time.
For example, running new.charm sysconfig-new --config cpu-affinity- range=" 0-5"
```
$ juju deploy ./sysconfig-
$ juju relate ubuntu sysconfig-new
```
will not trigger the reboot notification even though running the config command separately (as shown below) will trigger the notification.
``` new.charm sysconfig-new range=" 0-5"
$ juju deploy ./sysconfig-
$ juju relate ubuntu sysconfig-new
$ juju config sysconfig-new cpu-affinity-
```
This is because the if condition code in the config-changed handler checking whether the config has changed is not executed. This check is done through the `hookenv. config. changed` method documented here [1].
[1]: https:/ /github. com/juju/ charm-helpers/ blob/77bb1c88e2 6b12ba7eddcfb6d 597db1568a9e22b /charmhelpers/ core/hookenv. py#L386