Merge ~larsks/cloud-init:bz/1542578 into cloud-init:master

Proposed by Lars Kellogg-Stedman
Status: Merged
Approved by: Scott Moser
Approved revision: e65b8058aa663c096de90854b56e98f390717636
Merge reported by: Scott Moser
Merged at revision: 529d48f69d3784b2314397f5eab9d750ab03cf6a
Proposed branch: ~larsks/cloud-init:bz/1542578
Merge into: cloud-init:master
Diff against target: 263 lines (+136/-25)
3 files modified
cloudinit/config/cc_mounts.py (+34/-19)
cloudinit/tests/helpers.py (+2/-2)
tests/unittests/test_handler/test_handler_mounts.py (+100/-4)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
Scott Moser Approve
Chad Smith Approve
Review via email: mp+345113@code.launchpad.net

Commit message

cc_mounts: Do not add devices to fstab that are already present.

Do not add new entries to /etc/fstab for devices that already have an
existing fstab entry.

Resolves: rhbz#1542578

Description of the change

We had a customer who was booting images in OpenStack that had a pre-configured swap partition. Cloud-init was adding a second entry for the same partition. This commit resolves the problem by parsing device names from /etc/fstab and declining to generate new entries for devices that already exist.

As with the existing code, entries with the "comment=cloudconfig" option will be removed and potentially replaced.

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:288fbfe09ef5846547e96212252c7e8cb8d53fd7
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1102/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1102/rebuild

review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

Thats my first quick pass.
This code is really in need of re-factoring.

And could really use unit tests.

Revision history for this message
Lars Kellogg-Stedman (larsks) wrote :

I agree re: refactoring, but I'm not sure I'm up to that right now. I'll make the changes you suggested and I'll add a test or two.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:acd63782c1c4c90bb541355615aaee91adf8f6ed
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1103/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1103/rebuild

review: Approve (continuous-integration)
Revision history for this message
Lars Kellogg-Stedman (larsks) wrote :

I've added a few unit tests for the fstab handling code.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:62a5986a8d8e26bdaeb8a152424df7217286f432
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1104/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1104/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:74671dc8366b4ed30224f27b397bbf0a42c7605a
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1106/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1106/rebuild

review: Approve (continuous-integration)
Revision history for this message
Lars Kellogg-Stedman (larsks) wrote :

Ping @smoser; let me know if this looks okay. Thanks!

Revision history for this message
Scott Moser (smoser) wrote :

Lars, thank you, and prefectly fine to poke me.
I'll try to take a look at this on monday.

one comment in line about some infrastructure that can make this easier.

Thank you for adding some tests!

Scott

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:dd61338ba19d1a6917cedf6716155d710f7529fc
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1115/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1115/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:68c2ca587a00e86c8bb01a23a503e5a79d93e0a0
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1118/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1118/rebuild

review: Approve (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Lars Kellogg-Stedman (larsks) wrote :

Updated to address Chad's comments.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:759107db5ad3a90d7ad0b7fde52bbe0466df0731
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1120/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1120/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:5bd9e873de60135b96729d1be2d044d7276e85f8
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1125/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1125/rebuild

review: Approve (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) wrote :

This looks good to me. Thanks for the changes Lars!

review: Approve
Revision history for this message
Scott Moser (smoser) wrote :

Lars, can you rebase this ... it has conflicts on head and wont land.

review: Needs Fixing
Revision history for this message
Lars Kellogg-Stedman (larsks) wrote :

I've rebased this on b4ae0e1fb8a48a83ea325cf032eb1acb196ee31c.

Revision history for this message
Scott Moser (smoser) wrote :
review: Approve
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:e65b8058aa663c096de90854b56e98f390717636
https://jenkins.ubuntu.com/server/job/cloud-init-ci/24/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/24/rebuild

review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

An upstream commit landed for this bug.

To view that commit see the following URL:
https://git.launchpad.net/cloud-init/commit/?id=529d48f6

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/config/cc_mounts.py b/cloudinit/config/cc_mounts.py
2index b3b25c3..eca6ea3 100644
3--- a/cloudinit/config/cc_mounts.py
4+++ b/cloudinit/config/cc_mounts.py
5@@ -76,6 +76,7 @@ DEVICE_NAME_FILTER = r"^([x]{0,1}[shv]d[a-z][0-9]*|sr[0-9]+)$"
6 DEVICE_NAME_RE = re.compile(DEVICE_NAME_FILTER)
7 WS = re.compile("[%s]+" % (whitespace))
8 FSTAB_PATH = "/etc/fstab"
9+MNT_COMMENT = "comment=cloudconfig"
10
11 LOG = logging.getLogger(__name__)
12
13@@ -327,6 +328,22 @@ def handle(_name, cfg, cloud, log, _args):
14
15 LOG.debug("mounts configuration is %s", cfgmnt)
16
17+ fstab_lines = []
18+ fstab_devs = {}
19+ fstab_removed = []
20+
21+ for line in util.load_file(FSTAB_PATH).splitlines():
22+ if MNT_COMMENT in line:
23+ fstab_removed.append(line)
24+ continue
25+
26+ try:
27+ toks = WS.split(line)
28+ except Exception:
29+ pass
30+ fstab_devs[toks[0]] = line
31+ fstab_lines.append(line)
32+
33 for i in range(len(cfgmnt)):
34 # skip something that wasn't a list
35 if not isinstance(cfgmnt[i], list):
36@@ -336,12 +353,17 @@ def handle(_name, cfg, cloud, log, _args):
37
38 start = str(cfgmnt[i][0])
39 sanitized = sanitize_devname(start, cloud.device_name_to_device, log)
40+ if sanitized != start:
41+ log.debug("changed %s => %s" % (start, sanitized))
42+
43 if sanitized is None:
44 log.debug("Ignoring nonexistent named mount %s", start)
45 continue
46+ elif sanitized in fstab_devs:
47+ log.info("Device %s already defined in fstab: %s",
48+ sanitized, fstab_devs[sanitized])
49+ continue
50
51- if sanitized != start:
52- log.debug("changed %s => %s" % (start, sanitized))
53 cfgmnt[i][0] = sanitized
54
55 # in case the user did not quote a field (likely fs-freq, fs_passno)
56@@ -373,11 +395,17 @@ def handle(_name, cfg, cloud, log, _args):
57 for defmnt in defmnts:
58 start = defmnt[0]
59 sanitized = sanitize_devname(start, cloud.device_name_to_device, log)
60+ if sanitized != start:
61+ log.debug("changed default device %s => %s" % (start, sanitized))
62+
63 if sanitized is None:
64 log.debug("Ignoring nonexistent default named mount %s", start)
65 continue
66- if sanitized != start:
67- log.debug("changed default device %s => %s" % (start, sanitized))
68+ elif sanitized in fstab_devs:
69+ log.debug("Device %s already defined in fstab: %s",
70+ sanitized, fstab_devs[sanitized])
71+ continue
72+
73 defmnt[0] = sanitized
74
75 cfgmnt_has = False
76@@ -409,31 +437,18 @@ def handle(_name, cfg, cloud, log, _args):
77 log.debug("No modifications to fstab needed")
78 return
79
80- comment = "comment=cloudconfig"
81 cc_lines = []
82 needswap = False
83 dirs = []
84 for line in actlist:
85 # write 'comment' in the fs_mntops, entry, claiming this
86- line[3] = "%s,%s" % (line[3], comment)
87+ line[3] = "%s,%s" % (line[3], MNT_COMMENT)
88 if line[2] == "swap":
89 needswap = True
90 if line[1].startswith("/"):
91 dirs.append(line[1])
92 cc_lines.append('\t'.join(line))
93
94- fstab_lines = []
95- removed = []
96- for line in util.load_file(FSTAB_PATH).splitlines():
97- try:
98- toks = WS.split(line)
99- if toks[3].find(comment) != -1:
100- removed.append(line)
101- continue
102- except Exception:
103- pass
104- fstab_lines.append(line)
105-
106 for d in dirs:
107 try:
108 util.ensure_dir(d)
109@@ -441,7 +456,7 @@ def handle(_name, cfg, cloud, log, _args):
110 util.logexc(log, "Failed to make '%s' config-mount", d)
111
112 sadds = [WS.sub(" ", n) for n in cc_lines]
113- sdrops = [WS.sub(" ", n) for n in removed]
114+ sdrops = [WS.sub(" ", n) for n in fstab_removed]
115
116 sops = (["- " + drop for drop in sdrops if drop not in sadds] +
117 ["+ " + add for add in sadds if add not in sdrops])
118diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py
119index 117a9cf..07059fd 100644
120--- a/cloudinit/tests/helpers.py
121+++ b/cloudinit/tests/helpers.py
122@@ -111,12 +111,12 @@ class TestCase(unittest2.TestCase):
123 super(TestCase, self).setUp()
124 self.reset_global_state()
125
126- def add_patch(self, target, attr, **kwargs):
127+ def add_patch(self, target, attr, *args, **kwargs):
128 """Patches specified target object and sets it as attr on test
129 instance also schedules cleanup"""
130 if 'autospec' not in kwargs:
131 kwargs['autospec'] = True
132- m = mock.patch(target, **kwargs)
133+ m = mock.patch(target, *args, **kwargs)
134 p = m.start()
135 self.addCleanup(m.stop)
136 setattr(self, attr, p)
137diff --git a/tests/unittests/test_handler/test_handler_mounts.py b/tests/unittests/test_handler/test_handler_mounts.py
138index fe492d4..8fea6c2 100644
139--- a/tests/unittests/test_handler/test_handler_mounts.py
140+++ b/tests/unittests/test_handler/test_handler_mounts.py
141@@ -1,8 +1,6 @@
142 # This file is part of cloud-init. See LICENSE file for license information.
143
144 import os.path
145-import shutil
146-import tempfile
147
148 from cloudinit.config import cc_mounts
149
150@@ -18,8 +16,7 @@ class TestSanitizeDevname(test_helpers.FilesystemMockingTestCase):
151
152 def setUp(self):
153 super(TestSanitizeDevname, self).setUp()
154- self.new_root = tempfile.mkdtemp()
155- self.addCleanup(shutil.rmtree, self.new_root)
156+ self.new_root = self.tmp_dir()
157 self.patchOS(self.new_root)
158
159 def _touch(self, path):
160@@ -134,4 +131,103 @@ class TestSanitizeDevname(test_helpers.FilesystemMockingTestCase):
161 cc_mounts.sanitize_devname(
162 'ephemeral0.1', lambda x: disk_path, mock.Mock()))
163
164+
165+class TestFstabHandling(test_helpers.FilesystemMockingTestCase):
166+
167+ swap_path = '/dev/sdb1'
168+
169+ def setUp(self):
170+ super(TestFstabHandling, self).setUp()
171+ self.new_root = self.tmp_dir()
172+ self.patchOS(self.new_root)
173+
174+ self.fstab_path = os.path.join(self.new_root, 'etc/fstab')
175+ self._makedirs('/etc')
176+
177+ self.add_patch('cloudinit.config.cc_mounts.FSTAB_PATH',
178+ 'mock_fstab_path',
179+ self.fstab_path,
180+ autospec=False)
181+
182+ self.add_patch('cloudinit.config.cc_mounts._is_block_device',
183+ 'mock_is_block_device',
184+ return_value=True)
185+
186+ self.add_patch('cloudinit.config.cc_mounts.util.subp',
187+ 'mock_util_subp')
188+
189+ self.mock_cloud = mock.Mock()
190+ self.mock_log = mock.Mock()
191+ self.mock_cloud.device_name_to_device = self.device_name_to_device
192+
193+ def _makedirs(self, directory):
194+ directory = os.path.join(self.new_root, directory.lstrip('/'))
195+ if not os.path.exists(directory):
196+ os.makedirs(directory)
197+
198+ def device_name_to_device(self, path):
199+ if path == 'swap':
200+ return self.swap_path
201+ else:
202+ dev = None
203+
204+ return dev
205+
206+ def test_fstab_no_swap_device(self):
207+ '''Ensure that cloud-init adds a discovered swap partition
208+ to /etc/fstab.'''
209+
210+ fstab_original_content = ''
211+ fstab_expected_content = (
212+ '%s\tnone\tswap\tsw,comment=cloudconfig\t'
213+ '0\t0\n' % (self.swap_path,)
214+ )
215+
216+ with open(cc_mounts.FSTAB_PATH, 'w') as fd:
217+ fd.write(fstab_original_content)
218+
219+ cc_mounts.handle(None, {}, self.mock_cloud, self.mock_log, [])
220+
221+ with open(cc_mounts.FSTAB_PATH, 'r') as fd:
222+ fstab_new_content = fd.read()
223+ self.assertEqual(fstab_expected_content, fstab_new_content)
224+
225+ def test_fstab_same_swap_device_already_configured(self):
226+ '''Ensure that cloud-init will not add a swap device if the same
227+ device already exists in /etc/fstab.'''
228+
229+ fstab_original_content = '%s swap swap defaults 0 0\n' % (
230+ self.swap_path,)
231+ fstab_expected_content = fstab_original_content
232+
233+ with open(cc_mounts.FSTAB_PATH, 'w') as fd:
234+ fd.write(fstab_original_content)
235+
236+ cc_mounts.handle(None, {}, self.mock_cloud, self.mock_log, [])
237+
238+ with open(cc_mounts.FSTAB_PATH, 'r') as fd:
239+ fstab_new_content = fd.read()
240+ self.assertEqual(fstab_expected_content, fstab_new_content)
241+
242+ def test_fstab_alternate_swap_device_already_configured(self):
243+ '''Ensure that cloud-init will add a discovered swap device to
244+ /etc/fstab even when there exists a swap definition on another
245+ device.'''
246+
247+ fstab_original_content = '/dev/sdc1 swap swap defaults 0 0\n'
248+ fstab_expected_content = (
249+ fstab_original_content +
250+ '%s\tnone\tswap\tsw,comment=cloudconfig\t'
251+ '0\t0\n' % (self.swap_path,)
252+ )
253+
254+ with open(cc_mounts.FSTAB_PATH, 'w') as fd:
255+ fd.write(fstab_original_content)
256+
257+ cc_mounts.handle(None, {}, self.mock_cloud, self.mock_log, [])
258+
259+ with open(cc_mounts.FSTAB_PATH, 'r') as fd:
260+ fstab_new_content = fd.read()
261+ self.assertEqual(fstab_expected_content, fstab_new_content)
262+
263 # vi: ts=4 expandtab

Subscribers

People subscribed via source and target branches