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
diff --git a/cloudinit/config/cc_mounts.py b/cloudinit/config/cc_mounts.py
index b3b25c3..eca6ea3 100644
--- a/cloudinit/config/cc_mounts.py
+++ b/cloudinit/config/cc_mounts.py
@@ -76,6 +76,7 @@ DEVICE_NAME_FILTER = r"^([x]{0,1}[shv]d[a-z][0-9]*|sr[0-9]+)$"
76DEVICE_NAME_RE = re.compile(DEVICE_NAME_FILTER)76DEVICE_NAME_RE = re.compile(DEVICE_NAME_FILTER)
77WS = re.compile("[%s]+" % (whitespace))77WS = re.compile("[%s]+" % (whitespace))
78FSTAB_PATH = "/etc/fstab"78FSTAB_PATH = "/etc/fstab"
79MNT_COMMENT = "comment=cloudconfig"
7980
80LOG = logging.getLogger(__name__)81LOG = logging.getLogger(__name__)
8182
@@ -327,6 +328,22 @@ def handle(_name, cfg, cloud, log, _args):
327328
328 LOG.debug("mounts configuration is %s", cfgmnt)329 LOG.debug("mounts configuration is %s", cfgmnt)
329330
331 fstab_lines = []
332 fstab_devs = {}
333 fstab_removed = []
334
335 for line in util.load_file(FSTAB_PATH).splitlines():
336 if MNT_COMMENT in line:
337 fstab_removed.append(line)
338 continue
339
340 try:
341 toks = WS.split(line)
342 except Exception:
343 pass
344 fstab_devs[toks[0]] = line
345 fstab_lines.append(line)
346
330 for i in range(len(cfgmnt)):347 for i in range(len(cfgmnt)):
331 # skip something that wasn't a list348 # skip something that wasn't a list
332 if not isinstance(cfgmnt[i], list):349 if not isinstance(cfgmnt[i], list):
@@ -336,12 +353,17 @@ def handle(_name, cfg, cloud, log, _args):
336353
337 start = str(cfgmnt[i][0])354 start = str(cfgmnt[i][0])
338 sanitized = sanitize_devname(start, cloud.device_name_to_device, log)355 sanitized = sanitize_devname(start, cloud.device_name_to_device, log)
356 if sanitized != start:
357 log.debug("changed %s => %s" % (start, sanitized))
358
339 if sanitized is None:359 if sanitized is None:
340 log.debug("Ignoring nonexistent named mount %s", start)360 log.debug("Ignoring nonexistent named mount %s", start)
341 continue361 continue
362 elif sanitized in fstab_devs:
363 log.info("Device %s already defined in fstab: %s",
364 sanitized, fstab_devs[sanitized])
365 continue
342366
343 if sanitized != start:
344 log.debug("changed %s => %s" % (start, sanitized))
345 cfgmnt[i][0] = sanitized367 cfgmnt[i][0] = sanitized
346368
347 # in case the user did not quote a field (likely fs-freq, fs_passno)369 # in case the user did not quote a field (likely fs-freq, fs_passno)
@@ -373,11 +395,17 @@ def handle(_name, cfg, cloud, log, _args):
373 for defmnt in defmnts:395 for defmnt in defmnts:
374 start = defmnt[0]396 start = defmnt[0]
375 sanitized = sanitize_devname(start, cloud.device_name_to_device, log)397 sanitized = sanitize_devname(start, cloud.device_name_to_device, log)
398 if sanitized != start:
399 log.debug("changed default device %s => %s" % (start, sanitized))
400
376 if sanitized is None:401 if sanitized is None:
377 log.debug("Ignoring nonexistent default named mount %s", start)402 log.debug("Ignoring nonexistent default named mount %s", start)
378 continue403 continue
379 if sanitized != start:404 elif sanitized in fstab_devs:
380 log.debug("changed default device %s => %s" % (start, sanitized))405 log.debug("Device %s already defined in fstab: %s",
406 sanitized, fstab_devs[sanitized])
407 continue
408
381 defmnt[0] = sanitized409 defmnt[0] = sanitized
382410
383 cfgmnt_has = False411 cfgmnt_has = False
@@ -409,31 +437,18 @@ def handle(_name, cfg, cloud, log, _args):
409 log.debug("No modifications to fstab needed")437 log.debug("No modifications to fstab needed")
410 return438 return
411439
412 comment = "comment=cloudconfig"
413 cc_lines = []440 cc_lines = []
414 needswap = False441 needswap = False
415 dirs = []442 dirs = []
416 for line in actlist:443 for line in actlist:
417 # write 'comment' in the fs_mntops, entry, claiming this444 # write 'comment' in the fs_mntops, entry, claiming this
418 line[3] = "%s,%s" % (line[3], comment)445 line[3] = "%s,%s" % (line[3], MNT_COMMENT)
419 if line[2] == "swap":446 if line[2] == "swap":
420 needswap = True447 needswap = True
421 if line[1].startswith("/"):448 if line[1].startswith("/"):
422 dirs.append(line[1])449 dirs.append(line[1])
423 cc_lines.append('\t'.join(line))450 cc_lines.append('\t'.join(line))
424451
425 fstab_lines = []
426 removed = []
427 for line in util.load_file(FSTAB_PATH).splitlines():
428 try:
429 toks = WS.split(line)
430 if toks[3].find(comment) != -1:
431 removed.append(line)
432 continue
433 except Exception:
434 pass
435 fstab_lines.append(line)
436
437 for d in dirs:452 for d in dirs:
438 try:453 try:
439 util.ensure_dir(d)454 util.ensure_dir(d)
@@ -441,7 +456,7 @@ def handle(_name, cfg, cloud, log, _args):
441 util.logexc(log, "Failed to make '%s' config-mount", d)456 util.logexc(log, "Failed to make '%s' config-mount", d)
442457
443 sadds = [WS.sub(" ", n) for n in cc_lines]458 sadds = [WS.sub(" ", n) for n in cc_lines]
444 sdrops = [WS.sub(" ", n) for n in removed]459 sdrops = [WS.sub(" ", n) for n in fstab_removed]
445460
446 sops = (["- " + drop for drop in sdrops if drop not in sadds] +461 sops = (["- " + drop for drop in sdrops if drop not in sadds] +
447 ["+ " + add for add in sadds if add not in sdrops])462 ["+ " + add for add in sadds if add not in sdrops])
diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py
index 117a9cf..07059fd 100644
--- a/cloudinit/tests/helpers.py
+++ b/cloudinit/tests/helpers.py
@@ -111,12 +111,12 @@ class TestCase(unittest2.TestCase):
111 super(TestCase, self).setUp()111 super(TestCase, self).setUp()
112 self.reset_global_state()112 self.reset_global_state()
113113
114 def add_patch(self, target, attr, **kwargs):114 def add_patch(self, target, attr, *args, **kwargs):
115 """Patches specified target object and sets it as attr on test115 """Patches specified target object and sets it as attr on test
116 instance also schedules cleanup"""116 instance also schedules cleanup"""
117 if 'autospec' not in kwargs:117 if 'autospec' not in kwargs:
118 kwargs['autospec'] = True118 kwargs['autospec'] = True
119 m = mock.patch(target, **kwargs)119 m = mock.patch(target, *args, **kwargs)
120 p = m.start()120 p = m.start()
121 self.addCleanup(m.stop)121 self.addCleanup(m.stop)
122 setattr(self, attr, p)122 setattr(self, attr, p)
diff --git a/tests/unittests/test_handler/test_handler_mounts.py b/tests/unittests/test_handler/test_handler_mounts.py
index fe492d4..8fea6c2 100644
--- a/tests/unittests/test_handler/test_handler_mounts.py
+++ b/tests/unittests/test_handler/test_handler_mounts.py
@@ -1,8 +1,6 @@
1# This file is part of cloud-init. See LICENSE file for license information.1# This file is part of cloud-init. See LICENSE file for license information.
22
3import os.path3import os.path
4import shutil
5import tempfile
64
7from cloudinit.config import cc_mounts5from cloudinit.config import cc_mounts
86
@@ -18,8 +16,7 @@ class TestSanitizeDevname(test_helpers.FilesystemMockingTestCase):
1816
19 def setUp(self):17 def setUp(self):
20 super(TestSanitizeDevname, self).setUp()18 super(TestSanitizeDevname, self).setUp()
21 self.new_root = tempfile.mkdtemp()19 self.new_root = self.tmp_dir()
22 self.addCleanup(shutil.rmtree, self.new_root)
23 self.patchOS(self.new_root)20 self.patchOS(self.new_root)
2421
25 def _touch(self, path):22 def _touch(self, path):
@@ -134,4 +131,103 @@ class TestSanitizeDevname(test_helpers.FilesystemMockingTestCase):
134 cc_mounts.sanitize_devname(131 cc_mounts.sanitize_devname(
135 'ephemeral0.1', lambda x: disk_path, mock.Mock()))132 'ephemeral0.1', lambda x: disk_path, mock.Mock()))
136133
134
135class TestFstabHandling(test_helpers.FilesystemMockingTestCase):
136
137 swap_path = '/dev/sdb1'
138
139 def setUp(self):
140 super(TestFstabHandling, self).setUp()
141 self.new_root = self.tmp_dir()
142 self.patchOS(self.new_root)
143
144 self.fstab_path = os.path.join(self.new_root, 'etc/fstab')
145 self._makedirs('/etc')
146
147 self.add_patch('cloudinit.config.cc_mounts.FSTAB_PATH',
148 'mock_fstab_path',
149 self.fstab_path,
150 autospec=False)
151
152 self.add_patch('cloudinit.config.cc_mounts._is_block_device',
153 'mock_is_block_device',
154 return_value=True)
155
156 self.add_patch('cloudinit.config.cc_mounts.util.subp',
157 'mock_util_subp')
158
159 self.mock_cloud = mock.Mock()
160 self.mock_log = mock.Mock()
161 self.mock_cloud.device_name_to_device = self.device_name_to_device
162
163 def _makedirs(self, directory):
164 directory = os.path.join(self.new_root, directory.lstrip('/'))
165 if not os.path.exists(directory):
166 os.makedirs(directory)
167
168 def device_name_to_device(self, path):
169 if path == 'swap':
170 return self.swap_path
171 else:
172 dev = None
173
174 return dev
175
176 def test_fstab_no_swap_device(self):
177 '''Ensure that cloud-init adds a discovered swap partition
178 to /etc/fstab.'''
179
180 fstab_original_content = ''
181 fstab_expected_content = (
182 '%s\tnone\tswap\tsw,comment=cloudconfig\t'
183 '0\t0\n' % (self.swap_path,)
184 )
185
186 with open(cc_mounts.FSTAB_PATH, 'w') as fd:
187 fd.write(fstab_original_content)
188
189 cc_mounts.handle(None, {}, self.mock_cloud, self.mock_log, [])
190
191 with open(cc_mounts.FSTAB_PATH, 'r') as fd:
192 fstab_new_content = fd.read()
193 self.assertEqual(fstab_expected_content, fstab_new_content)
194
195 def test_fstab_same_swap_device_already_configured(self):
196 '''Ensure that cloud-init will not add a swap device if the same
197 device already exists in /etc/fstab.'''
198
199 fstab_original_content = '%s swap swap defaults 0 0\n' % (
200 self.swap_path,)
201 fstab_expected_content = fstab_original_content
202
203 with open(cc_mounts.FSTAB_PATH, 'w') as fd:
204 fd.write(fstab_original_content)
205
206 cc_mounts.handle(None, {}, self.mock_cloud, self.mock_log, [])
207
208 with open(cc_mounts.FSTAB_PATH, 'r') as fd:
209 fstab_new_content = fd.read()
210 self.assertEqual(fstab_expected_content, fstab_new_content)
211
212 def test_fstab_alternate_swap_device_already_configured(self):
213 '''Ensure that cloud-init will add a discovered swap device to
214 /etc/fstab even when there exists a swap definition on another
215 device.'''
216
217 fstab_original_content = '/dev/sdc1 swap swap defaults 0 0\n'
218 fstab_expected_content = (
219 fstab_original_content +
220 '%s\tnone\tswap\tsw,comment=cloudconfig\t'
221 '0\t0\n' % (self.swap_path,)
222 )
223
224 with open(cc_mounts.FSTAB_PATH, 'w') as fd:
225 fd.write(fstab_original_content)
226
227 cc_mounts.handle(None, {}, self.mock_cloud, self.mock_log, [])
228
229 with open(cc_mounts.FSTAB_PATH, 'r') as fd:
230 fstab_new_content = fd.read()
231 self.assertEqual(fstab_expected_content, fstab_new_content)
232
137# vi: ts=4 expandtab233# vi: ts=4 expandtab

Subscribers

People subscribed via source and target branches