← Back to team overview

cloud-init-dev team mailing list archive

[Merge] lp:~daniel-thewatkins/cloud-init/lp1411582 into lp:cloud-init

 

Dan Watkins has proposed merging lp:~daniel-thewatkins/cloud-init/lp1411582 into lp:cloud-init.

Requested reviews:
  cloud init development team (cloud-init-dev)

For more details, see:
https://code.launchpad.net/~daniel-thewatkins/cloud-init/lp1411582/+merge/264831

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?".
-- 
Your team cloud init development team is requested to review the proposed merge of lp:~daniel-thewatkins/cloud-init/lp1411582 into lp:cloud-init.
=== modified file 'cloudinit/config/cc_disk_setup.py'
--- cloudinit/config/cc_disk_setup.py	2015-03-18 13:33:12 +0000
+++ cloudinit/config/cc_disk_setup.py	2015-07-15 12:00:55 +0000
@@ -648,6 +648,8 @@
                 table_type: Which partition table to use, defaults to MBR
                 device: the device to work on.
     """
+    LOG.debug('Ensuring that we have a real device, not a symbolic link')
+    device = os.path.realpath(device)
 
     LOG.debug("Checking values for %s definition" % device)
     overwrite = definition.get('overwrite', False)
@@ -745,6 +747,9 @@
     fs_replace = fs_cfg.get('replace_fs', False)
     overwrite = fs_cfg.get('overwrite', False)
 
+    LOG.debug('Ensuring that we have a real device, not a symbolic link')
+    device = os.path.realpath(device)
+
     # This allows you to define the default ephemeral or swap
     LOG.debug("Checking %s against default devices", device)
 

=== modified file 'cloudinit/config/cc_mounts.py'
--- cloudinit/config/cc_mounts.py	2014-10-01 19:58:30 +0000
+++ cloudinit/config/cc_mounts.py	2015-07-15 12:00:55 +0000
@@ -28,15 +28,15 @@
 from cloudinit import util
 
 # Shortname matches 'sda', 'sda1', 'xvda', 'hda', 'sdb', xvdb, vda, vdd1, sr0
-SHORTNAME_FILTER = r"^([x]{0,1}[shv]d[a-z][0-9]*|sr[0-9]+)$"
-SHORTNAME = re.compile(SHORTNAME_FILTER)
+DEVICE_NAME_FILTER = r"^([x]{0,1}[shv]d[a-z][0-9]*|sr[0-9]+)$"
+DEVICE_NAME_RE = re.compile(DEVICE_NAME_FILTER)
 WS = re.compile("[%s]+" % (whitespace))
 FSTAB_PATH = "/etc/fstab"
 
 LOG = logging.getLogger(__name__)
 
 
-def is_mdname(name):
+def is_meta_device_name(name):
     # return true if this is a metadata service name
     if name in ["ami", "root", "swap"]:
         return True
@@ -48,6 +48,25 @@
     return False
 
 
+def _get_nth_partition_for_device(device_path, partition_number):
+    potential_suffixes = [str(partition_number), 'p%s' % (partition_number,),
+                          '-part%s' % (partition_number,)]
+    for suffix in potential_suffixes:
+        potential_partition_device = '%s%s' % (device_path, suffix)
+        if os.path.exists(potential_partition_device):
+            return potential_partition_device
+    return None
+
+
+def _is_block_device(device_path, partition_path=None):
+    device_name = os.path.realpath(device_path).split('/')[-1]
+    sys_path = os.path.join('/sys/block/', device_name)
+    if partition_path is not None:
+        sys_path = os.path.join(
+            sys_path, os.path.realpath(partition_path).split('/')[-1])
+    return os.path.exists(sys_path)
+
+
 def sanitize_devname(startname, transformer, log):
     log.debug("Attempting to determine the real name of %s", startname)
 
@@ -58,21 +77,34 @@
         devname = "ephemeral0"
         log.debug("Adjusted mount option from ephemeral to ephemeral0")
 
-    (blockdev, part) = util.expand_dotted_devname(devname)
-
-    if is_mdname(blockdev):
-        orig = blockdev
-        blockdev = transformer(blockdev)
-        if not blockdev:
-            return None
-        if not blockdev.startswith("/"):
-            blockdev = "/dev/%s" % blockdev
-        log.debug("Mapped metadata name %s to %s", orig, blockdev)
-    else:
-        if SHORTNAME.match(startname):
-            blockdev = "/dev/%s" % blockdev
-
-    return devnode_for_dev_part(blockdev, part)
+    device_path, partition_number = util.expand_dotted_devname(devname)
+
+    if is_meta_device_name(device_path):
+        orig = device_path
+        device_path = transformer(device_path)
+        if not device_path:
+            return None
+        if not device_path.startswith("/"):
+            device_path = "/dev/%s" % (device_path,)
+        log.debug("Mapped metadata name %s to %s", orig, device_path)
+    else:
+        if DEVICE_NAME_RE.match(startname):
+            device_path = "/dev/%s" % (device_path,)
+
+    partition_path = None
+    if partition_number is None:
+        partition_path = _get_nth_partition_for_device(device_path, 1)
+    else:
+        partition_path = _get_nth_partition_for_device(device_path,
+                                                       partition_number)
+        if partition_path is None:
+            return None
+
+    if _is_block_device(device_path, partition_path):
+        if partition_path is not None:
+            return partition_path
+        return device_path
+    return None
 
 
 def suggested_swapsize(memsize=None, maxsize=None, fsys=None):
@@ -366,49 +398,3 @@
         util.subp(("mount", "-a"))
     except:
         util.logexc(log, "Activating mounts via 'mount -a' failed")
-
-
-def devnode_for_dev_part(device, partition):
-    """
-    Find the name of the partition. While this might seem rather
-    straight forward, its not since some devices are '<device><partition>'
-    while others are '<device>p<partition>'. For example, /dev/xvda3 on EC2
-    will present as /dev/xvda3p1 for the first partition since /dev/xvda3 is
-    a block device.
-    """
-    if not os.path.exists(device):
-        return None
-
-    short_name = os.path.basename(device)
-    sys_path = "/sys/block/%s" % short_name
-
-    if not os.path.exists(sys_path):
-        LOG.debug("did not find entry for %s in /sys/block", short_name)
-        return None
-
-    sys_long_path = sys_path + "/" + short_name
-
-    if partition is not None:
-        partition = str(partition)
-
-    if partition is None:
-        valid_mappings = [sys_long_path + "1", sys_long_path + "p1"]
-    elif partition != "0":
-        valid_mappings = [sys_long_path + "%s" % partition,
-                          sys_long_path + "p%s" % partition]
-    else:
-        valid_mappings = []
-
-    for cdisk in valid_mappings:
-        if not os.path.exists(cdisk):
-            continue
-
-        dev_path = "/dev/%s" % os.path.basename(cdisk)
-        if os.path.exists(dev_path):
-            return dev_path
-
-    if partition is None or partition == "0":
-        return device
-
-    LOG.debug("Did not fine partition %s for device %s", partition, device)
-    return None

=== modified file 'cloudinit/sources/DataSourceAzure.py'
--- cloudinit/sources/DataSourceAzure.py	2015-05-22 16:28:17 +0000
+++ cloudinit/sources/DataSourceAzure.py	2015-07-15 12:00:55 +0000
@@ -254,7 +254,7 @@
 
         self.metadata.update(fabric_data)
 
-        found_ephemeral = find_ephemeral_disk()
+        found_ephemeral = find_fabric_formatted_ephemeral_disk()
         if found_ephemeral:
             self.ds_cfg['disk_aliases']['ephemeral0'] = found_ephemeral
             LOG.debug("using detected ephemeral0 of %s", found_ephemeral)
@@ -276,30 +276,33 @@
     return len(fnmatch.filter(os.listdir(mp), '*[!cdrom]*'))
 
 
-def find_ephemeral_part():
-    """
-    Locate the default ephmeral0.1 device. This will be the first device
-    that has a LABEL of DEF_EPHEMERAL_LABEL and is a NTFS device. If Azure
-    gets more ephemeral devices, this logic will only identify the first
-    such device.
-    """
-    c_label_devs = util.find_devs_with("LABEL=%s" % DEF_EPHEMERAL_LABEL)
-    c_fstype_devs = util.find_devs_with("TYPE=ntfs")
-    for dev in c_label_devs:
-        if dev in c_fstype_devs:
-            return dev
+def find_fabric_formatted_ephemeral_part():
+    """
+    Locate the first fabric formatted ephemeral device.
+    """
+    potential_locations = ['/dev/disk/cloud/azure_resource-part1',
+                           '/dev/disk/azure/resource-part1']
+    device_location = None
+    for potential_location in potential_locations:
+        if os.path.exists(potential_location):
+            device_location = potential_location
+            break
+    if device_location is None:
+        return None
+    ntfs_devices = util.find_devs_with("TYPE=ntfs")
+    real_device = os.path.realpath(device_location)
+    if real_device in ntfs_devices:
+        return device_location
     return None
 
 
-def find_ephemeral_disk():
+def find_fabric_formatted_ephemeral_disk():
     """
     Get the ephemeral disk.
     """
-    part_dev = find_ephemeral_part()
-    if part_dev and str(part_dev[-1]).isdigit():
-        return part_dev[:-1]
-    elif part_dev:
-        return part_dev
+    part_dev = find_fabric_formatted_ephemeral_part()
+    if part_dev:
+        return part_dev.split('-')[0]
     return None
 
 
@@ -313,7 +316,7 @@
     new ephemeral device is detected, cloud-init overrides the default
     frequency for both disk-setup and mounts for the current boot only.
     """
-    device = find_ephemeral_part()
+    device = find_fabric_formatted_ephemeral_part()
     if not device:
         LOG.debug("no default fabric formated ephemeral0.1 found")
         return None

=== modified file 'tests/unittests/test_datasource/test_azure.py'
--- tests/unittests/test_datasource/test_azure.py	2015-05-22 16:28:17 +0000
+++ tests/unittests/test_datasource/test_azure.py	2015-07-15 12:00:55 +0000
@@ -475,10 +475,12 @@
             mock.patch.object(DataSourceAzure, 'list_possible_azure_ds_devs',
                               mock.MagicMock(return_value=[])))
         self.patches.enter_context(
-            mock.patch.object(DataSourceAzure, 'find_ephemeral_disk',
+            mock.patch.object(DataSourceAzure,
+                              'find_fabric_formatted_ephemeral_disk',
                               mock.MagicMock(return_value=None)))
         self.patches.enter_context(
-            mock.patch.object(DataSourceAzure, 'find_ephemeral_part',
+            mock.patch.object(DataSourceAzure,
+                              'find_fabric_formatted_ephemeral_part',
                               mock.MagicMock(return_value=None)))
         self.patches.enter_context(
             mock.patch.object(DataSourceAzure, 'get_metadata_from_fabric',

=== added file 'tests/unittests/test_handler/test_handler_mounts.py'
--- tests/unittests/test_handler/test_handler_mounts.py	1970-01-01 00:00:00 +0000
+++ tests/unittests/test_handler/test_handler_mounts.py	2015-07-15 12:00:55 +0000
@@ -0,0 +1,133 @@
+import os.path
+import shutil
+import tempfile
+
+from cloudinit.config import cc_mounts
+
+from .. import helpers as test_helpers
+
+try:
+    from unittest import mock
+except ImportError:
+    import mock
+
+
+class TestSanitizeDevname(test_helpers.FilesystemMockingTestCase):
+
+    def setUp(self):
+        super(TestSanitizeDevname, self).setUp()
+        self.new_root = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.new_root)
+        self.patchOS(self.new_root)
+
+    def _touch(self, path):
+        path = os.path.join(self.new_root, path.lstrip('/'))
+        basedir = os.path.dirname(path)
+        if not os.path.exists(basedir):
+            os.makedirs(basedir)
+        open(path, 'a').close()
+
+    def _makedirs(self, directory):
+        directory = os.path.join(self.new_root, directory.lstrip('/'))
+        if not os.path.exists(directory):
+            os.makedirs(directory)
+
+    def mock_existence_of_disk(self, disk_path):
+        self._touch(disk_path)
+        self._makedirs(os.path.join('/sys/block', disk_path.split('/')[-1]))
+
+    def mock_existence_of_partition(self, disk_path, partition_number):
+        self.mock_existence_of_disk(disk_path)
+        self._touch(disk_path + str(partition_number))
+        disk_name = disk_path.split('/')[-1]
+        self._makedirs(os.path.join('/sys/block',
+                                    disk_name,
+                                    disk_name + str(partition_number)))
+
+    def test_existent_full_disk_path_is_returned(self):
+        disk_path = '/dev/sda'
+        self.mock_existence_of_disk(disk_path)
+        self.assertEqual(disk_path,
+                         cc_mounts.sanitize_devname(disk_path,
+                                                    lambda x: None,
+                                                    mock.Mock()))
+
+    def test_existent_disk_name_returns_full_path(self):
+        disk_name = 'sda'
+        disk_path = '/dev/' + disk_name
+        self.mock_existence_of_disk(disk_path)
+        self.assertEqual(disk_path,
+                         cc_mounts.sanitize_devname(disk_name,
+                                                    lambda x: None,
+                                                    mock.Mock()))
+
+    def test_existent_meta_disk_is_returned(self):
+        actual_disk_path = '/dev/sda'
+        self.mock_existence_of_disk(actual_disk_path)
+        self.assertEqual(
+            actual_disk_path,
+            cc_mounts.sanitize_devname('ephemeral0',
+                                       lambda x: actual_disk_path,
+                                       mock.Mock()))
+
+    def test_existent_meta_partition_is_returned(self):
+        disk_name, partition_part = '/dev/sda', '1'
+        actual_partition_path = disk_name + partition_part
+        self.mock_existence_of_partition(disk_name, partition_part)
+        self.assertEqual(
+            actual_partition_path,
+            cc_mounts.sanitize_devname('ephemeral0.1',
+                                       lambda x: disk_name,
+                                       mock.Mock()))
+
+    def test_existent_meta_partition_with_p_is_returned(self):
+        disk_name, partition_part = '/dev/sda', 'p1'
+        actual_partition_path = disk_name + partition_part
+        self.mock_existence_of_partition(disk_name, partition_part)
+        self.assertEqual(
+            actual_partition_path,
+            cc_mounts.sanitize_devname('ephemeral0.1',
+                                       lambda x: disk_name,
+                                       mock.Mock()))
+
+    def test_first_partition_returned_if_existent_disk_is_partitioned(self):
+        disk_name, partition_part = '/dev/sda', '1'
+        actual_partition_path = disk_name + partition_part
+        self.mock_existence_of_partition(disk_name, partition_part)
+        self.assertEqual(
+            actual_partition_path,
+            cc_mounts.sanitize_devname('ephemeral0',
+                                       lambda x: disk_name,
+                                       mock.Mock()))
+
+    def test_nth_partition_returned_if_requested(self):
+        disk_name, partition_part = '/dev/sda', '3'
+        actual_partition_path = disk_name + partition_part
+        self.mock_existence_of_partition(disk_name, partition_part)
+        self.assertEqual(
+            actual_partition_path,
+            cc_mounts.sanitize_devname('ephemeral0.3',
+                                       lambda x: disk_name,
+                                       mock.Mock()))
+
+    def test_transformer_returning_none_returns_none(self):
+        self.assertIsNone(
+            cc_mounts.sanitize_devname(
+                'ephemeral0', lambda x: None, mock.Mock()))
+
+    def test_missing_device_returns_none(self):
+        self.assertIsNone(
+            cc_mounts.sanitize_devname('/dev/sda', None, mock.Mock()))
+
+    def test_missing_sys_returns_none(self):
+        disk_path = '/dev/sda'
+        self._makedirs(disk_path)
+        self.assertIsNone(
+            cc_mounts.sanitize_devname(disk_path, None, mock.Mock()))
+
+    def test_existent_disk_but_missing_partition_returns_none(self):
+        disk_path = '/dev/sda'
+        self.mock_existence_of_disk(disk_path)
+        self.assertIsNone(
+            cc_mounts.sanitize_devname(
+                'ephemeral0.1', lambda x: disk_path, mock.Mock()))

=== added directory 'udev'
=== added file 'udev/azure-ephemeral.rules'
--- udev/azure-ephemeral.rules	1970-01-01 00:00:00 +0000
+++ udev/azure-ephemeral.rules	2015-07-15 12:00:55 +0000
@@ -0,0 +1,18 @@
+# Azure specific rules
+ACTION!="add|change", GOTO="cloud_init_end"
+SUBSYSTEM!="block", GOTO="cloud_init_end"
+ATTRS{ID_VENDOR}!="Msft", GOTO="cloud_init_end"
+ATTRS{ID_MODEL}!="Virtual_Disk", GOTO="cloud_init_end"
+
+# Root has a GUID of 0000 as the second value
+# The resource/resource has GUID of 0001 as the second value
+ATTRS{device_id}=="?00000000-0000-*", ENV{fabric_name}="azure_root", GOTO="ci_azure_names"
+ATTRS{device_id}=="?00000000-0001-*", ENV{fabric_name}="azure_resource", GOTO="ci_azure_names"
+GOTO="cloud_init_end"
+
+# Create the symlinks
+LABEL="ci_azure_names"
+ENV{DEVTYPE}=="disk", SYMLINK+="disk/cloud/$env{fabric_name}"
+ENV{DEVTYPE}=="partition", SYMLINK+="disk/cloud/$env{fabric_name}-part%n"
+
+LABEL="cloud_init_end"


Follow ups