Robustify writing iscsi target persistence file

With the current code, it's possible to end up with a zero-size
persistence file (or even corruption of the contents) if the
process gets killed or the system takes a power outage.

Switch to a "write to temp file and rename" model for writing the
persistence file.  This will make it more robust against unclean
process termination or unclean system shutdown.

Change-Id: Ic4fdb5a9f6f622b2ab9658f7d4206e4c8ca55046
Closes-Bug: #1499795
This commit is contained in:
Mitsuhiro Tanino 2015-09-25 10:38:23 -06:00
parent 0c5cce77be
commit 0d47c8ec3a
5 changed files with 90 additions and 18 deletions

View File

@ -178,20 +178,6 @@ class TestTgtAdmDriver(tf.TargetDriverFixture):
self.testvol_path,
chap_auth=('chap_foo', 'chap_bar')))
mock_open.assert_called_once_with(
os.path.join(self.fake_volumes_dir, self.test_vol.split(':')[1]),
'w+')
expected = ('\n<target iqn.2010-10.org.openstack:volume-%(id)s>\n'
' backing-store %(bspath)s\n'
' driver iscsi\n'
' incominguser chap_foo chap_bar\n'
' bsoflags foo\n'
' write-cache bar\n'
'</target>\n' % {'id': self.VOLUME_ID,
'bspath': self.testvol_path})
self.assertEqual(expected,
mock_open.return_value.write.call_args[0][0])
def test_create_iscsi_target_already_exists(self):
def _fake_execute(*args, **kwargs):
if 'update' in args:

View File

@ -191,6 +191,50 @@ class GenericUtilsTestCase(test.TestCase):
utils.read_file_as_root,
test_filepath)
@mock.patch('tempfile.NamedTemporaryFile')
@mock.patch.object(os, 'open')
@mock.patch.object(os, 'fdatasync')
@mock.patch.object(os, 'fsync')
@mock.patch.object(os, 'rename')
@mock.patch.object(os, 'close')
@mock.patch.object(os.path, 'isfile')
@mock.patch.object(os, 'unlink')
def test_write_configfile(self, mock_unlink, mock_isfile, mock_close,
mock_rename, mock_fsync, mock_fdatasync,
mock_open, mock_tmp):
filename = 'foo'
directory = '/some/random/path'
filepath = os.path.join(directory, filename)
expected = ('\n<target iqn.2010-10.org.openstack:volume-%(id)s>\n'
' backing-store %(bspath)s\n'
' driver iscsi\n'
' incominguser chap_foo chap_bar\n'
' bsoflags foo\n'
' write-cache bar\n'
'</target>\n' % {'id': filename,
'bspath': filepath})
# Normal case
utils.robust_file_write(directory, filename, expected)
mock_open.assert_called_once_with(directory, os.O_DIRECTORY)
mock_rename.assert_called_once_with(mock.ANY, filepath)
self.assertEqual(
expected.encode('utf-8'),
mock_tmp.return_value.__enter__.return_value.write.call_args[0][0]
)
# Failure to write persistent file.
tempfile = '/some/tempfile'
mock_tmp.return_value.__enter__.return_value.name = tempfile
mock_rename.side_effect = OSError
self.assertRaises(OSError,
utils.robust_file_write,
directory,
filename,
mock.MagicMock())
mock_isfile.assert_called_once_with(tempfile)
mock_unlink.assert_called_once_with(tempfile)
@mock.patch('oslo_utils.timeutils.utcnow')
def test_service_is_up(self, mock_utcnow):
fts_func = datetime.datetime.fromtimestamp

View File

@ -46,6 +46,7 @@ from oslo_concurrency import processutils
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import encodeutils
from oslo_utils import excutils
from oslo_utils import importutils
from oslo_utils import strutils
from oslo_utils import timeutils
@ -452,6 +453,49 @@ def read_file_as_root(file_path):
raise exception.FileNotFound(file_path=file_path)
def robust_file_write(directory, filename, data):
"""Robust file write.
Use "write to temp file and rename" model for writing the
persistence file.
:param directory: Target directory to create a file.
:param filename: File name to store specified data.
:param data: String data.
"""
tempname = None
dirfd = None
try:
dirfd = os.open(directory, os.O_DIRECTORY)
# write data to temporary file
with tempfile.NamedTemporaryFile(prefix=filename,
dir=directory,
delete=False) as tf:
tempname = tf.name
tf.write(data.encode('utf-8'))
tf.flush()
os.fdatasync(tf.fileno())
tf.close()
# Fsync the directory to ensure the fact of the existence of
# the temp file hits the disk.
os.fsync(dirfd)
# If destination file exists, it will be replaced silently.
os.rename(tempname, os.path.join(directory, filename))
# Fsync the directory to ensure the rename hits the disk.
os.fsync(dirfd)
except OSError:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Failed to write persistence file: %(path)s."),
{'path': os.path.join(directory, filename)})
if os.path.isfile(tempname):
os.unlink(tempname)
finally:
if dirfd:
os.close(dirfd)
@contextlib.contextmanager
def temporary_chown(path, owner_uid=None):
"""Temporarily chown a path.

View File

@ -130,8 +130,7 @@ class CxtAdm(iscsi.ISCSITarget):
if os.path.exists(volume_path):
LOG.warning(_LW('Persistence file already exists for volume, '
'found file at: %s'), volume_path)
with open(volume_path, 'w+') as f:
f.write(volume_conf)
utils.robust_file_write(volumes_dir, vol_id, volume_conf)
LOG.debug('Created volume path %(vp)s,\n'
'content: %(vc)s',
{'vp': volume_path, 'vc': volume_conf})

View File

@ -165,8 +165,7 @@ class TgtAdm(iscsi.ISCSITarget):
if os.path.exists(volume_path):
LOG.warning(_LW('Persistence file already exists for volume, '
'found file at: %s'), volume_path)
with open(volume_path, 'w+') as f:
f.write(volume_conf)
utils.robust_file_write(volumes_dir, vol_id, volume_conf)
LOG.debug(('Created volume path %(vp)s,\n'
'content: %(vc)s'),
{'vp': volume_path, 'vc': volume_conf})