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