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

Proposed by Dan Watkins
Status: Merged
Merged at revision: 1127
Proposed branch: lp:~oddbloke/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:~oddbloke/cloud-init/lp1411582
Reviewer Review Type Date Requested Status
cloud-init Commiters 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.
lp:~oddbloke/cloud-init/lp1411582 updated
1120. By Scott Moser

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

this fixes the cloudsigma datasource when used with python3.

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

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

lp:~oddbloke/cloud-init/lp1411582 updated
1121. By Dan Watkins

Return a sensible value for DataSourceGCE.availability_zone.

1122. By Dan Watkins

Refactor cc_mounts.sanitize_devname to make it easier to modify.

1123. By Dan Watkins

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

1124. By Dan Watkins

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

Add udev rules for Azure ephemeral disks.

And install them in the Debian packaging.

Revision history for this message
Dan Watkins (oddbloke) 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"