Merge ~hopem/charm-sysconfig:bug/1864217 into charm-sysconfig:master

Proposed by Edward Hope-Morley
Status: Merged
Approved by: Jeremy Lounder
Approved revision: ce427f3a0806a67ab779c85826db9123ce0f44f4
Merged at revision: 34bca16d53b97d8671ca2b4f9d237b28dd32147a
Proposed branch: ~hopem/charm-sysconfig:bug/1864217
Merge into: charm-sysconfig:master
Diff against target: 235 lines (+120/-17)
3 files modified
src/lib/lib_sysconfig.py (+55/-2)
src/reactive/sysconfig.py (+10/-0)
src/tests/unit/test_lib.py (+55/-15)
Reviewer Review Type Date Requested Status
Jeremy Lounder (community) Approve
Giuseppe Petralia Approve
Alvaro Uria (community) Needs Fixing
Xav Paice (community) Approve
Review via email: mp+379890@code.launchpad.net

Commit message

Check file content sha256sum when deciding if they have changed

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
Xav Paice (xavpaice) wrote :

LGTM, tests OK (manually) and does appear to resolve the bug.

review: Approve
Revision history for this message
Alvaro Uria (aluria) wrote :

Hey Edward, thank you for the fix. However, I would suggest using charmhelpers' host.write [1] which already checks if the content has changed (and skips re-writing the file if so).

1. https://github.com/juju/charm-helpers/blob/master/charmhelpers/core/host.py#L536

review: Needs Fixing
Revision history for this message
Alvaro Uria (aluria) wrote :

I meant host.write_file. BTW, the render helper also uses host.write_file underneath [1].

1. https://github.com/juju/charm-helpers/blob/master/charmhelpers/core/templating.py#L22

review: Needs Fixing
Revision history for this message
Edward Hope-Morley (hopem) wrote :

@aluria The existing code in the charm that tracks if resources have changed does not use the charm-helpers code so I don't want to rewrite the code in this patch. I have extensively tested this patch and confirm that it resolves the problem as well as doing so for pre-existing resources so would rather stick with this implementation. If somebody wants to rewrite the code to use charm-helpers that's fine but not in this patch please.

Revision history for this message
Giuseppe Petralia (peppepetra) wrote :

Hi Edward,

I have tested the MR. I see two issues:

1. on a fresh install the message of the unit is always "ready" even if the resources are updated.
2. on two consecutive update-status the message of the unit will be always set to ready.

I have suggested how to fix that (comments inline)

Also the MR needs to be rebased.

Thanks,
Giuseppe

review: Needs Fixing
Revision history for this message
Edward Hope-Morley (hopem) wrote :

@peppepetra86 thanks yeah it does need a rebase to get https://code.launchpad.net/~hopem/charm-sysconfig/+git/charm-sysconfig/+merge/379879 ill do that now and check the issue you mention.

Revision history for this message
Edward Hope-Morley (hopem) wrote :

@peppepetra86 thank you for your feedback you were absolutely right, fresh install was falling into this trap. I have updated the logic to fix (and actually think it is much cleaner now). I have tested:

  * fresh install
  * upgrade (patched to patched)
  * config change
  * upgrade (current to patched)

Thing to note is that you do need to wait for update-status hook to run in order for the status message to go away (in my test i set to 10s refresh to speed things up). Since no hooks are executed on reboot I don't think there is another way to resolve this.

Revision history for this message
Giuseppe Petralia (peppepetra) wrote :

lgtm! Thank you, Giuseppe

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

Change successfully merged at revision 34bca16d53b97d8671ca2b4f9d237b28dd32147a

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 862a484..7d4513d 100644
3--- a/src/lib/lib_sysconfig.py
4+++ b/src/lib/lib_sysconfig.py
5@@ -2,6 +2,7 @@
6
7 Manage grub, systemd, coufrequtils and kernel version configuration.
8 """
9+import hashlib
10 import os
11 import subprocess
12 from datetime import datetime, timedelta, timezone
13@@ -75,10 +76,31 @@ class BootResourceState:
14 """Return db key for a given resource."""
15 return "sysconfig.boot_resource.{}".format(resource_name)
16
17+ def calculate_resource_sha256sum(self, resource_name):
18+ """Calcucate sha256sum of contents of provided resource."""
19+ sha = hashlib.sha256()
20+ sha.update(open(resource_name, 'rb').read())
21+ return sha.hexdigest()
22+
23+ def update_resource_checksums(self, resources):
24+ """Update db entry for the resource_name with sha256sum of its contents."""
25+ for resource in resources:
26+ if not os.path.exists(resource):
27+ continue
28+
29+ self.db.set("{}.sha256sum".format(self.key_for(resource)),
30+ self.calculate_resource_sha256sum(resource))
31+
32 def set_resource(self, resource_name):
33 """Update db entry for the resource_name with time.now."""
34 timestamp = datetime.now(timezone.utc)
35 self.db.set(self.key_for(resource_name), timestamp.timestamp())
36+ # NOTE: don't set checksum here
37+
38+ def get_resource_sha256sum(self, resource_name):
39+ """Get db record of sha256sum of contents of provided resource."""
40+ key = self.key_for(resource_name)
41+ return self.db.get("{}.sha256sum".format(key))
42
43 def get_resource_changed_timestamp(self, resource_name):
44 """Retrieve timestamp of last resource change recorded.
45@@ -91,6 +113,20 @@ class BootResourceState:
46 return datetime.fromtimestamp(tfloat, timezone.utc)
47 return datetime.min.replace(tzinfo=timezone.utc) # We don't have a ts -> changed at dawn of time
48
49+ def checksum_changed(self, resource_name):
50+ """Return True if checksum has changed since last recorded."""
51+ # NOTE: we treat checksum == None as True because this is required for
52+ # backwards compatibility (see bug 1864217) since new resources
53+ # created since the charm was patched will always have a checksum.
54+ if not self.get_resource_sha256sum(resource_name):
55+ return True
56+
57+ new_sum = self.calculate_resource_sha256sum(resource_name)
58+ if self.get_resource_sha256sum(resource_name) != new_sum:
59+ return True
60+
61+ return False
62+
63 def resources_changed_since_boot(self, resource_names):
64 """Given a list of resource names return those that have changed since boot.
65
66@@ -98,8 +134,25 @@ class BootResourceState:
67 :return: list of names
68 """
69 boot_ts = boot_time()
70- changed = [name for name in resource_names if boot_ts < self.get_resource_changed_timestamp(name)]
71- return changed
72+ time_changed = [name for name in resource_names
73+ if boot_ts < self.get_resource_changed_timestamp(name)]
74+
75+ csum_changed = [name for name in resource_names
76+ if self.checksum_changed(name)]
77+
78+ a = set(time_changed)
79+ b = set(csum_changed)
80+ c = set(csum_changed).difference(set(time_changed))
81+ d = set(b).difference(c)
82+ # i.e. update resources that have csum mismatch but did not change
83+ # since boot (we only ever update csum here).
84+ self.update_resource_checksums(c)
85+
86+ # i.e. resources that have changed since boot time that do have csum
87+ # mismatch.
88+ changed = a.intersection(d)
89+
90+ return list(changed)
91
92
93 class SysConfigHelper:
94diff --git a/src/reactive/sysconfig.py b/src/reactive/sysconfig.py
95index 85beb8c..2237caa 100644
96--- a/src/reactive/sysconfig.py
97+++ b/src/reactive/sysconfig.py
98@@ -114,6 +114,16 @@ def config_changed():
99 update_status()
100
101
102+@hook('upgrade-charm')
103+def upgrade_charm():
104+ """Extras to run when charm is upgraded."""
105+ # NOTE(hopem): do this for backwards compatibility to ensure that db
106+ # records of resources are updated with a sha256sum prior to config-changed
107+ # being run where they may be overwritten with no content change - see bug
108+ # 1864217 for context.
109+ update_status()
110+
111+
112 @hook('update-status')
113 def update_status():
114 """Update the workload message checking if reboot is needed.
115diff --git a/src/tests/unit/test_lib.py b/src/tests/unit/test_lib.py
116index ceca18c..455018b 100644
117--- a/src/tests/unit/test_lib.py
118+++ b/src/tests/unit/test_lib.py
119@@ -1,7 +1,8 @@
120 #!/usr/bin/python3
121 """Unit tests for SysConfigHelper and BootResourceState classes."""
122 import subprocess
123-from datetime import datetime, timezone
124+from datetime import datetime, timedelta, timezone
125+from tempfile import NamedTemporaryFile
126
127 import lib_sysconfig
128
129@@ -13,16 +14,28 @@ import pytest
130 class TestBootResourceState:
131 """Test BootResourceState class."""
132
133+ @property
134+ def datetime(self):
135+ """Return a datetime object."""
136+ return datetime(2019, 1, 1, tzinfo=timezone.utc)
137+
138 def boot_resource(self):
139 """Mock unitdata.kv()."""
140 db = mock.MagicMock()
141- db.get.return_value = datetime(2019, 1, 1, tzinfo=timezone.utc).timestamp()
142+
143+ def fake_db_get(key):
144+ if key.endswith("sha256sum"):
145+ return "1234"
146+ else:
147+ return self.datetime.timestamp()
148+
149+ db.get.side_effect = fake_db_get
150 return lib_sysconfig.BootResourceState(db=db)
151
152 @mock.patch("lib_sysconfig.datetime")
153 def test_set_resource(self, mock_datetime):
154 """Test updating resource entry in the db."""
155- test_time = datetime(2019, 1, 1, tzinfo=timezone.utc)
156+ test_time = self.datetime
157 mock_datetime.now.return_value = test_time
158 boot_resource = self.boot_resource()
159 boot_resource.set_resource("foofile")
160@@ -34,7 +47,7 @@ class TestBootResourceState:
161
162 def test_get_resource(self):
163 """Test retrieving timestamp of last resource update."""
164- test_time = datetime(2019, 1, 1, tzinfo=timezone.utc)
165+ test_time = self.datetime
166 boot_resource = self.boot_resource()
167 timestamp = boot_resource.get_resource_changed_timestamp("foofile")
168 assert timestamp == test_time
169@@ -54,17 +67,43 @@ class TestBootResourceState:
170 """Test retrieving of resources changed since last boot."""
171 mock_boot_time.return_value = datetime.min.replace(tzinfo=timezone.utc)
172 boot_resource = self.boot_resource()
173- changed = boot_resource.resources_changed_since_boot(["foofile"])
174- assert len(changed) == 1
175- assert changed[0] == "foofile"
176+ with NamedTemporaryFile() as ftmp:
177+ changed = boot_resource.resources_changed_since_boot([ftmp.name])
178+ assert len(changed) == 1
179+ assert changed[0] == ftmp.name
180+
181+ @mock.patch("lib_sysconfig.boot_time")
182+ def test_resources_not_changed(self, mock_boot_time):
183+ """Test resource is not changed since last boot."""
184+ mock_boot_time.return_value = self.datetime
185+ boot_resource = self.boot_resource()
186+ with NamedTemporaryFile() as ftmp:
187+ changed = boot_resource.resources_changed_since_boot([ftmp.name])
188+ assert not changed
189
190 @mock.patch("lib_sysconfig.boot_time")
191- def test_resources_changed_future(self, mock_boot_time):
192+ def test_resources_time_changed_contents_no_change(self, mock_boot_time):
193 """Test resource is not changed since last boot."""
194- mock_boot_time.return_value = datetime.max.replace(tzinfo=timezone.utc)
195+ mock_boot_time.return_value = self.datetime - timedelta(1)
196 boot_resource = self.boot_resource()
197- changed = boot_resource.resources_changed_since_boot(["foofile"])
198- assert not changed
199+ with NamedTemporaryFile() as ftmp:
200+ with mock.patch.object(boot_resource,
201+ 'calculate_resource_sha256sum') as mock_calc:
202+ mock_calc.return_value = "1234"
203+ changed = boot_resource.resources_changed_since_boot([ftmp.name])
204+ assert not changed
205+
206+ @mock.patch("lib_sysconfig.boot_time")
207+ def test_resources_time_changed_contents_changed(self, mock_boot_time):
208+ """Test resource is not changed since last boot."""
209+ mock_boot_time.return_value = self.datetime - timedelta(1)
210+ boot_resource = self.boot_resource()
211+ with NamedTemporaryFile() as ftmp:
212+ with mock.patch.object(boot_resource,
213+ 'calculate_resource_sha256sum') as mock_calc:
214+ mock_calc.return_value = "2345"
215+ changed = boot_resource.resources_changed_since_boot([ftmp.name])
216+ assert changed
217
218
219 class TestLib:
220@@ -411,10 +450,11 @@ class TestLib:
221 }
222 exists.return_value = True
223
224- sysh = lib_sysconfig.SysConfigHelper()
225- sysh.remove_grub_configuration()
226-
227- os_remove.assert_called_with(lib_sysconfig.GRUB_CONF)
228+ with NamedTemporaryFile() as ftmp:
229+ lib_sysconfig.GRUB_CONF = ftmp.name
230+ sysh = lib_sysconfig.SysConfigHelper()
231+ sysh.remove_grub_configuration()
232+ os_remove.assert_called_with(lib_sysconfig.GRUB_CONF)
233
234 @mock.patch("lib_sysconfig.hookenv.config")
235 @mock.patch("lib_sysconfig.os.remove")

Subscribers

People subscribed via source and target branches

to all changes: