Merge charm-sysconfig:bug/1895189 into charm-sysconfig:master

Proposed by Chi Wai CHAN
Status: Merged
Approved by: Eric Chen
Approved revision: b2abfb713794b9af6e442a1bf5c1a7888240796d
Merged at revision: 62059a0dd7be66a3f0bebd48712eb41f9d7a8026
Proposed branch: charm-sysconfig:bug/1895189
Merge into: charm-sysconfig:master
Diff against target: 215 lines (+116/-9)
7 files modified
src/actions.yaml (+3/-0)
src/actions/check-update-grub (+12/-0)
src/lib/lib_sysconfig.py (+34/-0)
src/reactive/sysconfig.py (+18/-5)
src/tests/functional/conftest.py (+0/-1)
src/tests/functional/test_deploy.py (+13/-3)
src/tests/unit/test_lib.py (+36/-0)
Reviewer Review Type Date Requested Status
Eric Chen Approve
Robert Gildein Needs Fixing
BootStack Reviewers Pending
Review via email: mp+427878@code.launchpad.net

Commit message

Add an extra check to see if update-grub is required or not. This addresses bug #1895189: "reboot required" juju status message disappears even if update-grub was not run.

To post a comment you must log in.
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
Robert Gildein (rgildein) wrote :

It seems good to me, but we need to at least add unit tests for these changes.
Also I would prefer at least a basic functional test.

review: Needs Fixing
Revision history for this message
Chi Wai CHAN (raychan96) wrote :

Added an action to check if any updates to grub is available and add a functional test for that.

Revision history for this message
Robert Gildein (rgildein) wrote :

See comments below.

review: Needs Fixing
Revision history for this message
Chi Wai CHAN (raychan96) wrote :

Updated the patch based on your comments. For lines 121-124[0] and lines 133-138[1], they are similar but served for different purpose. [0] tests that if update-grub will be reported as "required" whenever a grub config has changed. [1] tests the if check-update-grub actions can complete without error, the outcome is unimportant, that's why it only assert status == "completed".

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

I will write the code as following to reduce the level of if/else code structure. That is a choice, how do you and Robert think?

def main():
    if not config_valid():
        message = "configurtion is invalid"
    elif check_update_grub():
        message = "update-grub required."
    else:
        message = "nothing to be updated."
    hookenv.action_set({"message": message})

There are also some other comment inline

review: Needs Fixing
Revision history for this message
Chi Wai CHAN (raychan96) wrote :

I think there's no simple way to check if grub config is valid or not, until you reboot. Though, it does complain about the syntax being invalid. Currently, there are only two options: update-grub can be applied (syntax correct and something is different) or no update-grub can be applied (syntax incorrect *or* syntax is correct and nothing has changed). If you prefer, I can split the "syntax incorrect" and "syntax is correct and nothing has changed".

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

> I think there's no simple way to check if grub config is valid or not, until
> you reboot. Though, it does complain about the syntax being invalid.
> Currently, there are only two options: update-grub can be applied (syntax
> correct and something is different) or no update-grub can be applied (syntax
> incorrect *or* syntax is correct and nothing has changed). If you prefer, I
> can split the "syntax incorrect" and "syntax is correct and nothing has
> changed".

Yes, I prefer to separte these case and give clear prompt to user.

Revision history for this message
Robert Gildein (rgildein) wrote :

> I will write the code as following to reduce the level of if/else code structure. That is a choice, how do you and Robert think?

I like it, look simple and easy to read.

Revision history for this message
Chi Wai CHAN (raychan96) wrote (last edit ):

Update the patch based on the comments.

Unittests: https://pastebin.ubuntu.com/p/2pk5Q8kC2S/
Functional tests: https://pastebin.ubuntu.com/p/rwnq8G3fxj/

Functional tests: 1. Skip xenial because of operator framework bug. 2. Pin juju < 3.0.0 because libjuju regression.

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

much much better this time. It's simple and clean.

My last comment for the unit test. In idea case:

Rule: Make each test case method test Just One Thing
https://pylonsproject.org/community-unit-testing-guidelines.html

The other unit test is written by Alvaro which is a very good example for you.

review: Needs Fixing
Revision history for this message
Chi Wai CHAN (raychan96) wrote :

Thanks, I've splitted the unittest.

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

Merge proposal is approved, but source revision has changed, setting status to needs review.

Revision history for this message
Chi Wai CHAN (raychan96) wrote :

lint and black:

https://pastebin.ubuntu.com/p/tTDxYv9Z6x/

Note: pyproject-flake8 is currently broken because of API changes in flake8 >= 5. pyproject-flake8 is working on fixing it. Current work around is to pin flake8 < 5.0.0

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

Change successfully merged at revision 62059a0dd7be66a3f0bebd48712eb41f9d7a8026

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/actions.yaml b/src/actions.yaml
2index c496add..1dcaedb 100644
3--- a/src/actions.yaml
4+++ b/src/actions.yaml
5@@ -7,3 +7,6 @@ clear-notification:
6 update-grub:
7 description: |
8 This action will run 'update-grub' to the generate a GRUB configuration file.
9+check-update-grub:
10+ description: |
11+ This action will test if there is any GRUB configuration changes available.
12diff --git a/src/actions/check-update-grub b/src/actions/check-update-grub
13new file mode 100755
14index 0000000..46693aa
15--- /dev/null
16+++ b/src/actions/check-update-grub
17@@ -0,0 +1,12 @@
18+#!/usr/local/sbin/charm-env python3
19+
20+from charmhelpers.core import hookenv
21+from lib_sysconfig import check_update_grub
22+
23+
24+def main():
25+ _, message = check_update_grub()
26+ hookenv.action_set({"message": message})
27+
28+if __name__ == "__main__":
29+ main()
30diff --git a/src/lib/lib_sysconfig.py b/src/lib/lib_sysconfig.py
31index c7b59b2..19931df 100644
32--- a/src/lib/lib_sysconfig.py
33+++ b/src/lib/lib_sysconfig.py
34@@ -2,6 +2,7 @@
35
36 Manage grub, systemd, coufrequtils and kernel version configuration.
37 """
38+import filecmp
39 import hashlib
40 import os
41 import subprocess
42@@ -67,6 +68,39 @@ def boot_time():
43 return boot_time
44
45
46+def check_update_grub(tmp_output="/tmp/tmp_grub.cfg"):
47+ """Check if an update to /boot/grub/grub.cfg is available."""
48+ # Some sensible default values
49+ update_available = False
50+ message = "No available grub updates found."
51+
52+ # Check for grub config update.
53+ hookenv.log(
54+ "Checking if an update to /boot/grub/grub.cfg is available.", hookenv.DEBUG
55+ )
56+ try:
57+ subprocess.check_output(
58+ "grub-mkconfig -o {}".format(tmp_output),
59+ stderr=subprocess.STDOUT,
60+ shell=True,
61+ )
62+ except subprocess.CalledProcessError as err:
63+ update_available = False
64+ message = "Unable to check update-grub: {}".format(err)
65+ else:
66+ if not filecmp.cmp("/boot/grub/grub.cfg", tmp_output):
67+ update_available = True
68+ message = (
69+ "Found available grub updates. You can run "
70+ "`juju run-action <sysconfig-unit> update-grub` to update grub."
71+ )
72+ else:
73+ update_available = False
74+ message = "No available grub updates found."
75+ hookenv.log(message, hookenv.DEBUG)
76+ return update_available, message
77+
78+
79 class BootResourceState:
80 """A class to track resources changed since last reboot."""
81
82diff --git a/src/reactive/sysconfig.py b/src/reactive/sysconfig.py
83index 1137304..d1983fd 100644
84--- a/src/reactive/sysconfig.py
85+++ b/src/reactive/sysconfig.py
86@@ -35,6 +35,7 @@ from lib_sysconfig import (
87 SYSTEMD_RESOLVED,
88 SYSTEMD_SYSTEM,
89 SysConfigHelper,
90+ check_update_grub,
91 )
92
93
94@@ -173,12 +174,24 @@ def update_status():
95 resources
96 )
97
98- if boot_changes:
99- hookenv.status_set(
100- "blocked", "reboot required. Changes in: {}".format(", ".join(boot_changes))
101- )
102+ config = hookenv.config()
103+ if not config["update-grub"]:
104+ grub_update_available, _ = check_update_grub()
105 else:
106- hookenv.status_set("active", "ready")
107+ # if update-grub is set to true, then no need to check for grub update
108+ # since it will be applied automatically.
109+ grub_update_available = False
110+
111+ status = "active"
112+ message = "ready"
113+ if boot_changes:
114+ status = "blocked"
115+ message = "reboot required. Changes in: {}".format(", ".join(boot_changes))
116+ # if update-grub is set to false and there is an update to grub config,
117+ # then add more info to the status message. (addressing #1895189)
118+ if grub_update_available:
119+ message = "update-grub and " + message
120+ hookenv.status_set(status, message)
121
122
123 @when("config.changed.enable-container")
124diff --git a/src/tests/functional/conftest.py b/src/tests/functional/conftest.py
125index aee0555..672e1c3 100644
126--- a/src/tests/functional/conftest.py
127+++ b/src/tests/functional/conftest.py
128@@ -13,7 +13,6 @@ import os
129 import subprocess
130 import uuid
131
132-import pytest
133 import pytest_asyncio
134 from juju.controller import Controller
135 from juju_tools import JujuTools
136diff --git a/src/tests/functional/test_deploy.py b/src/tests/functional/test_deploy.py
137index bec17ea..aa1f371 100644
138--- a/src/tests/functional/test_deploy.py
139+++ b/src/tests/functional/test_deploy.py
140@@ -127,7 +127,7 @@ async def test_default_config(app, jujutools):
141 assert "GRUB_DEFAULT" not in grub_content
142 assert "default_hugepagesz" not in grub_content
143
144- sysctl_path = '/etc/sysctl.d/90-charm-sysconfig.conf'
145+ sysctl_path = "/etc/sysctl.d/90-charm-sysconfig.conf"
146 sysctl_exists = await jujutools.file_exists(sysctl_path, unit)
147 assert sysctl_exists
148
149@@ -241,8 +241,18 @@ async def test_config_changed(app, model, jujutools):
150 irqbalance_content = await jujutools.file_contents(irqbalance_path, unit)
151 assert "IRQBALANCE_BANNED_CPUS=3000030000300003" in irqbalance_content
152
153- # test update-status show that reboot is required
154- assert "reboot required." in unit.workload_status_message
155+ # test update-status show that update-grub and reboot is required, since
156+ # "grub-config-flags" is changed and "update-grub" is set to false by
157+ # default.
158+ assert "update-grub and reboot required." in unit.workload_status_message
159+
160+
161+async def test_check_update_grub(app):
162+ """Tests that check-update-grub action complete."""
163+ unit = app.units[0]
164+ action = await unit.run_action("check-update-grub")
165+ action = await action.wait()
166+ assert action.status == "completed"
167
168
169 async def test_clear_notification(app):
170diff --git a/src/tests/unit/test_lib.py b/src/tests/unit/test_lib.py
171index 73b9b3e..93dac0a 100644
172--- a/src/tests/unit/test_lib.py
173+++ b/src/tests/unit/test_lib.py
174@@ -9,6 +9,42 @@ import lib_sysconfig
175 import pytest
176
177
178+@mock.patch("filecmp.cmp")
179+@mock.patch("lib_sysconfig.subprocess.check_output")
180+def test_check_update_grub_error(check_output, cmp_file):
181+ """Test check_update_grub function when error occurs."""
182+ tmp_output = "/tmp/tmp_grub.cfg"
183+
184+ check_output.side_effect = subprocess.CalledProcessError(1, "grub-mkconfig")
185+ update_available, message = lib_sysconfig.check_update_grub(tmp_output)
186+ assert update_available is False
187+ assert "Unable to check update-grub" in message
188+
189+
190+@mock.patch("filecmp.cmp")
191+@mock.patch("lib_sysconfig.subprocess.check_output")
192+def test_check_update_grub_available(check_output, cmp_file):
193+ """Test check_update_grub function when grub update available."""
194+ tmp_output = "/tmp/tmp_grub.cfg"
195+
196+ cmp_file.return_value = False
197+ update_available, message = lib_sysconfig.check_update_grub(tmp_output)
198+ assert update_available is True
199+ assert "Found available grub updates." in message
200+
201+
202+@mock.patch("filecmp.cmp")
203+@mock.patch("lib_sysconfig.subprocess.check_output")
204+def test_check_update_grub_unavailable(check_output, cmp_file):
205+ """Test check_update_grub function when grub update unavailable."""
206+ tmp_output = "/tmp/tmp_grub.cfg"
207+
208+ cmp_file.return_value = True
209+ update_available, message = lib_sysconfig.check_update_grub(tmp_output)
210+ assert update_available is False
211+ assert "No available grub updates found." in message
212+
213+
214 class TestBootResourceState:
215 """Test BootResourceState class."""
216

Subscribers

No one subscribed via source and target branches