Merge ~raharper/cloud-init:ds-ovf-use-util-find-devs-with into cloud-init:master
- Git
- lp:~raharper/cloud-init
- ds-ovf-use-util-find-devs-with
- Merge into master
Status: | Merged |
---|---|
Approved by: | Scott Moser |
Approved revision: | 6e2771370102fbed46fba7c077fa9c328d02ecaa |
Merged at revision: | da6562e21d0b17a0957adc0c5a2c9da076e0d219 |
Proposed branch: | ~raharper/cloud-init:ds-ovf-use-util-find-devs-with |
Merge into: | cloud-init:master |
Diff against target: |
309 lines (+221/-27) 3 files modified
cloudinit/sources/DataSourceOVF.py (+47/-27) cloudinit/tests/helpers.py (+10/-0) tests/unittests/test_datasource/test_ovf.py (+164/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Scott Moser | Approve | ||
Server Team CI bot | continuous-integration | Approve | |
Review via email:
|
Commit message
Description of the change
DataSourceOVF: use util.find_
DataSourceOVF attempts to find iso files via walking os.listdir('/dev/')
which is far too wide. This approach is too invasive and can sometimes
race with systemd attempting to fsck and mount devices.
Instead, utilize cloudinit.
criteria (which uses blkid under the covers). This should result in fewer
attempts to mount block devices which are not actual iso filesystems.

Scott Moser (smoser) wrote : | # |

Scott Moser (smoser) wrote : | # |
also seems quite possible that just changing the 'default_regex' to drop 'xvd.*' would probably fix ec2. that explains why we only see this on ec2 and not elsewhere (other than places that have 'hd' disks, which is unlikely at this point in the world).
you can/should/

Ryan Harper (raharper) wrote : | # |
As it turns out, two parallel instances of /bin/mount pointing to the same device will cause one to fail; it appears that there is some sort of locking/
This sort of race is recreatable outside of cloud-init:
# rw mount loop
while true; do
umount -f ${MNT_POINT} |:
mount ${DISK} ${MNT_POINT} -t ext4 -o defaults || {
echo "`date +%s.%N`: mount failed on COUNT=$COUNT";
exit 1
}
echo "`date +%s.%N`: COUNT=$COUNT" > ${MNT_POINT}
COUNT=$(($COUNT + 1))
done
# ro mount loop
while true; do
dd if=${DISK} bs=512 count=1 of=/dev/null
# we expect this to fail, that's OK
mount -o ro,sync -t iso9660 ${DISK} ${MNT_POINT} |:
echo "`date +%s.%N`: READ COUNT=$COUNT"
COUNT=$(($COUNT + 1))
done

Ryan Harper (raharper) wrote : | # |
I've kept the regex filter as suggested and added unittests.

Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:2699208b16a
https:/
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:/

Ryan Harper (raharper) wrote : | # |
> As it turns out, two parallel instances of /bin/mount pointing
> to the same device will cause one to fail; it appears that there
> is some sort of locking/
> that results in one of the two mount processes getting EBUSY as a result.
In particular, when we open the block device, the kernel sets
FMODE_EXCL flag which prevents additional opens of the same device
where the filesystem is of a different type.
This is expected behavior by the kernel (and our setup).

Scott Moser (smoser) wrote : | # |
some small comments.

Ryan Harper (raharper) wrote : | # |
I'll add the os.path.basename(); should we do that in both places?

Scott Moser (smoser) wrote : | # |
clean up the comment i guess. we can use os.path.basename in both places.

Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:6e277137010
https:/
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:/
Preview Diff
1 | diff --git a/cloudinit/sources/DataSourceOVF.py b/cloudinit/sources/DataSourceOVF.py |
2 | index 24b45d5..ccebf11 100644 |
3 | --- a/cloudinit/sources/DataSourceOVF.py |
4 | +++ b/cloudinit/sources/DataSourceOVF.py |
5 | @@ -375,26 +375,56 @@ def get_ovf_env(dirname): |
6 | return (None, False) |
7 | |
8 | |
9 | -# Transport functions take no input and return |
10 | -# a 3 tuple of content, path, filename |
11 | -def transport_iso9660(require_iso=True): |
12 | +def maybe_cdrom_device(devname): |
13 | + """Test if devname matches known list of devices which may contain iso9660 |
14 | + filesystems. |
15 | |
16 | - # default_regex matches values in |
17 | - # /lib/udev/rules.d/60-cdrom_id.rules |
18 | - # KERNEL!="sr[0-9]*|hd[a-z]|xvd*", GOTO="cdrom_end" |
19 | - envname = "CLOUD_INIT_CDROM_DEV_REGEX" |
20 | - default_regex = "^(sr[0-9]+|hd[a-z]|xvd.*)" |
21 | + Be helpful in accepting either knames (with no leading /dev/) or full path |
22 | + names, but do not allow paths outside of /dev/, like /dev/foo/bar/xxx. |
23 | + """ |
24 | + if not devname: |
25 | + return False |
26 | + elif not isinstance(devname, util.string_types): |
27 | + raise ValueError("Unexpected input for devname: %s" % devname) |
28 | + |
29 | + # resolve '..' and multi '/' elements |
30 | + devname = os.path.normpath(devname) |
31 | |
32 | - devname_regex = os.environ.get(envname, default_regex) |
33 | + # drop leading '/dev/' |
34 | + if devname.startswith("/dev/"): |
35 | + # partition returns tuple (before, partition, after) |
36 | + devname = devname.partition("/dev/")[-1] |
37 | + |
38 | + # ignore leading slash (/sr0), else fail on / in name (foo/bar/xvdc) |
39 | + if devname.startswith("/"): |
40 | + devname = devname.split("/")[-1] |
41 | + elif devname.count("/") > 0: |
42 | + return False |
43 | + |
44 | + # if empty string |
45 | + if not devname: |
46 | + return False |
47 | + |
48 | + # default_regex matches values in /lib/udev/rules.d/60-cdrom_id.rules |
49 | + # KERNEL!="sr[0-9]*|hd[a-z]|xvd*", GOTO="cdrom_end" |
50 | + default_regex = r"^(sr[0-9]+|hd[a-z]|xvd.*)" |
51 | + devname_regex = os.environ.get("CLOUD_INIT_CDROM_DEV_REGEX", default_regex) |
52 | cdmatch = re.compile(devname_regex) |
53 | |
54 | + return cdmatch.match(devname) is not None |
55 | + |
56 | + |
57 | +# Transport functions take no input and return |
58 | +# a 3 tuple of content, path, filename |
59 | +def transport_iso9660(require_iso=True): |
60 | + |
61 | # Go through mounts to see if it was already mounted |
62 | mounts = util.mounts() |
63 | for (dev, info) in mounts.items(): |
64 | fstype = info['fstype'] |
65 | if fstype != "iso9660" and require_iso: |
66 | continue |
67 | - if cdmatch.match(dev[5:]) is None: # take off '/dev/' |
68 | + if not maybe_cdrom_device(dev): |
69 | continue |
70 | mp = info['mountpoint'] |
71 | (fname, contents) = get_ovf_env(mp) |
72 | @@ -406,29 +436,19 @@ def transport_iso9660(require_iso=True): |
73 | else: |
74 | mtype = None |
75 | |
76 | - devs = os.listdir("/dev/") |
77 | - devs.sort() |
78 | + # generate a list of devices with mtype filesystem, filter by regex |
79 | + devs = [dev for dev in |
80 | + util.find_devs_with("TYPE=%s" % mtype if mtype else None) |
81 | + if maybe_cdrom_device(dev)] |
82 | for dev in devs: |
83 | - fullp = os.path.join("/dev/", dev) |
84 | - |
85 | - if (fullp in mounts or |
86 | - not cdmatch.match(dev) or os.path.isdir(fullp)): |
87 | - continue |
88 | - |
89 | - try: |
90 | - # See if we can read anything at all...?? |
91 | - util.peek_file(fullp, 512) |
92 | - except IOError: |
93 | - continue |
94 | - |
95 | try: |
96 | - (fname, contents) = util.mount_cb(fullp, get_ovf_env, mtype=mtype) |
97 | + (fname, contents) = util.mount_cb(dev, get_ovf_env, mtype=mtype) |
98 | except util.MountFailedError: |
99 | - LOG.debug("%s not mountable as iso9660", fullp) |
100 | + LOG.debug("%s not mountable as iso9660", dev) |
101 | continue |
102 | |
103 | if contents is not False: |
104 | - return (contents, fullp, fname) |
105 | + return (contents, dev, fname) |
106 | |
107 | return (False, None, None) |
108 | |
109 | diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py |
110 | index 28e2662..6f88a5b 100644 |
111 | --- a/cloudinit/tests/helpers.py |
112 | +++ b/cloudinit/tests/helpers.py |
113 | @@ -104,6 +104,16 @@ class TestCase(unittest2.TestCase): |
114 | super(TestCase, self).setUp() |
115 | self.reset_global_state() |
116 | |
117 | + def add_patch(self, target, attr, **kwargs): |
118 | + """Patches specified target object and sets it as attr on test |
119 | + instance also schedules cleanup""" |
120 | + if 'autospec' not in kwargs: |
121 | + kwargs['autospec'] = True |
122 | + m = mock.patch(target, **kwargs) |
123 | + p = m.start() |
124 | + self.addCleanup(m.stop) |
125 | + setattr(self, attr, p) |
126 | + |
127 | |
128 | class CiTestCase(TestCase): |
129 | """This is the preferred test case base class unless user |
130 | diff --git a/tests/unittests/test_datasource/test_ovf.py b/tests/unittests/test_datasource/test_ovf.py |
131 | index 9dbf4dd..700da86 100644 |
132 | --- a/tests/unittests/test_datasource/test_ovf.py |
133 | +++ b/tests/unittests/test_datasource/test_ovf.py |
134 | @@ -5,6 +5,7 @@ |
135 | # This file is part of cloud-init. See LICENSE file for license information. |
136 | |
137 | import base64 |
138 | +from collections import OrderedDict |
139 | |
140 | from cloudinit.tests import helpers as test_helpers |
141 | |
142 | @@ -70,4 +71,167 @@ class TestReadOvfEnv(test_helpers.TestCase): |
143 | self.assertEqual({'password': "passw0rd"}, cfg) |
144 | self.assertIsNone(ud) |
145 | |
146 | + |
147 | +class TestTransportIso9660(test_helpers.CiTestCase): |
148 | + |
149 | + def setUp(self): |
150 | + super(TestTransportIso9660, self).setUp() |
151 | + self.add_patch('cloudinit.util.find_devs_with', |
152 | + 'm_find_devs_with') |
153 | + self.add_patch('cloudinit.util.mounts', 'm_mounts') |
154 | + self.add_patch('cloudinit.util.mount_cb', 'm_mount_cb') |
155 | + self.add_patch('cloudinit.sources.DataSourceOVF.get_ovf_env', |
156 | + 'm_get_ovf_env') |
157 | + self.m_get_ovf_env.return_value = ('myfile', 'mycontent') |
158 | + |
159 | + def test_find_already_mounted(self): |
160 | + """Check we call get_ovf_env from on matching mounted devices""" |
161 | + mounts = { |
162 | + '/dev/sr9': { |
163 | + 'fstype': 'iso9660', |
164 | + 'mountpoint': 'wark/media/sr9', |
165 | + 'opts': 'ro', |
166 | + } |
167 | + } |
168 | + self.m_mounts.return_value = mounts |
169 | + |
170 | + (contents, fullp, fname) = dsovf.transport_iso9660() |
171 | + self.assertEqual("mycontent", contents) |
172 | + self.assertEqual("/dev/sr9", fullp) |
173 | + self.assertEqual("myfile", fname) |
174 | + |
175 | + def test_find_already_mounted_skips_non_iso9660(self): |
176 | + """Check we call get_ovf_env ignoring non iso9660""" |
177 | + mounts = { |
178 | + '/dev/xvdb': { |
179 | + 'fstype': 'vfat', |
180 | + 'mountpoint': 'wark/foobar', |
181 | + 'opts': 'defaults,noatime', |
182 | + }, |
183 | + '/dev/xvdc': { |
184 | + 'fstype': 'iso9660', |
185 | + 'mountpoint': 'wark/media/sr9', |
186 | + 'opts': 'ro', |
187 | + } |
188 | + } |
189 | + # We use an OrderedDict here to ensure we check xvdb before xvdc |
190 | + # as we're not mocking the regex matching, however, if we place |
191 | + # an entry in the results then we can be reasonably sure that |
192 | + # we're skipping an entry which fails to match. |
193 | + self.m_mounts.return_value = ( |
194 | + OrderedDict(sorted(mounts.items(), key=lambda t: t[0]))) |
195 | + |
196 | + (contents, fullp, fname) = dsovf.transport_iso9660() |
197 | + self.assertEqual("mycontent", contents) |
198 | + self.assertEqual("/dev/xvdc", fullp) |
199 | + self.assertEqual("myfile", fname) |
200 | + |
201 | + def test_find_already_mounted_matches_kname(self): |
202 | + """Check we dont regex match on basename of the device""" |
203 | + mounts = { |
204 | + '/dev/foo/bar/xvdc': { |
205 | + 'fstype': 'iso9660', |
206 | + 'mountpoint': 'wark/media/sr9', |
207 | + 'opts': 'ro', |
208 | + } |
209 | + } |
210 | + # we're skipping an entry which fails to match. |
211 | + self.m_mounts.return_value = mounts |
212 | + |
213 | + (contents, fullp, fname) = dsovf.transport_iso9660() |
214 | + self.assertEqual(False, contents) |
215 | + self.assertIsNone(fullp) |
216 | + self.assertIsNone(fname) |
217 | + |
218 | + def test_mount_cb_called_on_blkdevs_with_iso9660(self): |
219 | + """Check we call mount_cb on blockdevs with iso9660 only""" |
220 | + self.m_mounts.return_value = {} |
221 | + self.m_find_devs_with.return_value = ['/dev/sr0'] |
222 | + self.m_mount_cb.return_value = ("myfile", "mycontent") |
223 | + |
224 | + (contents, fullp, fname) = dsovf.transport_iso9660() |
225 | + |
226 | + self.m_mount_cb.assert_called_with( |
227 | + "/dev/sr0", dsovf.get_ovf_env, mtype="iso9660") |
228 | + self.assertEqual("mycontent", contents) |
229 | + self.assertEqual("/dev/sr0", fullp) |
230 | + self.assertEqual("myfile", fname) |
231 | + |
232 | + def test_mount_cb_called_on_blkdevs_with_iso9660_check_regex(self): |
233 | + """Check we call mount_cb on blockdevs with iso9660 and match regex""" |
234 | + self.m_mounts.return_value = {} |
235 | + self.m_find_devs_with.return_value = [ |
236 | + '/dev/abc', '/dev/my-cdrom', '/dev/sr0'] |
237 | + self.m_mount_cb.return_value = ("myfile", "mycontent") |
238 | + |
239 | + (contents, fullp, fname) = dsovf.transport_iso9660() |
240 | + |
241 | + self.m_mount_cb.assert_called_with( |
242 | + "/dev/sr0", dsovf.get_ovf_env, mtype="iso9660") |
243 | + self.assertEqual("mycontent", contents) |
244 | + self.assertEqual("/dev/sr0", fullp) |
245 | + self.assertEqual("myfile", fname) |
246 | + |
247 | + def test_mount_cb_not_called_no_matches(self): |
248 | + """Check we don't call mount_cb if nothing matches""" |
249 | + self.m_mounts.return_value = {} |
250 | + self.m_find_devs_with.return_value = ['/dev/vg/myovf'] |
251 | + |
252 | + (contents, fullp, fname) = dsovf.transport_iso9660() |
253 | + |
254 | + self.assertEqual(0, self.m_mount_cb.call_count) |
255 | + self.assertEqual(False, contents) |
256 | + self.assertIsNone(fullp) |
257 | + self.assertIsNone(fname) |
258 | + |
259 | + def test_mount_cb_called_require_iso_false(self): |
260 | + """Check we call mount_cb on blockdevs with require_iso=False""" |
261 | + self.m_mounts.return_value = {} |
262 | + self.m_find_devs_with.return_value = ['/dev/xvdz'] |
263 | + self.m_mount_cb.return_value = ("myfile", "mycontent") |
264 | + |
265 | + (contents, fullp, fname) = dsovf.transport_iso9660(require_iso=False) |
266 | + |
267 | + self.m_mount_cb.assert_called_with( |
268 | + "/dev/xvdz", dsovf.get_ovf_env, mtype=None) |
269 | + self.assertEqual("mycontent", contents) |
270 | + self.assertEqual("/dev/xvdz", fullp) |
271 | + self.assertEqual("myfile", fname) |
272 | + |
273 | + def test_maybe_cdrom_device_none(self): |
274 | + """Test maybe_cdrom_device returns False for none/empty input""" |
275 | + self.assertFalse(dsovf.maybe_cdrom_device(None)) |
276 | + self.assertFalse(dsovf.maybe_cdrom_device('')) |
277 | + |
278 | + def test_maybe_cdrom_device_non_string_exception(self): |
279 | + """Test maybe_cdrom_device raises ValueError on non-string types""" |
280 | + with self.assertRaises(ValueError): |
281 | + dsovf.maybe_cdrom_device({'a': 'eleven'}) |
282 | + |
283 | + def test_maybe_cdrom_device_false_on_multi_dir_paths(self): |
284 | + """Test maybe_cdrom_device is false on /dev[/.*]/* paths""" |
285 | + self.assertFalse(dsovf.maybe_cdrom_device('/dev/foo/sr0')) |
286 | + self.assertFalse(dsovf.maybe_cdrom_device('foo/sr0')) |
287 | + self.assertFalse(dsovf.maybe_cdrom_device('../foo/sr0')) |
288 | + self.assertFalse(dsovf.maybe_cdrom_device('../foo/sr0')) |
289 | + |
290 | + def test_maybe_cdrom_device_true_on_hd_partitions(self): |
291 | + """Test maybe_cdrom_device is false on /dev/hd[a-z][0-9]+ paths""" |
292 | + self.assertTrue(dsovf.maybe_cdrom_device('/dev/hda1')) |
293 | + self.assertTrue(dsovf.maybe_cdrom_device('hdz9')) |
294 | + |
295 | + def test_maybe_cdrom_device_true_on_valid_relative_paths(self): |
296 | + """Test maybe_cdrom_device normalizes paths""" |
297 | + self.assertTrue(dsovf.maybe_cdrom_device('/dev/wark/../sr9')) |
298 | + self.assertTrue(dsovf.maybe_cdrom_device('///sr0')) |
299 | + self.assertTrue(dsovf.maybe_cdrom_device('/sr0')) |
300 | + self.assertTrue(dsovf.maybe_cdrom_device('//dev//hda')) |
301 | + |
302 | + def test_maybe_cdrom_device_true_on_xvd_partitions(self): |
303 | + """Test maybe_cdrom_device returns true on xvd*""" |
304 | + self.assertTrue(dsovf.maybe_cdrom_device('/dev/xvda')) |
305 | + self.assertTrue(dsovf.maybe_cdrom_device('/dev/xvda1')) |
306 | + self.assertTrue(dsovf.maybe_cdrom_device('xvdza1')) |
307 | + |
308 | +# |
309 | # vi: ts=4 expandtab |
I'm pretty sure that we end up trying to mount everything with:
mount -t iso966o <device> </tmp/mountpoint>
so i'd have thought that that would fail really early, i'm not sure how this would cause issues with other mounts, unless there was actually iso9660 filesystems on those devices. which would be odd.
also, please file a bug