Merge ~dashmage/charm-sysconfig:systemd-reboot-notification into charm-sysconfig:master

Proposed by Ashley James
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)
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.

To post a comment you must log in.
Revision history for this message
Ashley James (dashmage) wrote :

One problem that pops up while testing these changes is when providing the config options directly during deploy time.

For example, running
```
$ juju deploy ./sysconfig-new.charm sysconfig-new --config cpu-affinity-range="0-5"
$ 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.

```
$ juju deploy ./sysconfig-new.charm sysconfig-new
$ juju relate ubuntu sysconfig-new
$ juju config sysconfig-new cpu-affinity-range="0-5"
```

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/77bb1c88e26b12ba7eddcfb6d597db1568a9e22b/charmhelpers/core/hookenv.py#L386

Revision history for this message
Ashley James (dashmage) wrote :

```
$ juju deploy ./sysconfig-new.charm sysconfig-new
$ juju config sysconfig-new cpu-affinity-range="0-5"
$ 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.

Revision history for this message
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_systemd_system_file() able to be run at any point, safely, without triggering unnecessary 'reboot required'. Since systemd conf files are INI-like, they can easily be parsed with configparser. What I think you should do is have the charm:
  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.ConfigParser()
c_new = configparser.ConfigParser()
c_old.read('/etc/systemd/system.conf')
c_new.read('/some/temp/file')

c_old == c_new # returns True if the configs are semantically equal
```

[0] https://juju.is/docs/sdk/events#heading--lifecycle-event-triggers

review: Needs Fixing
Revision history for this message
Ashley James (dashmage) wrote :

Thanks for your suggestion @aieri.

> We should rather make update_systemd_system_file() able to be run at any point, safely, without triggering unnecessary 'reboot required'. Since systemd conf files are INI-like, they can easily be parsed with configparser.

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.

Revision history for this message
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.

review: Needs Fixing
Revision history for this message
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/lib_sysconfig.py

Grub:
- _assemble_grub_context: generate the context
- 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_context: generate the context
- systemd_conf_changed (?) : compare final systemd file with the temporary systemd context
- update_systemd_system_file (?): generate the final systemd file
- clear_systemd_notification (?)

review: Needs Fixing
Revision history for this message
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/lib_sysconfig.py
>
> Grub:
> - _assemble_grub_context: generate the context
> - 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_context: generate the context
> - systemd_conf_changed (?) : compare final systemd file with the temporary
> systemd context
> - update_systemd_system_file (?): generate the final systemd file
> - clear_systemd_notification (?)

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/grub/grub.cfg) but only the intermediate one in /etc/grub.d which is used to generate the final config file (using grub-mkconfig). Whereas for systemd, the charm updates the final conf file (/etc/systemd/system.conf) itself. We set a flag in the config-changed handler for systemd since we have to perform this comparison check before the final conf file is updated.

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_systemd_context: generate the context

- update_systemd_system_file: generate the final systemd file with updated configs

- check_update_systemd : compare system.conf file (before config changes have been updated) with the temp file (where config changes are updated). I've made the name similar to the grub one.

- check_systemd_clear_notification: This method name would make it clearer that we are checking whether the systemd reboot notification should be cleared. This logic is slightly different with grub because of the same reasons mentioned earlier.

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.

Revision history for this message
Eric Chen (eric-chen) wrote :

Other than the naming, an inline comment for the logic.

review: Needs Fixing
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Eric Chen (eric-chen) wrote :

nitpick about the comment, other than that, LGTM

review: Needs Fixing
Revision history for this message
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`

review: Needs Fixing
Revision history for this message
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://github.com/juju/python-libjuju/issues/221 which has expired. I'm not really sure if we do any testing on JAAS now because otherwise, it's much better practice to do the deployment with libjuju. In case the JAAS based deployment is still necessary, I can revert that part of my code.

Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Approve (continuous-integration)
Revision history for this message
Eric Chen (eric-chen) wrote :

LGTM

review: Approve
Revision history for this message
Andrea Ieri (aieri) wrote :

Overall lgtm, I have a few optional suggestions and a question about the test.

review: Needs Information
Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
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.read('config.txt')
```

is not equivalent to

```
config = ConfigParser().read('config.txt')
```

Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Approve (continuous-integration)
Revision history for this message
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.read('config.txt')
> ```
>
> is not equivalent to
>
> ```
> config = ConfigParser().read('config.txt')
> ```

I'm struggling to see your inline reply... where is it? It's neither attached to commit 5055be3 nor 058376c

Revision history for this message
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().read(SYSTEMD_SYSTEM)
new_config = ConfigParser().read_string(render_output)
```

I had initially made this change and couldn't figure out why the code wasn't working anymore. Turns out that

```
config = ConfigParser()
config.read('config.txt')
```

is not equivalent to

```
config = ConfigParser().read('config.txt')
```

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://docs.python.org/3/library/configparser.html) provide examples where the instance is defined first and then used to call the `read` method.

Similarly for the `read_string` method, if we directly call it like so `ConfigParser().read_string(my_string)`, it returns None.

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_systemd_system_file(),
(...)

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.

Revision history for this message
Andrea Ieri (aieri) wrote :

TIL! I had run some small tests on my side but did not catch that `config = ConfigParser().read('config.txt')` is not equivalent to instantiating and reading separately.

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_deploy would not run without it. This is not good design (all tests should be independent) and makes me really think the deployment work should move to conftest.py

review: Needs Fixing
Revision history for this message
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.

Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Approve (continuous-integration)
Revision history for this message
Andrea Ieri (aieri) wrote :

thank you, this is a lot cleaner!

review: Approve
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision ec090fc3fd17648d455b838b57e284b11e28ec9f

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/lib/lib_sysconfig.py b/src/lib/lib_sysconfig.py
2index 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."""
88diff --git a/src/tests/functional/conftest.py b/src/tests/functional/conftest.py
89index 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."""
207diff --git a/src/tests/functional/test_deploy.py b/src/tests/functional/test_deploy.py
208index 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(
347diff --git a/src/tests/unit/test_lib.py b/src/tests/unit/test_lib.py
348index 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,

Subscribers

People subscribed via source and target branches

to all changes: