Merge ~mwhudson/curtin:lp-1928839 into curtin:master

Proposed by Michael Hudson-Doyle
Status: Merged
Approved by: Michael Hudson-Doyle
Approved revision: 3fea6362aecc349960c4b31c54c6b904d7a4e2b5
Merge reported by: Server Team CI bot
Merged at revision: not available
Proposed branch: ~mwhudson/curtin:lp-1928839
Merge into: curtin:master
Diff against target: 118 lines (+35/-9)
2 files modified
curtin/util.py (+20/-1)
tests/unittests/test_curthooks.py (+15/-8)
Reviewer Review Type Date Requested Status
Ryan Harper (community) Approve
Dan Bungert Approve
Server Team CI bot continuous-integration Approve
Review via email: mp+405670@code.launchpad.net

Commit message

fix tearing down ChrootableTarget when mounts appear while it is set up

There are several bug reports that boil down to
ChrootableTarget.__exit__ failing to unmount bind mounts with "target is
busy". For example, ssh-ing in while running tends to do this, because
that creates a mount in /run and then umounting /target/run fails
because of the sub mount. Fix this by marking the mountpoints
recursively private and then unmounting them recursively.

LP: #1928839
LP: #1934775

Description of the change

Hopefully this fixes a problem that's always niggled at me.

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
~mwhudson/curtin:lp-1928839 updated
af67b1f... by Michael Hudson-Doyle

fix tests

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

I had the reporter of https://bugs.launchpad.net/subiquity/+bug/1934775 test a subiquity branch with this change in it and it helped, fwiw.

Revision history for this message
Dan Bungert (dbungert) :
review: Approve
Revision history for this message
Ryan Harper (raharper) wrote :

This is quite an excellent solution; I had been thinking about using mount namespaces for this but private is much simpler.

review: Approve
~mwhudson/curtin:lp-1928839 updated
3fea636... by Michael Hudson-Doyle

add bug references to comment

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

Thanks for the reviews! Happy to have a fix for this.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/curtin/util.py b/curtin/util.py
2index be063d7..d8f9c47 100644
3--- a/curtin/util.py
4+++ b/curtin/util.py
5@@ -695,7 +695,26 @@ class ChrootableTarget(object):
6 log_call(subp, ['udevadm', 'settle'])
7
8 for p in reversed(self.umounts):
9- do_umount(p)
10+ # Consider the following sequence:
11+ #
12+ # mkdir a a/b c
13+ # mount --bind a c
14+ # mount -t sysfs sysfs a/b
15+ # umount c
16+ #
17+ # This umount fails with "mountpoint is busy", because of
18+ # the mountpoint at c/b. But if we unmount c recursively,
19+ # a/b ends up getting unmounted. What we need to do is to
20+ # make the mountpoints c and c/b "private" so that
21+ # unmounting them does not propagate to the mount tree c
22+ # was cloned from (See "Shared subtree operations" in
23+ # mount(8) for more on this).
24+ #
25+ # Related bug reports:
26+ # https://bugs.launchpad.net/maas/+bug/1928839
27+ # https://bugs.launchpad.net/subiquity/+bug/1934775
28+ subp(['mount', '--make-rprivate', p])
29+ do_umount(p, recursive=True)
30
31 rconf = paths.target_path(self.target, "/etc/resolv.conf")
32 if self.sys_resolvconf and self.rconf_d:
33diff --git a/tests/unittests/test_curthooks.py b/tests/unittests/test_curthooks.py
34index 41670f7..ca0fc90 100644
35--- a/tests/unittests/test_curthooks.py
36+++ b/tests/unittests/test_curthooks.py
37@@ -109,7 +109,7 @@ class TestEnableDisableUpdateInitramfs(CiTestCase):
38
39 def test_disable_changes_binary_name_write_dummy_binary(self):
40 self.mock_which.return_value = self.update_initramfs
41- self.mock_subp.side_effect = iter([('', '')] * 10)
42+ self.mock_subp.side_effect = iter([('', '')] * 15)
43 curthooks.disable_update_initramfs({}, self.target)
44 self.assertIn(
45 call(['dpkg-divert', '--add', '--rename', '--divert',
46@@ -215,18 +215,25 @@ class TestUpdateInitramfs(CiTestCase):
47 target = os.path.join(self.target, point)
48 return call(['mount', '--bind', '/%s' % point, target])
49
50+ def _rpriv_call(self, point):
51+ target = os.path.join(self.target, point)
52+ return call(['mount', '--make-rprivate', target])
53+
54 def _side_eff(self, cmd_out=None, cmd_err=None):
55 if cmd_out is None:
56 cmd_out = ''
57 if cmd_err is None:
58 cmd_err = ''
59 effects = ([('mount', '')] * len(self.mounts) +
60- [(cmd_out, cmd_err)] + [('settle', '')])
61+ [(cmd_out, cmd_err)] +
62+ [('settle', '')] +
63+ [('mount', '')]*len(self.mounts))
64 return effects
65
66 def _subp_calls(self, mycall):
67 pre = [self._mnt_call(point) for point in self.mounts]
68- post = [call(['udevadm', 'settle'])]
69+ post = [call(['udevadm', 'settle'])] + [
70+ self._rpriv_call(point) for point in reversed(self.mounts)]
71 return pre + [mycall] + post
72
73 def test_does_nothing_if_binary_diverted(self):
74@@ -243,7 +250,7 @@ class TestUpdateInitramfs(CiTestCase):
75 target=self.target)
76 calls = self._subp_calls(dcall)
77 self.mock_subp.assert_has_calls(calls)
78- self.assertEqual(6, self.mock_subp.call_count)
79+ self.assertEqual(10, self.mock_subp.call_count)
80
81 def test_mounts_and_runs(self):
82 # in_chroot calls to dpkg-divert, update-initramfs
83@@ -256,7 +263,7 @@ class TestUpdateInitramfs(CiTestCase):
84 call(['update-initramfs', '-c', '-k', self.kversion],
85 target=self.target))
86 self.mock_subp.assert_has_calls(subp_calls)
87- self.assertEqual(12, self.mock_subp.call_count)
88+ self.assertEqual(20, self.mock_subp.call_count)
89
90 def test_mounts_and_runs_for_all_kernels(self):
91 kversion2 = '5.4.0-generic'
92@@ -280,7 +287,7 @@ class TestUpdateInitramfs(CiTestCase):
93 call(['update-initramfs', '-c', '-k', kversion2],
94 target=self.target))
95 self.mock_subp.assert_has_calls(subp_calls)
96- self.assertEqual(24, self.mock_subp.call_count)
97+ self.assertEqual(40, self.mock_subp.call_count)
98
99 def test_calls_update_if_initrd_exists_else_create(self):
100 kversion2 = '5.2.0-generic'
101@@ -290,7 +297,7 @@ class TestUpdateInitramfs(CiTestCase):
102 with open(os.path.join(self.boot, 'initrd.img-' + kversion2), 'w'):
103 pass
104
105- effects = self._side_eff() * 3
106+ effects = self._side_eff() * 4
107 self.mock_subp.side_effect = iter(effects)
108 curthooks.update_initramfs(self.target, True)
109 subp_calls = self._subp_calls(
110@@ -302,7 +309,7 @@ class TestUpdateInitramfs(CiTestCase):
111 call(['update-initramfs', '-c', '-k', self.kversion],
112 target=self.target))
113 self.mock_subp.assert_has_calls(subp_calls)
114- self.assertEqual(18, self.mock_subp.call_count)
115+ self.assertEqual(30, self.mock_subp.call_count)
116
117
118 class TestSetupKernelImgConf(CiTestCase):

Subscribers

People subscribed via source and target branches