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