Merge lp:~daniel-thewatkins/cloud-init/lp1411582 into lp:~cloud-init-dev/cloud-init/trunk

Proposed by Dan Watkins on 2015-07-15
Status: Merged
Merged at revision: 1127
Proposed branch: lp:~daniel-thewatkins/cloud-init/lp1411582
Merge into: lp:~cloud-init-dev/cloud-init/trunk
Diff against target: 455 lines (+236/-86)
8 files modified
cloudinit/config/cc_disk_setup.py (+5/-0)
cloudinit/config/cc_mounts.py (+50/-64)
cloudinit/sources/DataSourceAzure.py (+23/-20)
packages/debian/dirs (+1/-0)
setup.py (+2/-0)
tests/unittests/test_datasource/test_azure.py (+4/-2)
tests/unittests/test_handler/test_handler_mounts.py (+133/-0)
udev/66-azure-ephemeral.rules (+18/-0)
To merge this branch: bzr merge lp:~daniel-thewatkins/cloud-init/lp1411582
Reviewer Review Type Date Requested Status
cloud-init commiters 2015-07-15 Pending
Review via email: mp+264831@code.launchpad.net

Description of the Change

Azure's ephemeral disks are not guaranteed to be assigned the same name by the kernel every boot. This causes problems on ~2% of Azure instances, and can be fixed by using udev rules to give us a deterministic path to mount; this patch introduces those udev rules and modifies the Azure data source to use them.

Changes to a couple of config modules were also required. In some places, they just needed to learn to dereference symlinks. In cc_mounts this wasn't sufficient because the dereferenced device would have been put in /etc/fstab (rather defeating the point of using the udev rules in the first place). A fairly hefty refactor was required to separate "is this a valid block device?" from "what shall I put in fstab?".

To post a comment you must log in.
1120. By Scott Moser on 2015-07-16

CloudSigma: encode/decode data before communicating over the serial channel

this fixes the cloudsigma datasource when used with python3.

Scott Moser (smoser) wrote :

mostly fine.
how does the udev rule get installed? i guess i'd like packaging in trunk updated too.

1121. By Dan Watkins on 2015-07-20

Return a sensible value for DataSourceGCE.availability_zone.

1122. By Dan Watkins on 2015-07-21

Refactor cc_mounts.sanitize_devname to make it easier to modify.

1123. By Dan Watkins on 2015-07-21

Extend disk_setup and mounts to handle /dev/disk symlinks.

1124. By Dan Watkins on 2015-07-21

Use /dev/disk devices for Azure ephemeral disk.

The ephemeral disk will not necessarily be assigned the same name at
each boot (LP: #1411582), so we use some udev rules to ensure we always
get the right one.

1125. By Dan Watkins on 2015-07-21

Add udev rules for Azure ephemeral disks.

And install them in the Debian packaging.

Dan Watkins (daniel-thewatkins) wrote :

Added that comment and updated the Debian packaging. No idea about RPM packaging, so can't do anything there.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cloudinit/config/cc_disk_setup.py'
2--- cloudinit/config/cc_disk_setup.py 2015-03-18 13:33:12 +0000
3+++ cloudinit/config/cc_disk_setup.py 2015-07-21 12:06:21 +0000
4@@ -648,6 +648,8 @@
5 table_type: Which partition table to use, defaults to MBR
6 device: the device to work on.
7 """
8+ LOG.debug('Ensuring that we have a real device, not a symbolic link')
9+ device = os.path.realpath(device)
10
11 LOG.debug("Checking values for %s definition" % device)
12 overwrite = definition.get('overwrite', False)
13@@ -745,6 +747,9 @@
14 fs_replace = fs_cfg.get('replace_fs', False)
15 overwrite = fs_cfg.get('overwrite', False)
16
17+ LOG.debug('Ensuring that we have a real device, not a symbolic link')
18+ device = os.path.realpath(device)
19+
20 # This allows you to define the default ephemeral or swap
21 LOG.debug("Checking %s against default devices", device)
22
23
24=== modified file 'cloudinit/config/cc_mounts.py'
25--- cloudinit/config/cc_mounts.py 2014-10-01 19:58:30 +0000
26+++ cloudinit/config/cc_mounts.py 2015-07-21 12:06:21 +0000
27@@ -28,15 +28,15 @@
28 from cloudinit import util
29
30 # Shortname matches 'sda', 'sda1', 'xvda', 'hda', 'sdb', xvdb, vda, vdd1, sr0
31-SHORTNAME_FILTER = r"^([x]{0,1}[shv]d[a-z][0-9]*|sr[0-9]+)$"
32-SHORTNAME = re.compile(SHORTNAME_FILTER)
33+DEVICE_NAME_FILTER = r"^([x]{0,1}[shv]d[a-z][0-9]*|sr[0-9]+)$"
34+DEVICE_NAME_RE = re.compile(DEVICE_NAME_FILTER)
35 WS = re.compile("[%s]+" % (whitespace))
36 FSTAB_PATH = "/etc/fstab"
37
38 LOG = logging.getLogger(__name__)
39
40
41-def is_mdname(name):
42+def is_meta_device_name(name):
43 # return true if this is a metadata service name
44 if name in ["ami", "root", "swap"]:
45 return True
46@@ -48,6 +48,25 @@
47 return False
48
49
50+def _get_nth_partition_for_device(device_path, partition_number):
51+ potential_suffixes = [str(partition_number), 'p%s' % (partition_number,),
52+ '-part%s' % (partition_number,)]
53+ for suffix in potential_suffixes:
54+ potential_partition_device = '%s%s' % (device_path, suffix)
55+ if os.path.exists(potential_partition_device):
56+ return potential_partition_device
57+ return None
58+
59+
60+def _is_block_device(device_path, partition_path=None):
61+ device_name = os.path.realpath(device_path).split('/')[-1]
62+ sys_path = os.path.join('/sys/block/', device_name)
63+ if partition_path is not None:
64+ sys_path = os.path.join(
65+ sys_path, os.path.realpath(partition_path).split('/')[-1])
66+ return os.path.exists(sys_path)
67+
68+
69 def sanitize_devname(startname, transformer, log):
70 log.debug("Attempting to determine the real name of %s", startname)
71
72@@ -58,21 +77,34 @@
73 devname = "ephemeral0"
74 log.debug("Adjusted mount option from ephemeral to ephemeral0")
75
76- (blockdev, part) = util.expand_dotted_devname(devname)
77-
78- if is_mdname(blockdev):
79- orig = blockdev
80- blockdev = transformer(blockdev)
81- if not blockdev:
82- return None
83- if not blockdev.startswith("/"):
84- blockdev = "/dev/%s" % blockdev
85- log.debug("Mapped metadata name %s to %s", orig, blockdev)
86- else:
87- if SHORTNAME.match(startname):
88- blockdev = "/dev/%s" % blockdev
89-
90- return devnode_for_dev_part(blockdev, part)
91+ device_path, partition_number = util.expand_dotted_devname(devname)
92+
93+ if is_meta_device_name(device_path):
94+ orig = device_path
95+ device_path = transformer(device_path)
96+ if not device_path:
97+ return None
98+ if not device_path.startswith("/"):
99+ device_path = "/dev/%s" % (device_path,)
100+ log.debug("Mapped metadata name %s to %s", orig, device_path)
101+ else:
102+ if DEVICE_NAME_RE.match(startname):
103+ device_path = "/dev/%s" % (device_path,)
104+
105+ partition_path = None
106+ if partition_number is None:
107+ partition_path = _get_nth_partition_for_device(device_path, 1)
108+ else:
109+ partition_path = _get_nth_partition_for_device(device_path,
110+ partition_number)
111+ if partition_path is None:
112+ return None
113+
114+ if _is_block_device(device_path, partition_path):
115+ if partition_path is not None:
116+ return partition_path
117+ return device_path
118+ return None
119
120
121 def suggested_swapsize(memsize=None, maxsize=None, fsys=None):
122@@ -366,49 +398,3 @@
123 util.subp(("mount", "-a"))
124 except:
125 util.logexc(log, "Activating mounts via 'mount -a' failed")
126-
127-
128-def devnode_for_dev_part(device, partition):
129- """
130- Find the name of the partition. While this might seem rather
131- straight forward, its not since some devices are '<device><partition>'
132- while others are '<device>p<partition>'. For example, /dev/xvda3 on EC2
133- will present as /dev/xvda3p1 for the first partition since /dev/xvda3 is
134- a block device.
135- """
136- if not os.path.exists(device):
137- return None
138-
139- short_name = os.path.basename(device)
140- sys_path = "/sys/block/%s" % short_name
141-
142- if not os.path.exists(sys_path):
143- LOG.debug("did not find entry for %s in /sys/block", short_name)
144- return None
145-
146- sys_long_path = sys_path + "/" + short_name
147-
148- if partition is not None:
149- partition = str(partition)
150-
151- if partition is None:
152- valid_mappings = [sys_long_path + "1", sys_long_path + "p1"]
153- elif partition != "0":
154- valid_mappings = [sys_long_path + "%s" % partition,
155- sys_long_path + "p%s" % partition]
156- else:
157- valid_mappings = []
158-
159- for cdisk in valid_mappings:
160- if not os.path.exists(cdisk):
161- continue
162-
163- dev_path = "/dev/%s" % os.path.basename(cdisk)
164- if os.path.exists(dev_path):
165- return dev_path
166-
167- if partition is None or partition == "0":
168- return device
169-
170- LOG.debug("Did not fine partition %s for device %s", partition, device)
171- return None
172
173=== modified file 'cloudinit/sources/DataSourceAzure.py'
174--- cloudinit/sources/DataSourceAzure.py 2015-05-22 16:28:17 +0000
175+++ cloudinit/sources/DataSourceAzure.py 2015-07-21 12:06:21 +0000
176@@ -254,7 +254,7 @@
177
178 self.metadata.update(fabric_data)
179
180- found_ephemeral = find_ephemeral_disk()
181+ found_ephemeral = find_fabric_formatted_ephemeral_disk()
182 if found_ephemeral:
183 self.ds_cfg['disk_aliases']['ephemeral0'] = found_ephemeral
184 LOG.debug("using detected ephemeral0 of %s", found_ephemeral)
185@@ -276,30 +276,33 @@
186 return len(fnmatch.filter(os.listdir(mp), '*[!cdrom]*'))
187
188
189-def find_ephemeral_part():
190- """
191- Locate the default ephmeral0.1 device. This will be the first device
192- that has a LABEL of DEF_EPHEMERAL_LABEL and is a NTFS device. If Azure
193- gets more ephemeral devices, this logic will only identify the first
194- such device.
195- """
196- c_label_devs = util.find_devs_with("LABEL=%s" % DEF_EPHEMERAL_LABEL)
197- c_fstype_devs = util.find_devs_with("TYPE=ntfs")
198- for dev in c_label_devs:
199- if dev in c_fstype_devs:
200- return dev
201+def find_fabric_formatted_ephemeral_part():
202+ """
203+ Locate the first fabric formatted ephemeral device.
204+ """
205+ potential_locations = ['/dev/disk/cloud/azure_resource-part1',
206+ '/dev/disk/azure/resource-part1']
207+ device_location = None
208+ for potential_location in potential_locations:
209+ if os.path.exists(potential_location):
210+ device_location = potential_location
211+ break
212+ if device_location is None:
213+ return None
214+ ntfs_devices = util.find_devs_with("TYPE=ntfs")
215+ real_device = os.path.realpath(device_location)
216+ if real_device in ntfs_devices:
217+ return device_location
218 return None
219
220
221-def find_ephemeral_disk():
222+def find_fabric_formatted_ephemeral_disk():
223 """
224 Get the ephemeral disk.
225 """
226- part_dev = find_ephemeral_part()
227- if part_dev and str(part_dev[-1]).isdigit():
228- return part_dev[:-1]
229- elif part_dev:
230- return part_dev
231+ part_dev = find_fabric_formatted_ephemeral_part()
232+ if part_dev:
233+ return part_dev.split('-')[0]
234 return None
235
236
237@@ -313,7 +316,7 @@
238 new ephemeral device is detected, cloud-init overrides the default
239 frequency for both disk-setup and mounts for the current boot only.
240 """
241- device = find_ephemeral_part()
242+ device = find_fabric_formatted_ephemeral_part()
243 if not device:
244 LOG.debug("no default fabric formated ephemeral0.1 found")
245 return None
246
247=== modified file 'packages/debian/dirs'
248--- packages/debian/dirs 2011-02-04 21:58:24 +0000
249+++ packages/debian/dirs 2015-07-21 12:06:21 +0000
250@@ -3,3 +3,4 @@
251 etc/init
252 usr/share/doc/cloud
253 etc/cloud
254+lib/udev/rules.d
255
256=== modified file 'setup.py'
257--- setup.py 2015-01-21 20:28:32 +0000
258+++ setup.py 2015-07-21 12:06:21 +0000
259@@ -84,6 +84,7 @@
260 USR = "/usr"
261 ETC = "/etc"
262 USR_LIB_EXEC = "/usr/lib"
263+LIB = "/lib"
264 if os.uname()[0] == 'FreeBSD':
265 USR = "/usr/local"
266 USR_LIB_EXEC = "/usr/local/lib"
267@@ -167,6 +168,7 @@
268 [f for f in glob('doc/examples/*') if is_f(f)]),
269 (USR + '/share/doc/cloud-init/examples/seed',
270 [f for f in glob('doc/examples/seed/*') if is_f(f)]),
271+ (LIB + '/udev/rules.d', ['udev/66-azure-ephemeral.rules']),
272 ]
273 # Use a subclass for install that handles
274 # adding on the right init system configuration files
275
276=== modified file 'tests/unittests/test_datasource/test_azure.py'
277--- tests/unittests/test_datasource/test_azure.py 2015-05-22 16:28:17 +0000
278+++ tests/unittests/test_datasource/test_azure.py 2015-07-21 12:06:21 +0000
279@@ -475,10 +475,12 @@
280 mock.patch.object(DataSourceAzure, 'list_possible_azure_ds_devs',
281 mock.MagicMock(return_value=[])))
282 self.patches.enter_context(
283- mock.patch.object(DataSourceAzure, 'find_ephemeral_disk',
284+ mock.patch.object(DataSourceAzure,
285+ 'find_fabric_formatted_ephemeral_disk',
286 mock.MagicMock(return_value=None)))
287 self.patches.enter_context(
288- mock.patch.object(DataSourceAzure, 'find_ephemeral_part',
289+ mock.patch.object(DataSourceAzure,
290+ 'find_fabric_formatted_ephemeral_part',
291 mock.MagicMock(return_value=None)))
292 self.patches.enter_context(
293 mock.patch.object(DataSourceAzure, 'get_metadata_from_fabric',
294
295=== added file 'tests/unittests/test_handler/test_handler_mounts.py'
296--- tests/unittests/test_handler/test_handler_mounts.py 1970-01-01 00:00:00 +0000
297+++ tests/unittests/test_handler/test_handler_mounts.py 2015-07-21 12:06:21 +0000
298@@ -0,0 +1,133 @@
299+import os.path
300+import shutil
301+import tempfile
302+
303+from cloudinit.config import cc_mounts
304+
305+from .. import helpers as test_helpers
306+
307+try:
308+ from unittest import mock
309+except ImportError:
310+ import mock
311+
312+
313+class TestSanitizeDevname(test_helpers.FilesystemMockingTestCase):
314+
315+ def setUp(self):
316+ super(TestSanitizeDevname, self).setUp()
317+ self.new_root = tempfile.mkdtemp()
318+ self.addCleanup(shutil.rmtree, self.new_root)
319+ self.patchOS(self.new_root)
320+
321+ def _touch(self, path):
322+ path = os.path.join(self.new_root, path.lstrip('/'))
323+ basedir = os.path.dirname(path)
324+ if not os.path.exists(basedir):
325+ os.makedirs(basedir)
326+ open(path, 'a').close()
327+
328+ def _makedirs(self, directory):
329+ directory = os.path.join(self.new_root, directory.lstrip('/'))
330+ if not os.path.exists(directory):
331+ os.makedirs(directory)
332+
333+ def mock_existence_of_disk(self, disk_path):
334+ self._touch(disk_path)
335+ self._makedirs(os.path.join('/sys/block', disk_path.split('/')[-1]))
336+
337+ def mock_existence_of_partition(self, disk_path, partition_number):
338+ self.mock_existence_of_disk(disk_path)
339+ self._touch(disk_path + str(partition_number))
340+ disk_name = disk_path.split('/')[-1]
341+ self._makedirs(os.path.join('/sys/block',
342+ disk_name,
343+ disk_name + str(partition_number)))
344+
345+ def test_existent_full_disk_path_is_returned(self):
346+ disk_path = '/dev/sda'
347+ self.mock_existence_of_disk(disk_path)
348+ self.assertEqual(disk_path,
349+ cc_mounts.sanitize_devname(disk_path,
350+ lambda x: None,
351+ mock.Mock()))
352+
353+ def test_existent_disk_name_returns_full_path(self):
354+ disk_name = 'sda'
355+ disk_path = '/dev/' + disk_name
356+ self.mock_existence_of_disk(disk_path)
357+ self.assertEqual(disk_path,
358+ cc_mounts.sanitize_devname(disk_name,
359+ lambda x: None,
360+ mock.Mock()))
361+
362+ def test_existent_meta_disk_is_returned(self):
363+ actual_disk_path = '/dev/sda'
364+ self.mock_existence_of_disk(actual_disk_path)
365+ self.assertEqual(
366+ actual_disk_path,
367+ cc_mounts.sanitize_devname('ephemeral0',
368+ lambda x: actual_disk_path,
369+ mock.Mock()))
370+
371+ def test_existent_meta_partition_is_returned(self):
372+ disk_name, partition_part = '/dev/sda', '1'
373+ actual_partition_path = disk_name + partition_part
374+ self.mock_existence_of_partition(disk_name, partition_part)
375+ self.assertEqual(
376+ actual_partition_path,
377+ cc_mounts.sanitize_devname('ephemeral0.1',
378+ lambda x: disk_name,
379+ mock.Mock()))
380+
381+ def test_existent_meta_partition_with_p_is_returned(self):
382+ disk_name, partition_part = '/dev/sda', 'p1'
383+ actual_partition_path = disk_name + partition_part
384+ self.mock_existence_of_partition(disk_name, partition_part)
385+ self.assertEqual(
386+ actual_partition_path,
387+ cc_mounts.sanitize_devname('ephemeral0.1',
388+ lambda x: disk_name,
389+ mock.Mock()))
390+
391+ def test_first_partition_returned_if_existent_disk_is_partitioned(self):
392+ disk_name, partition_part = '/dev/sda', '1'
393+ actual_partition_path = disk_name + partition_part
394+ self.mock_existence_of_partition(disk_name, partition_part)
395+ self.assertEqual(
396+ actual_partition_path,
397+ cc_mounts.sanitize_devname('ephemeral0',
398+ lambda x: disk_name,
399+ mock.Mock()))
400+
401+ def test_nth_partition_returned_if_requested(self):
402+ disk_name, partition_part = '/dev/sda', '3'
403+ actual_partition_path = disk_name + partition_part
404+ self.mock_existence_of_partition(disk_name, partition_part)
405+ self.assertEqual(
406+ actual_partition_path,
407+ cc_mounts.sanitize_devname('ephemeral0.3',
408+ lambda x: disk_name,
409+ mock.Mock()))
410+
411+ def test_transformer_returning_none_returns_none(self):
412+ self.assertIsNone(
413+ cc_mounts.sanitize_devname(
414+ 'ephemeral0', lambda x: None, mock.Mock()))
415+
416+ def test_missing_device_returns_none(self):
417+ self.assertIsNone(
418+ cc_mounts.sanitize_devname('/dev/sda', None, mock.Mock()))
419+
420+ def test_missing_sys_returns_none(self):
421+ disk_path = '/dev/sda'
422+ self._makedirs(disk_path)
423+ self.assertIsNone(
424+ cc_mounts.sanitize_devname(disk_path, None, mock.Mock()))
425+
426+ def test_existent_disk_but_missing_partition_returns_none(self):
427+ disk_path = '/dev/sda'
428+ self.mock_existence_of_disk(disk_path)
429+ self.assertIsNone(
430+ cc_mounts.sanitize_devname(
431+ 'ephemeral0.1', lambda x: disk_path, mock.Mock()))
432
433=== added directory 'udev'
434=== added file 'udev/66-azure-ephemeral.rules'
435--- udev/66-azure-ephemeral.rules 1970-01-01 00:00:00 +0000
436+++ udev/66-azure-ephemeral.rules 2015-07-21 12:06:21 +0000
437@@ -0,0 +1,18 @@
438+# Azure specific rules
439+ACTION!="add|change", GOTO="cloud_init_end"
440+SUBSYSTEM!="block", GOTO="cloud_init_end"
441+ATTRS{ID_VENDOR}!="Msft", GOTO="cloud_init_end"
442+ATTRS{ID_MODEL}!="Virtual_Disk", GOTO="cloud_init_end"
443+
444+# Root has a GUID of 0000 as the second value
445+# The resource/resource has GUID of 0001 as the second value
446+ATTRS{device_id}=="?00000000-0000-*", ENV{fabric_name}="azure_root", GOTO="ci_azure_names"
447+ATTRS{device_id}=="?00000000-0001-*", ENV{fabric_name}="azure_resource", GOTO="ci_azure_names"
448+GOTO="cloud_init_end"
449+
450+# Create the symlinks
451+LABEL="ci_azure_names"
452+ENV{DEVTYPE}=="disk", SYMLINK+="disk/cloud/$env{fabric_name}"
453+ENV{DEVTYPE}=="partition", SYMLINK+="disk/cloud/$env{fabric_name}-part%n"
454+
455+LABEL="cloud_init_end"