Merge "Add wait_for_disk to destroy_disk_metadata function"

This commit is contained in:
Zuul 2018-01-17 22:09:47 +00:00 committed by Gerrit Code Review
commit cadd5786db
6 changed files with 187 additions and 68 deletions

View File

@ -13,12 +13,8 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import re
from oslo_concurrency import processutils
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from oslo_service import loopingcall
from ironic_lib.common.i18n import _ from ironic_lib.common.i18n import _
from ironic_lib import exception from ironic_lib import exception
@ -67,7 +63,6 @@ class DiskPartitioner(object):
self._disk_label = disk_label self._disk_label = disk_label
self._alignment = alignment self._alignment = alignment
self._partitions = [] self._partitions = []
self._fuser_pids_re = re.compile(r'((\d)+\s*)+')
def _exec(self, *args): def _exec(self, *args):
# NOTE(lucasagomes): utils.execute() is already a wrapper on top # NOTE(lucasagomes): utils.execute() is already a wrapper on top
@ -110,30 +105,6 @@ class DiskPartitioner(object):
""" """
return enumerate(self._partitions, 1) return enumerate(self._partitions, 1)
def _wait_for_disk_to_become_available(self, retries, max_retries, pids,
stderr):
retries[0] += 1
if retries[0] > max_retries:
raise loopingcall.LoopingCallDone()
try:
# NOTE(ifarkas): fuser returns a non-zero return code if none of
# the specified files is accessed
out, err = utils.execute('fuser', self._device,
check_exit_code=[0, 1], run_as_root=True)
if not out and not err:
raise loopingcall.LoopingCallDone()
else:
if err:
stderr[0] = err
if out:
pids_match = re.search(self._fuser_pids_re, out)
pids[0] = pids_match.group()
except processutils.ProcessExecutionError as exc:
LOG.warning('Failed to check the device %(device)s with fuser:'
' %(err)s', {'device': self._device, 'err': exc})
def commit(self): def commit(self):
"""Write to the disk.""" """Write to the disk."""
LOG.debug("Committing partitions to disk.") LOG.debug("Committing partitions to disk.")
@ -151,30 +122,13 @@ class DiskPartitioner(object):
self._exec(*cmd_args) self._exec(*cmd_args)
retries = [0] try:
pids = [''] utils.wait_for_disk_to_become_available(self._device)
fuser_err = [''] except exception.IronicException as e:
interval = CONF.disk_partitioner.check_device_interval
max_retries = CONF.disk_partitioner.check_device_max_retries
timer = loopingcall.FixedIntervalLoopingCall(
self._wait_for_disk_to_become_available,
retries, max_retries, pids, fuser_err)
timer.start(interval=interval).wait()
if retries[0] > max_retries:
if pids[0]:
raise exception.InstanceDeployFailure( raise exception.InstanceDeployFailure(
_('Disk partitioning failed on device %(device)s. ' _('Disk partitioning failed on device %(device)s. '
'Processes with the following PIDs are holding it: ' 'Error: %(error)s')
'%(pids)s. Time out waiting for completion.') % {'device': self._device, 'error': e})
% {'device': self._device, 'pids': pids[0]})
else:
raise exception.InstanceDeployFailure(
_('Disk partitioning failed on device %(device)s. Fuser '
'exited with "%(fuser_err)s". Time out waiting for '
'completion.')
% {'device': self._device, 'fuser_err': fuser_err[0]})
def list_opts(): def list_opts():

View File

@ -379,6 +379,14 @@ def destroy_disk_metadata(dev, node_uuid):
utils.execute('sgdisk', '-Z', dev, run_as_root=True, utils.execute('sgdisk', '-Z', dev, run_as_root=True,
use_standard_locale=True) use_standard_locale=True)
try:
utils.wait_for_disk_to_become_available(dev)
except exception.IronicException as e:
raise exception.InstanceDeployFailure(
_('Destroying metadata failed on device %(device)s. '
'Error: %(error)s')
% {'device': dev, 'error': e})
LOG.info("Disk metadata on %(dev)s successfully destroyed for node " LOG.info("Disk metadata on %(dev)s successfully destroyed for node "
"%(node)s", {'dev': dev, 'node': node_uuid}) "%(node)s", {'dev': dev, 'node': node_uuid})

View File

@ -81,7 +81,8 @@ class DiskPartitionerTestCase(base.IronicLibTestCase):
'mkpart', 'fake-type', 'fake-fs-type', '3', '4', 'mkpart', 'fake-type', 'fake-fs-type', '3', '4',
'set', '3', 'bios_grub', 'on') 'set', '3', 'bios_grub', 'on')
mock_utils_exc.assert_called_once_with( mock_utils_exc.assert_called_once_with(
'fuser', '/dev/fake', run_as_root=True, check_exit_code=[0, 1]) 'fuser', '/dev/fake', run_as_root=True,
check_exit_code=[0, 1])
@mock.patch.object(eventlet.greenthread, 'sleep', lambda seconds: None) @mock.patch.object(eventlet.greenthread, 'sleep', lambda seconds: None)
@mock.patch.object(disk_partitioner.DiskPartitioner, '_exec', @mock.patch.object(disk_partitioner.DiskPartitioner, '_exec',
@ -98,6 +99,9 @@ class DiskPartitionerTestCase(base.IronicLibTestCase):
'fs_type': 'fake-fs-type', 'fs_type': 'fake-fs-type',
'type': 'fake-type', 'type': 'fake-type',
'size': 1})] 'size': 1})]
# TODO(TheJulia): fuser man page indicates only pids are returned via
# stdout. Meaning tests that put the device on stdout need to be
# corrected.
fuser_outputs = iter([("/dev/fake: 10000 10001", None), (None, None)]) fuser_outputs = iter([("/dev/fake: 10000 10001", None), (None, None)])
with mock.patch.object(dp, 'get_partitions', autospec=True) as mock_gp: with mock.patch.object(dp, 'get_partitions', autospec=True) as mock_gp:
@ -111,7 +115,8 @@ class DiskPartitionerTestCase(base.IronicLibTestCase):
'mkpart', 'fake-type', 'fake-fs-type', '2', '3', 'mkpart', 'fake-type', 'fake-fs-type', '2', '3',
'set', '2', 'boot', 'on') 'set', '2', 'boot', 'on')
mock_utils_exc.assert_called_with( mock_utils_exc.assert_called_with(
'fuser', '/dev/fake', run_as_root=True, check_exit_code=[0, 1]) 'fuser', '/dev/fake', run_as_root=True,
check_exit_code=[0, 1])
self.assertEqual(2, mock_utils_exc.call_count) self.assertEqual(2, mock_utils_exc.call_count)
@mock.patch.object(eventlet.greenthread, 'sleep', lambda seconds: None) @mock.patch.object(eventlet.greenthread, 'sleep', lambda seconds: None)
@ -141,7 +146,8 @@ class DiskPartitionerTestCase(base.IronicLibTestCase):
'mkpart', 'fake-type', 'fake-fs-type', '2', '3', 'mkpart', 'fake-type', 'fake-fs-type', '2', '3',
'set', '2', 'boot', 'on') 'set', '2', 'boot', 'on')
mock_utils_exc.assert_called_with( mock_utils_exc.assert_called_with(
'fuser', '/dev/fake', run_as_root=True, check_exit_code=[0, 1]) 'fuser', '/dev/fake', run_as_root=True,
check_exit_code=[0, 1])
self.assertEqual(20, mock_utils_exc.call_count) self.assertEqual(20, mock_utils_exc.call_count)
# Mock the eventlet.greenthread.sleep for the looping_call # Mock the eventlet.greenthread.sleep for the looping_call
@ -173,5 +179,6 @@ class DiskPartitionerTestCase(base.IronicLibTestCase):
'mkpart', 'fake-type', 'fake-fs-type', '2', '3', 'mkpart', 'fake-type', 'fake-fs-type', '2', '3',
'set', '2', 'boot', 'on') 'set', '2', 'boot', 'on')
mock_utils_exc.assert_called_with( mock_utils_exc.assert_called_with(
'fuser', '/dev/fake', run_as_root=True, check_exit_code=[0, 1]) 'fuser', '/dev/fake', run_as_root=True,
check_exit_code=[0, 1])
self.assertEqual(20, mock_utils_exc.call_count) self.assertEqual(20, mock_utils_exc.call_count)

View File

@ -23,7 +23,6 @@ import mock
from oslo_concurrency import processutils from oslo_concurrency import processutils
from oslo_config import cfg from oslo_config import cfg
from oslo_serialization import base64 from oslo_serialization import base64
from oslo_service import loopingcall
from oslo_utils import imageutils from oslo_utils import imageutils
import requests import requests
@ -465,7 +464,7 @@ class MakePartitionsTestCase(base.IronicLibTestCase):
self.assertEqual(expected_result, result) self.assertEqual(expected_result, result)
@mock.patch.object(utils, 'execute', autospec=True) @mock.patch.object(utils, 'execute', autospec=True, return_value=('', ''))
class DestroyMetaDataTestCase(base.IronicLibTestCase): class DestroyMetaDataTestCase(base.IronicLibTestCase):
def setUp(self): def setUp(self):
@ -479,7 +478,10 @@ class DestroyMetaDataTestCase(base.IronicLibTestCase):
use_standard_locale=True), use_standard_locale=True),
mock.call('sgdisk', '-Z', 'fake-dev', mock.call('sgdisk', '-Z', 'fake-dev',
run_as_root=True, run_as_root=True,
use_standard_locale=True)] use_standard_locale=True),
mock.call('fuser', self.dev,
check_exit_code=[0, 1],
run_as_root=True)]
disk_utils.destroy_disk_metadata(self.dev, self.node_uuid) disk_utils.destroy_disk_metadata(self.dev, self.node_uuid)
mock_exec.assert_has_calls(expected_calls) mock_exec.assert_has_calls(expected_calls)
@ -514,7 +516,8 @@ class DestroyMetaDataTestCase(base.IronicLibTestCase):
mock_exec.side_effect = iter( mock_exec.side_effect = iter(
[processutils.ProcessExecutionError(description='--force'), [processutils.ProcessExecutionError(description='--force'),
(None, None), (None, None),
(None, None)]) (None, None),
('', '')])
expected_call = [mock.call('wipefs', '--force', '--all', 'fake-dev', expected_call = [mock.call('wipefs', '--force', '--all', 'fake-dev',
run_as_root=True, run_as_root=True,
@ -562,13 +565,7 @@ class PopulateImageTestCase(base.IronicLibTestCase):
self.assertFalse(mock_dd.called) self.assertFalse(mock_dd.called)
def _looping_call_done(*args, **kwargs): @mock.patch.object(utils, 'wait_for_disk_to_become_available', lambda *_: None)
raise loopingcall.LoopingCallDone()
@mock.patch.object(disk_partitioner.DiskPartitioner,
'_wait_for_disk_to_become_available',
_looping_call_done)
@mock.patch.object(disk_utils, 'is_block_device', lambda d: True) @mock.patch.object(disk_utils, 'is_block_device', lambda d: True)
@mock.patch.object(disk_utils, 'block_uuid', lambda p: 'uuid') @mock.patch.object(disk_utils, 'block_uuid', lambda p: 'uuid')
@mock.patch.object(disk_utils, 'dd', lambda *_: None) @mock.patch.object(disk_utils, 'dd', lambda *_: None)

View File

@ -463,3 +463,79 @@ class MatchRootDeviceTestCase(base.IronicLibTestCase):
dev = utils.match_root_device_hints(empty_dev, {'model': 'foo'}) dev = utils.match_root_device_hints(empty_dev, {'model': 'foo'})
self.assertIsNone(dev) self.assertIsNone(dev)
self.assertTrue(mock_warn.called) self.assertTrue(mock_warn.called)
class WaitForDisk(base.IronicLibTestCase):
@mock.patch.object(utils, 'execute', autospec=True)
def test_wait_for_disk_to_become_available(self, mock_exc):
mock_exc.return_value = ('', '')
utils.wait_for_disk_to_become_available('fake-dev')
fuser_cmd = ['fuser', 'fake-dev']
fuser_call = mock.call(*fuser_cmd, run_as_root=True,
check_exit_code=[0, 1])
self.assertEqual(1, mock_exc.call_count)
mock_exc.assert_has_calls([fuser_call])
@mock.patch.object(utils, 'execute', autospec=True,
side_effect=processutils.ProcessExecutionError(
stderr='fake'))
def test_wait_for_disk_to_become_available_no_fuser(self, mock_exc):
CONF.disk_partitioner.check_device_max_retries = 2
self.assertRaises(exception.IronicException,
utils.wait_for_disk_to_become_available,
'fake-dev')
fuser_cmd = ['fuser', 'fake-dev']
fuser_call = mock.call(*fuser_cmd, run_as_root=True,
check_exit_code=[0, 1])
self.assertEqual(2, mock_exc.call_count)
mock_exc.assert_has_calls([fuser_call, fuser_call])
@mock.patch.object(utils, 'execute', autospec=True)
def test_wait_for_disk_to_become_available_device_in_use(self, mock_exc):
# NOTE(TheJulia): Looks like fuser returns the actual list of pids
# in the stdout output, where as all other text is returned in
# stderr.
CONF.disk_partitioner.check_device_interval = .01
CONF.disk_partitioner.check_device_max_retries = 2
mock_exc.side_effect = [(' 1234 ', 'fake-dev: '),
(' 15503 3919 15510 15511', 'fake-dev:')]
expected_error = ('Processes with the following PIDs are '
'holding device fake-dev: 15503, 3919, 15510, '
'15511. Timed out waiting for completion.')
self.assertRaisesRegex(
exception.IronicException,
expected_error,
utils.wait_for_disk_to_become_available,
'fake-dev')
fuser_cmd = ['fuser', 'fake-dev']
fuser_call = mock.call(*fuser_cmd, run_as_root=True,
check_exit_code=[0, 1])
self.assertEqual(2, mock_exc.call_count)
mock_exc.assert_has_calls([fuser_call, fuser_call])
@mock.patch.object(utils, 'execute', autospec=True)
def test_wait_for_disk_to_become_available_no_device(self, mock_exc):
# NOTE(TheJulia): Looks like fuser returns the actual list of pids
# in the stdout output, where as all other text is returned in
# stderr.
CONF.disk_partitioner.check_device_interval = .01
CONF.disk_partitioner.check_device_max_retries = 2
mock_exc.return_value = ('', 'Specified filename /dev/fake '
'does not exist.')
expected_error = ('Fuser exited with "Specified filename '
'/dev/fake does not exist." while checking '
'locks for device fake-dev. Timed out waiting '
'for completion.')
self.assertRaisesRegex(
exception.IronicException,
expected_error,
utils.wait_for_disk_to_become_available,
'fake-dev')
fuser_cmd = ['fuser', 'fake-dev']
fuser_call = mock.call(*fuser_cmd, run_as_root=True,
check_exit_code=[0, 1])
self.assertEqual(2, mock_exc.call_count)
mock_exc.assert_has_calls([fuser_call, fuser_call])

View File

@ -26,6 +26,7 @@ import re
from oslo_concurrency import processutils from oslo_concurrency import processutils
from oslo_config import cfg from oslo_config import cfg
from oslo_service import loopingcall
from oslo_utils import excutils from oslo_utils import excutils
from oslo_utils import specs_matcher from oslo_utils import specs_matcher
from oslo_utils import strutils from oslo_utils import strutils
@ -417,3 +418,79 @@ def match_root_device_hints(devices, root_device_hints):
return dev return dev
LOG.warning('No device found that matches the root device hints') LOG.warning('No device found that matches the root device hints')
def wait_for_disk_to_become_available(device):
"""Wait for a disk device to become available.
Waits for a disk device to become available for use by
waiting until all process locks on the device have been
released.
Timeout and iteration settings come from the configuration
options used by the in-library disk_partitioner:
``check_device_interval`` and ``check_device_max_retries``.
:params device: The path to the device.
:raises: IronicException If the disk fails to become
available.
"""
retries = [0]
pids = ['']
stderr = ['']
interval = CONF.disk_partitioner.check_device_interval
max_retries = CONF.disk_partitioner.check_device_max_retries
def _wait_for_disk(device, retries, max_retries, pids, stderr):
# A regex is likely overkill here, but variations in fuser
# means we should likely use it.
fuser_pids_re = re.compile(r'\d+')
retries[0] += 1
if retries[0] > max_retries:
raise loopingcall.LoopingCallDone()
try:
# NOTE(ifarkas): fuser returns a non-zero return code if none of
# the specified files is accessed.
# NOTE(TheJulia): fuser does not report LVM devices as in use
# unless the LVM device-mapper device is the
# device that is directly polled.
# NOTE(TheJulia): The -m flag allows fuser to reveal data about
# mounted filesystems, which should be considered
# busy/locked. That being said, it is not used
# because busybox fuser has a different behavior.
# NOTE(TheJuia): fuser outputs a list of found PIDs to stdout.
# All other text is returned via stderr, and the
# output to a terminal is merged as a result.
out, err = execute('fuser', device, check_exit_code=[0, 1],
run_as_root=True)
if err:
stderr[0] = err
if out:
pids[0] = fuser_pids_re.findall(out)
if not out and not err:
raise loopingcall.LoopingCallDone()
except processutils.ProcessExecutionError as exc:
LOG.warning('Failed to check the device %(device)s with fuser:'
' %(err)s', {'device': device, 'err': exc})
timer = loopingcall.FixedIntervalLoopingCall(
_wait_for_disk,
device, retries, max_retries, pids, stderr)
timer.start(interval=interval).wait()
if retries[0] > max_retries:
if pids[0]:
raise exception.IronicException(
_('Processes with the following PIDs are holding '
'device %(device)s: %(pids)s. '
'Timed out waiting for completion.')
% {'device': device, 'pids': ', '.join(pids[0])})
else:
raise exception.IronicException(
_('Fuser exited with "%(fuser_err)s" while checking '
'locks for device %(device)s. Timed out waiting for '
'completion.')
% {'device': device, 'fuser_err': stderr[0]})