222d62
From 767c4f590bd1ac6cd32c34be8cb813a2cbec08ad Mon Sep 17 00:00:00 2001
222d62
From: Eduardo Otubo <otubo@redhat.com>
222d62
Date: Fri, 5 Oct 2018 09:53:01 +0200
222d62
Subject: [PATCH 2/4] Azure: Ignore NTFS mount errors when checking ephemeral
222d62
 drive
222d62
222d62
RH-Author: Eduardo Otubo <otubo@redhat.com>
222d62
Message-id: <20181005095303.20597-3-otubo@redhat.com>
222d62
Patchwork-id: 82385
222d62
O-Subject: [RHEL-8.0 cloud-init PATCH 2/4] Azure: Ignore NTFS mount errors when checking ephemeral drive
222d62
Bugzilla: 1615599
222d62
RH-Acked-by: Cathy Avery <cavery@redhat.com>
222d62
RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
222d62
RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com>
222d62
222d62
commit aa4eeb80839382117e1813e396dc53aa634fd7ba
222d62
Author: Paul Meyer <paulmey@microsoft.com>
222d62
Date:   Wed May 23 15:45:39 2018 -0400
222d62
222d62
    Azure: Ignore NTFS mount errors when checking ephemeral drive
222d62
222d62
    The Azure data source provides a method to check whether a NTFS partition
222d62
    on the ephemeral disk is safe for reformatting to ext4. The method checks
222d62
    to see if there are customer data files on the disk. However, mounting
222d62
    the partition fails on systems that do not have the capability of
222d62
    mounting NTFS. Note that in this case, it is also very unlikely that the
222d62
    NTFS partition would have been used by the system (since it can't mount
222d62
    it). The only case would be where an update to the system removed the
222d62
    capability to mount NTFS, the likelihood of which is also very small.
222d62
    This change allows the reformatting of the ephemeral disk to ext4 on
222d62
    systems where mounting NTFS is not supported.
222d62
222d62
Signed-off-by: Eduardo Otubo <otubo@redhat.com>
222d62
Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
222d62
---
222d62
 cloudinit/sources/DataSourceAzure.py          |  63 ++++++++++++----
222d62
 cloudinit/util.py                             |   5 +-
222d62
 tests/unittests/test_datasource/test_azure.py | 105 +++++++++++++++++++++-----
222d62
 3 files changed, 138 insertions(+), 35 deletions(-)
222d62
222d62
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
222d62
index 23b4d53..7e49455 100644
222d62
--- a/cloudinit/sources/DataSourceAzure.py
222d62
+++ b/cloudinit/sources/DataSourceAzure.py
222d62
@@ -214,6 +214,7 @@ BUILTIN_CLOUD_CONFIG = {
222d62
 }
222d62
 
222d62
 DS_CFG_PATH = ['datasource', DS_NAME]
222d62
+DS_CFG_KEY_PRESERVE_NTFS = 'never_destroy_ntfs'
222d62
 DEF_EPHEMERAL_LABEL = 'Temporary Storage'
222d62
 
222d62
 # The redacted password fails to meet password complexity requirements
222d62
@@ -400,14 +401,9 @@ class DataSourceAzure(sources.DataSource):
222d62
         if found == ddir:
222d62
             LOG.debug("using files cached in %s", ddir)
222d62
 
222d62
-        # azure / hyper-v provides random data here
222d62
-        # TODO. find the seed on FreeBSD platform
222d62
-        # now update ds_cfg to reflect contents pass in config
222d62
-        if not util.is_FreeBSD():
222d62
-            seed = util.load_file("/sys/firmware/acpi/tables/OEM0",
222d62
-                                  quiet=True, decode=False)
222d62
-            if seed:
222d62
-                self.metadata['random_seed'] = seed
222d62
+        seed = _get_random_seed()
222d62
+        if seed:
222d62
+            self.metadata['random_seed'] = seed
222d62
 
222d62
         user_ds_cfg = util.get_cfg_by_path(self.cfg, DS_CFG_PATH, {})
222d62
         self.ds_cfg = util.mergemanydict([user_ds_cfg, self.ds_cfg])
222d62
@@ -537,7 +533,9 @@ class DataSourceAzure(sources.DataSource):
222d62
         return fabric_data
222d62
 
222d62
     def activate(self, cfg, is_new_instance):
222d62
-        address_ephemeral_resize(is_new_instance=is_new_instance)
222d62
+        address_ephemeral_resize(is_new_instance=is_new_instance,
222d62
+                                 preserve_ntfs=self.ds_cfg.get(
222d62
+                                     DS_CFG_KEY_PRESERVE_NTFS, False))
222d62
         return
222d62
 
222d62
     @property
222d62
@@ -581,17 +579,29 @@ def _has_ntfs_filesystem(devpath):
222d62
     return os.path.realpath(devpath) in ntfs_devices
222d62
 
222d62
 
222d62
-def can_dev_be_reformatted(devpath):
222d62
-    """Determine if block device devpath is newly formatted ephemeral.
222d62
+def can_dev_be_reformatted(devpath, preserve_ntfs):
222d62
+    """Determine if the ephemeral drive at devpath should be reformatted.
222d62
 
222d62
-    A newly formatted disk will:
222d62
+    A fresh ephemeral disk is formatted by Azure and will:
222d62
       a.) have a partition table (dos or gpt)
222d62
       b.) have 1 partition that is ntfs formatted, or
222d62
           have 2 partitions with the second partition ntfs formatted.
222d62
           (larger instances with >2TB ephemeral disk have gpt, and will
222d62
            have a microsoft reserved partition as part 1.  LP: #1686514)
222d62
       c.) the ntfs partition will have no files other than possibly
222d62
-          'dataloss_warning_readme.txt'"""
222d62
+          'dataloss_warning_readme.txt'
222d62
+
222d62
+    User can indicate that NTFS should never be destroyed by setting
222d62
+    DS_CFG_KEY_PRESERVE_NTFS in dscfg.
222d62
+    If data is found on NTFS, user is warned to set DS_CFG_KEY_PRESERVE_NTFS
222d62
+    to make sure cloud-init does not accidentally wipe their data.
222d62
+    If cloud-init cannot mount the disk to check for data, destruction
222d62
+    will be allowed, unless the dscfg key is set."""
222d62
+    if preserve_ntfs:
222d62
+        msg = ('config says to never destroy NTFS (%s.%s), skipping checks' %
222d62
+               (".".join(DS_CFG_PATH), DS_CFG_KEY_PRESERVE_NTFS))
222d62
+        return False, msg
222d62
+
222d62
     if not os.path.exists(devpath):
222d62
         return False, 'device %s does not exist' % devpath
222d62
 
222d62
@@ -624,18 +634,27 @@ def can_dev_be_reformatted(devpath):
222d62
     bmsg = ('partition %s (%s) on device %s was ntfs formatted' %
222d62
             (cand_part, cand_path, devpath))
222d62
     try:
222d62
-        file_count = util.mount_cb(cand_path, count_files)
222d62
+        file_count = util.mount_cb(cand_path, count_files, mtype="ntfs",
222d62
+                                   update_env_for_mount={'LANG': 'C'})
222d62
     except util.MountFailedError as e:
222d62
+        if "mount: unknown filesystem type 'ntfs'" in str(e):
222d62
+            return True, (bmsg + ' but this system cannot mount NTFS,'
222d62
+                          ' assuming there are no important files.'
222d62
+                          ' Formatting allowed.')
222d62
         return False, bmsg + ' but mount of %s failed: %s' % (cand_part, e)
222d62
 
222d62
     if file_count != 0:
222d62
+        LOG.warning("it looks like you're using NTFS on the ephemeral disk, "
222d62
+                    'to ensure that filesystem does not get wiped, set '
222d62
+                    '%s.%s in config', '.'.join(DS_CFG_PATH),
222d62
+                    DS_CFG_KEY_PRESERVE_NTFS)
222d62
         return False, bmsg + ' but had %d files on it.' % file_count
222d62
 
222d62
     return True, bmsg + ' and had no important files. Safe for reformatting.'
222d62
 
222d62
 
222d62
 def address_ephemeral_resize(devpath=RESOURCE_DISK_PATH, maxwait=120,
222d62
-                             is_new_instance=False):
222d62
+                             is_new_instance=False, preserve_ntfs=False):
222d62
     # wait for ephemeral disk to come up
222d62
     naplen = .2
222d62
     missing = util.wait_for_files([devpath], maxwait=maxwait, naplen=naplen,
222d62
@@ -651,7 +670,7 @@ def address_ephemeral_resize(devpath=RESOURCE_DISK_PATH, maxwait=120,
222d62
     if is_new_instance:
222d62
         result, msg = (True, "First instance boot.")
222d62
     else:
222d62
-        result, msg = can_dev_be_reformatted(devpath)
222d62
+        result, msg = can_dev_be_reformatted(devpath, preserve_ntfs)
222d62
 
222d62
     LOG.debug("reformattable=%s: %s", result, msg)
222d62
     if not result:
222d62
@@ -965,6 +984,18 @@ def _check_freebsd_cdrom(cdrom_dev):
222d62
     return False
222d62
 
222d62
 
222d62
+def _get_random_seed():
222d62
+    """Return content random seed file if available, otherwise,
222d62
+       return None."""
222d62
+    # azure / hyper-v provides random data here
222d62
+    # TODO. find the seed on FreeBSD platform
222d62
+    # now update ds_cfg to reflect contents pass in config
222d62
+    if util.is_FreeBSD():
222d62
+        return None
222d62
+    return util.load_file("/sys/firmware/acpi/tables/OEM0",
222d62
+                          quiet=True, decode=False)
222d62
+
222d62
+
222d62
 def list_possible_azure_ds_devs():
222d62
     devlist = []
222d62
     if util.is_FreeBSD():
222d62
diff --git a/cloudinit/util.py b/cloudinit/util.py
222d62
index 0ab2c48..c8e14ba 100644
222d62
--- a/cloudinit/util.py
222d62
+++ b/cloudinit/util.py
222d62
@@ -1608,7 +1608,8 @@ def mounts():
222d62
     return mounted
222d62
 
222d62
 
222d62
-def mount_cb(device, callback, data=None, rw=False, mtype=None, sync=True):
222d62
+def mount_cb(device, callback, data=None, rw=False, mtype=None, sync=True,
222d62
+             update_env_for_mount=None):
222d62
     """
222d62
     Mount the device, call method 'callback' passing the directory
222d62
     in which it was mounted, then unmount.  Return whatever 'callback'
222d62
@@ -1670,7 +1671,7 @@ def mount_cb(device, callback, data=None, rw=False, mtype=None, sync=True):
222d62
                         mountcmd.extend(['-t', mtype])
222d62
                     mountcmd.append(device)
222d62
                     mountcmd.append(tmpd)
222d62
-                    subp(mountcmd)
222d62
+                    subp(mountcmd, update_env=update_env_for_mount)
222d62
                     umount = tmpd  # This forces it to be unmounted (when set)
222d62
                     mountpoint = tmpd
222d62
                     break
222d62
diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
222d62
index 3e8b791..af2c93a 100644
222d62
--- a/tests/unittests/test_datasource/test_azure.py
222d62
+++ b/tests/unittests/test_datasource/test_azure.py
222d62
@@ -1,10 +1,10 @@
222d62
 # This file is part of cloud-init. See LICENSE file for license information.
222d62
 
222d62
 from cloudinit import helpers
222d62
-from cloudinit.util import b64e, decode_binary, load_file, write_file
222d62
 from cloudinit.sources import DataSourceAzure as dsaz
222d62
-from cloudinit.util import find_freebsd_part
222d62
-from cloudinit.util import get_path_dev_freebsd
222d62
+from cloudinit.util import (b64e, decode_binary, load_file, write_file,
222d62
+                            find_freebsd_part, get_path_dev_freebsd,
222d62
+                            MountFailedError)
222d62
 from cloudinit.version import version_string as vs
222d62
 from cloudinit.tests.helpers import (CiTestCase, TestCase, populate_dir, mock,
222d62
                                      ExitStack, PY26, SkipTest)
222d62
@@ -95,6 +95,8 @@ class TestAzureDataSource(CiTestCase):
222d62
         self.patches = ExitStack()
222d62
         self.addCleanup(self.patches.close)
222d62
 
222d62
+        self.patches.enter_context(mock.patch.object(dsaz, '_get_random_seed'))
222d62
+
222d62
         super(TestAzureDataSource, self).setUp()
222d62
 
222d62
     def apply_patches(self, patches):
222d62
@@ -335,6 +337,18 @@ fdescfs            /dev/fd          fdescfs rw              0 0
222d62
         self.assertTrue(ret)
222d62
         self.assertEqual(data['agent_invoked'], '_COMMAND')
222d62
 
222d62
+    def test_sys_cfg_set_never_destroy_ntfs(self):
222d62
+        sys_cfg = {'datasource': {'Azure': {
222d62
+            'never_destroy_ntfs': 'user-supplied-value'}}}
222d62
+        data = {'ovfcontent': construct_valid_ovf_env(data={}),
222d62
+                'sys_cfg': sys_cfg}
222d62
+
222d62
+        dsrc = self._get_ds(data)
222d62
+        ret = self._get_and_setup(dsrc)
222d62
+        self.assertTrue(ret)
222d62
+        self.assertEqual(dsrc.ds_cfg.get(dsaz.DS_CFG_KEY_PRESERVE_NTFS),
222d62
+                         'user-supplied-value')
222d62
+
222d62
     def test_username_used(self):
222d62
         odata = {'HostName': "myhost", 'UserName': "myuser"}
222d62
         data = {'ovfcontent': construct_valid_ovf_env(data=odata)}
222d62
@@ -676,6 +690,8 @@ class TestAzureBounce(CiTestCase):
222d62
                               mock.MagicMock(return_value={})))
222d62
         self.patches.enter_context(
222d62
             mock.patch.object(dsaz.util, 'which', lambda x: True))
222d62
+        self.patches.enter_context(
222d62
+            mock.patch.object(dsaz, '_get_random_seed'))
222d62
 
222d62
         def _dmi_mocks(key):
222d62
             if key == 'system-uuid':
222d62
@@ -957,7 +973,9 @@ class TestCanDevBeReformatted(CiTestCase):
222d62
             # return sorted by partition number
222d62
             return sorted(ret, key=lambda d: d[0])
222d62
 
222d62
-        def mount_cb(device, callback):
222d62
+        def mount_cb(device, callback, mtype, update_env_for_mount):
222d62
+            self.assertEqual('ntfs', mtype)
222d62
+            self.assertEqual('C', update_env_for_mount.get('LANG'))
222d62
             p = self.tmp_dir()
222d62
             for f in bypath.get(device).get('files', []):
222d62
                 write_file(os.path.join(p, f), content=f)
222d62
@@ -988,14 +1006,16 @@ class TestCanDevBeReformatted(CiTestCase):
222d62
                     '/dev/sda2': {'num': 2},
222d62
                     '/dev/sda3': {'num': 3},
222d62
                 }}})
222d62
-        value, msg = dsaz.can_dev_be_reformatted("/dev/sda")
222d62
+        value, msg = dsaz.can_dev_be_reformatted("/dev/sda",
222d62
+                                                 preserve_ntfs=False)
222d62
         self.assertFalse(value)
222d62
         self.assertIn("3 or more", msg.lower())
222d62
 
222d62
     def test_no_partitions_is_false(self):
222d62
         """A disk with no partitions can not be formatted."""
222d62
         self.patchup({'/dev/sda': {}})
222d62
-        value, msg = dsaz.can_dev_be_reformatted("/dev/sda")
222d62
+        value, msg = dsaz.can_dev_be_reformatted("/dev/sda",
222d62
+                                                 preserve_ntfs=False)
222d62
         self.assertFalse(value)
222d62
         self.assertIn("not partitioned", msg.lower())
222d62
 
222d62
@@ -1007,7 +1027,8 @@ class TestCanDevBeReformatted(CiTestCase):
222d62
                     '/dev/sda1': {'num': 1},
222d62
                     '/dev/sda2': {'num': 2, 'fs': 'ext4', 'files': []},
222d62
                 }}})
222d62
-        value, msg = dsaz.can_dev_be_reformatted("/dev/sda")
222d62
+        value, msg = dsaz.can_dev_be_reformatted("/dev/sda",
222d62
+                                                 preserve_ntfs=False)
222d62
         self.assertFalse(value)
222d62
         self.assertIn("not ntfs", msg.lower())
222d62
 
222d62
@@ -1020,7 +1041,8 @@ class TestCanDevBeReformatted(CiTestCase):
222d62
                     '/dev/sda2': {'num': 2, 'fs': 'ntfs',
222d62
                                   'files': ['secret.txt']},
222d62
                 }}})
222d62
-        value, msg = dsaz.can_dev_be_reformatted("/dev/sda")
222d62
+        value, msg = dsaz.can_dev_be_reformatted("/dev/sda",
222d62
+                                                 preserve_ntfs=False)
222d62
         self.assertFalse(value)
222d62
         self.assertIn("files on it", msg.lower())
222d62
 
222d62
@@ -1032,7 +1054,8 @@ class TestCanDevBeReformatted(CiTestCase):
222d62
                     '/dev/sda1': {'num': 1},
222d62
                     '/dev/sda2': {'num': 2, 'fs': 'ntfs', 'files': []},
222d62
                 }}})
222d62
-        value, msg = dsaz.can_dev_be_reformatted("/dev/sda")
222d62
+        value, msg = dsaz.can_dev_be_reformatted("/dev/sda",
222d62
+                                                 preserve_ntfs=False)
222d62
         self.assertTrue(value)
222d62
         self.assertIn("safe for", msg.lower())
222d62
 
222d62
@@ -1043,7 +1066,8 @@ class TestCanDevBeReformatted(CiTestCase):
222d62
                 'partitions': {
222d62
                     '/dev/sda1': {'num': 1, 'fs': 'zfs'},
222d62
                 }}})
222d62
-        value, msg = dsaz.can_dev_be_reformatted("/dev/sda")
222d62
+        value, msg = dsaz.can_dev_be_reformatted("/dev/sda",
222d62
+                                                 preserve_ntfs=False)
222d62
         self.assertFalse(value)
222d62
         self.assertIn("not ntfs", msg.lower())
222d62
 
222d62
@@ -1055,9 +1079,14 @@ class TestCanDevBeReformatted(CiTestCase):
222d62
                     '/dev/sda1': {'num': 1, 'fs': 'ntfs',
222d62
                                   'files': ['file1.txt', 'file2.exe']},
222d62
                 }}})
222d62
-        value, msg = dsaz.can_dev_be_reformatted("/dev/sda")
222d62
-        self.assertFalse(value)
222d62
-        self.assertIn("files on it", msg.lower())
222d62
+        with mock.patch.object(dsaz.LOG, 'warning') as warning:
222d62
+            value, msg = dsaz.can_dev_be_reformatted("/dev/sda",
222d62
+                                                     preserve_ntfs=False)
222d62
+            wmsg = warning.call_args[0][0]
222d62
+            self.assertIn("looks like you're using NTFS on the ephemeral disk",
222d62
+                          wmsg)
222d62
+            self.assertFalse(value)
222d62
+            self.assertIn("files on it", msg.lower())
222d62
 
222d62
     def test_one_partition_ntfs_empty_is_true(self):
222d62
         """1 mountable ntfs partition and no files can be formatted."""
222d62
@@ -1066,7 +1095,8 @@ class TestCanDevBeReformatted(CiTestCase):
222d62
                 'partitions': {
222d62
                     '/dev/sda1': {'num': 1, 'fs': 'ntfs', 'files': []}
222d62
                 }}})
222d62
-        value, msg = dsaz.can_dev_be_reformatted("/dev/sda")
222d62
+        value, msg = dsaz.can_dev_be_reformatted("/dev/sda",
222d62
+                                                 preserve_ntfs=False)
222d62
         self.assertTrue(value)
222d62
         self.assertIn("safe for", msg.lower())
222d62
 
222d62
@@ -1078,7 +1108,8 @@ class TestCanDevBeReformatted(CiTestCase):
222d62
                     '/dev/sda1': {'num': 1, 'fs': 'ntfs',
222d62
                                   'files': ['dataloss_warning_readme.txt']}
222d62
                 }}})
222d62
-        value, msg = dsaz.can_dev_be_reformatted("/dev/sda")
222d62
+        value, msg = dsaz.can_dev_be_reformatted("/dev/sda",
222d62
+                                                 preserve_ntfs=False)
222d62
         self.assertTrue(value)
222d62
         self.assertIn("safe for", msg.lower())
222d62
 
222d62
@@ -1093,7 +1124,8 @@ class TestCanDevBeReformatted(CiTestCase):
222d62
                         'num': 1, 'fs': 'ntfs', 'files': [self.warning_file],
222d62
                         'realpath': '/dev/sdb1'}
222d62
                 }}})
222d62
-        value, msg = dsaz.can_dev_be_reformatted(epath)
222d62
+        value, msg = dsaz.can_dev_be_reformatted(epath,
222d62
+                                                 preserve_ntfs=False)
222d62
         self.assertTrue(value)
222d62
         self.assertIn("safe for", msg.lower())
222d62
 
222d62
@@ -1112,10 +1144,49 @@ class TestCanDevBeReformatted(CiTestCase):
222d62
                     epath + '-part3': {'num': 3, 'fs': 'ext',
222d62
                                        'realpath': '/dev/sdb3'}
222d62
                 }}})
222d62
-        value, msg = dsaz.can_dev_be_reformatted(epath)
222d62
+        value, msg = dsaz.can_dev_be_reformatted(epath,
222d62
+                                                 preserve_ntfs=False)
222d62
         self.assertFalse(value)
222d62
         self.assertIn("3 or more", msg.lower())
222d62
 
222d62
+    def test_ntfs_mount_errors_true(self):
222d62
+        """can_dev_be_reformatted does not fail if NTFS is unknown fstype."""
222d62
+        self.patchup({
222d62
+            '/dev/sda': {
222d62
+                'partitions': {
222d62
+                    '/dev/sda1': {'num': 1, 'fs': 'ntfs', 'files': []}
222d62
+                }}})
222d62
+
222d62
+        err = ("Unexpected error while running command.\n",
222d62
+               "Command: ['mount', '-o', 'ro,sync', '-t', 'auto', ",
222d62
+               "'/dev/sda1', '/fake-tmp/dir']\n"
222d62
+               "Exit code: 32\n"
222d62
+               "Reason: -\n"
222d62
+               "Stdout: -\n"
222d62
+               "Stderr: mount: unknown filesystem type 'ntfs'")
222d62
+        self.m_mount_cb.side_effect = MountFailedError(
222d62
+            'Failed mounting %s to %s due to: %s' %
222d62
+            ('/dev/sda', '/fake-tmp/dir', err))
222d62
+
222d62
+        value, msg = dsaz.can_dev_be_reformatted('/dev/sda',
222d62
+                                                 preserve_ntfs=False)
222d62
+        self.assertTrue(value)
222d62
+        self.assertIn('cannot mount NTFS, assuming', msg)
222d62
+
222d62
+    def test_never_destroy_ntfs_config_false(self):
222d62
+        """Normally formattable situation with never_destroy_ntfs set."""
222d62
+        self.patchup({
222d62
+            '/dev/sda': {
222d62
+                'partitions': {
222d62
+                    '/dev/sda1': {'num': 1, 'fs': 'ntfs',
222d62
+                                  'files': ['dataloss_warning_readme.txt']}
222d62
+                }}})
222d62
+        value, msg = dsaz.can_dev_be_reformatted("/dev/sda",
222d62
+                                                 preserve_ntfs=True)
222d62
+        self.assertFalse(value)
222d62
+        self.assertIn("config says to never destroy NTFS "
222d62
+                      "(datasource.Azure.never_destroy_ntfs)", msg)
222d62
+
222d62
 
222d62
 class TestAzureNetExists(CiTestCase):
222d62
 
222d62
-- 
222d62
1.8.3.1
222d62