Replace pexpect with processutils in volume.py

In investigating the issues with resize, it was noticed that pexpect
is used throughout guestagent/volume.py. These were replaced with
oslo_concurrency.processutils.execute to allow more robust error
handling/reporting.

Certain cases were made more tolerant to unknown states (for
example resize_fs now makes sure the volume is unmounted).
This (hopefully) will fix the intermittent gate issues
plaguing volume-resize lately.

While examining the errors returned from the guest, it was
observed that in one case a fault message from the guest was
almost immediately overwritten by a less-detailed one
from the taskmanager.  Handling was put in place for this
case to only write the fault if the previous fault was 'old'
enough.

The unit tests for volume were cleaned up as well with
regards to proper mocking and checking that the calls
were actually made properly.

Depends-On: Ia30cf5826415e47d59e6d407f95e4d7ff4e8ff6c
Change-Id: I844dd26a968a50532a6220a0762a302c2cadc2d0
Closes-Bug: 1651589
This commit is contained in:
Peter Stachowski 2016-12-20 14:08:32 -08:00
parent 90cb0b022d
commit 8d5576aa1a
4 changed files with 274 additions and 223 deletions

View File

@ -14,15 +14,14 @@
# under the License. # under the License.
import os import os
import shlex
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
import traceback import traceback
from oslo_log import log as logging from oslo_log import log as logging
import pexpect
from trove.common import cfg from trove.common import cfg
from trove.common.exception import GuestError from trove.common import exception
from trove.common.exception import ProcessExecutionError
from trove.common.i18n import _ from trove.common.i18n import _
from trove.common import utils from trove.common import utils
from trove.guestagent.common import operating_system from trove.guestagent.common import operating_system
@ -33,6 +32,12 @@ LOG = logging.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
def log_and_raise(message):
LOG.exception(message)
raise_msg = message + _("\nExc: %s") % traceback.format_exc()
raise exception.GuestError(original_message=raise_msg)
class VolumeDevice(object): class VolumeDevice(object):
def __init__(self, device_path): def __init__(self, device_path):
@ -48,9 +53,14 @@ class VolumeDevice(object):
target_dir = TMP_MOUNT_POINT target_dir = TMP_MOUNT_POINT
if target_subdir: if target_subdir:
target_dir = target_dir + "/" + target_subdir target_dir = target_dir + "/" + target_subdir
utils.execute("sudo", "rsync", "--safe-links", "--perms", try:
"--recursive", "--owner", "--group", "--xattrs", utils.execute("rsync", "--safe-links", "--perms",
"--sparse", source_dir, target_dir) "--recursive", "--owner", "--group", "--xattrs",
"--sparse", source_dir, target_dir,
run_as_root=True, root_helper="sudo")
except exception.ProcessExecutionError:
msg = _("Could not migrate data.")
log_and_raise(msg)
self.unmount(TMP_MOUNT_POINT) self.unmount(TMP_MOUNT_POINT)
def _check_device_exists(self): def _check_device_exists(self):
@ -64,46 +74,48 @@ class VolumeDevice(object):
num_tries = CONF.num_tries num_tries = CONF.num_tries
LOG.debug("Checking if %s exists." % self.device_path) LOG.debug("Checking if %s exists." % self.device_path)
utils.execute('sudo', 'blockdev', '--getsize64', self.device_path, utils.execute("blockdev", "--getsize64", self.device_path,
run_as_root=True, root_helper="sudo",
attempts=num_tries) attempts=num_tries)
except ProcessExecutionError: except exception.ProcessExecutionError:
LOG.exception(_("Error getting device status")) msg = _("Device '%s' is not ready.") % self.device_path
raise GuestError(original_message=_( log_and_raise(msg)
"InvalidDevicePath(path=%s)") % self.device_path)
def _check_format(self): def _check_format(self):
"""Checks that an unmounted volume is formatted.""" """Checks that a volume is formatted."""
cmd = "sudo dumpe2fs %s" % self.device_path LOG.debug("Checking whether '%s' is formatted." % self.device_path)
LOG.debug("Checking whether %s is formatted: %s." %
(self.device_path, cmd))
child = pexpect.spawn(cmd)
try: try:
i = child.expect(['has_journal', 'Wrong magic number']) stdout, stderr = utils.execute(
if i == 0: "dumpe2fs", self.device_path,
return run_as_root=True, root_helper="sudo")
volume_fstype = CONF.volume_fstype if 'has_journal' not in stdout:
raise IOError( msg = _("Volume '%s' does not appear to be formatted.") % (
_('Device path at {0} did not seem to be {1}.').format( self.device_path)
self.device_path, volume_fstype)) raise exception.GuestError(original_message=msg)
except exception.ProcessExecutionError as pe:
except pexpect.EOF: if 'Wrong magic number' in pe.stderr:
raise IOError(_("Volume was not formatted.")) volume_fstype = CONF.volume_fstype
child.expect(pexpect.EOF) msg = _("'Device '%(dev)s' did not seem to be '%(type)s'.") % (
{'dev': self.device_path, 'type': volume_fstype})
log_and_raise(msg)
msg = _("Volume '%s' was not formatted.") % self.device_path
log_and_raise(msg)
def _format(self): def _format(self):
"""Calls mkfs to format the device at device_path.""" """Calls mkfs to format the device at device_path."""
volume_fstype = CONF.volume_fstype volume_fstype = CONF.volume_fstype
format_options = CONF.format_options format_options = shlex.split(CONF.format_options)
cmd = "sudo mkfs -t %s %s %s" % (volume_fstype, format_options.append(self.device_path)
format_options, self.device_path)
volume_format_timeout = CONF.volume_format_timeout volume_format_timeout = CONF.volume_format_timeout
LOG.debug("Formatting %s. Executing: %s." % LOG.debug("Formatting '%s'." % self.device_path)
(self.device_path, cmd)) try:
child = pexpect.spawn(cmd, timeout=volume_format_timeout) utils.execute_with_timeout(
# child.expect("(y,n)") "mkfs", "--type", volume_fstype, *format_options,
# child.sendline('y') run_as_root=True, root_helper="sudo",
child.expect(pexpect.EOF) timeout=volume_format_timeout)
except exception.ProcessExecutionError:
msg = _("Could not format '%s'.") % self.device_path
log_and_raise(msg)
def format(self): def format(self):
"""Formats the device at device_path and checks the filesystem.""" """Formats the device at device_path and checks the filesystem."""
@ -120,59 +132,77 @@ class VolumeDevice(object):
if write_to_fstab: if write_to_fstab:
mount_point.write_to_fstab() mount_point.write_to_fstab()
def _wait_for_mount(self, mount_point, timeout=2):
"""Wait for a fs to be mounted."""
def wait_for_mount():
return operating_system.is_mount(mount_point)
try:
utils.poll_until(wait_for_mount, sleep_time=1, time_out=timeout)
except exception.PollTimeOut:
return False
return True
def resize_fs(self, mount_point): def resize_fs(self, mount_point):
"""Resize the filesystem on the specified device.""" """Resize the filesystem on the specified device."""
self._check_device_exists() self._check_device_exists()
# Some OS's will mount a file systems after it's attached if
# an entry is put in the fstab file (like Trove does).
# Thus it may be necessary to wait for the mount and then unmount
# the fs again (since the volume was just attached).
if self._wait_for_mount(mount_point, timeout=2):
LOG.debug("Unmounting '%s' before resizing." % mount_point)
self.unmount(mount_point)
try: try:
# check if the device is mounted at mount_point before e2fsck utils.execute("e2fsck", "-f", "-p", self.device_path,
if not os.path.ismount(mount_point): run_as_root=True, root_helper="sudo")
utils.execute("e2fsck", "-f", "-p", self.device_path,
run_as_root=True, root_helper="sudo")
utils.execute("resize2fs", self.device_path, utils.execute("resize2fs", self.device_path,
run_as_root=True, root_helper="sudo") run_as_root=True, root_helper="sudo")
except ProcessExecutionError: except exception.ProcessExecutionError:
LOG.exception(_("Error resizing file system.")) msg = _("Error resizing the filesystem with device '%s'.") % (
msg = _("Error resizing the filesystem with device '%(dev)s'.\n" self.device_path)
"Exc: %(exc)s") % ( log_and_raise(msg)
{'dev': self.device_path,
'exc': traceback.format_exc()})
raise GuestError(original_message=msg)
def unmount(self, mount_point): def unmount(self, mount_point):
if operating_system.is_mount(mount_point): if operating_system.is_mount(mount_point):
cmd = "sudo umount %s" % mount_point try:
child = pexpect.spawn(cmd) utils.execute("umount", mount_point,
child.expect(pexpect.EOF) run_as_root=True, root_helper='sudo')
except exception.ProcessExecutionError:
msg = _("Error unmounting '%s'.") % mount_point
log_and_raise(msg)
else:
LOG.debug("'%s' is not a mounted fs, cannot unmount", mount_point)
def unmount_device(self, device_path): def unmount_device(self, device_path):
# unmount if device is already mounted # unmount if device is already mounted
mount_points = self.mount_points(device_path) mount_points = self.mount_points(device_path)
for mnt in mount_points: for mnt in mount_points:
LOG.info(_("Device %(device)s is already mounted in " LOG.info(_("Device '%(device)s' is mounted on "
"%(mount_point)s. Unmounting now.") % "'%(mount_point)s'. Unmounting now.") %
{'device': device_path, 'mount_point': mnt}) {'device': device_path, 'mount_point': mnt})
self.unmount(mnt) self.unmount(mnt)
def mount_points(self, device_path): def mount_points(self, device_path):
"""Returns a list of mount points on the specified device.""" """Returns a list of mount points on the specified device."""
stdout, stderr = utils.execute( stdout, stderr = utils.execute(
"grep %s /etc/mtab" % device_path, "grep '^%s ' /etc/mtab" % device_path,
shell=True, check_exit_code=[0, 1]) shell=True, check_exit_code=[0, 1])
return [entry.strip().split()[1] for entry in stdout.splitlines()] return [entry.strip().split()[1] for entry in stdout.splitlines()]
def set_readahead_size(self, readahead_size, def set_readahead_size(self, readahead_size):
execute_function=utils.execute):
"""Set the readahead size of disk.""" """Set the readahead size of disk."""
self._check_device_exists() self._check_device_exists()
try: try:
execute_function("sudo", "blockdev", "--setra", utils.execute("blockdev", "--setra",
readahead_size, self.device_path) readahead_size, self.device_path,
except ProcessExecutionError: run_as_root=True, root_helper="sudo")
LOG.exception(_("Error setting readhead size to %(size)s " except exception.ProcessExecutionError:
"for device %(device)s.") % msg = _("Error setting readahead size to %(size)s "
{'size': readahead_size, 'device': self.device_path}) "for device %(device)s.") % {
raise GuestError(original_message=_( 'size': readahead_size, 'device': self.device_path}
"Error setting readhead size: %s.") % self.device_path) log_and_raise(msg)
class VolumeMountPoint(object): class VolumeMountPoint(object):
@ -184,17 +214,21 @@ class VolumeMountPoint(object):
self.mount_options = CONF.mount_options self.mount_options = CONF.mount_options
def mount(self): def mount(self):
if not os.path.exists(self.mount_point): if not operating_system.exists(self.mount_point, is_directory=True,
as_root=True):
operating_system.create_directory(self.mount_point, as_root=True) operating_system.create_directory(self.mount_point, as_root=True)
LOG.debug("Mounting volume. Device path:{0}, mount_point:{1}, " LOG.debug("Mounting volume. Device path:{0}, mount_point:{1}, "
"volume_type:{2}, mount options:{3}".format( "volume_type:{2}, mount options:{3}".format(
self.device_path, self.mount_point, self.volume_fstype, self.device_path, self.mount_point, self.volume_fstype,
self.mount_options)) self.mount_options))
cmd = ("sudo mount -t %s -o %s %s %s" % try:
(self.volume_fstype, self.mount_options, self.device_path, utils.execute("mount", "-t", self.volume_fstype,
self.mount_point)) "-o", self.mount_options,
child = pexpect.spawn(cmd) self.device_path, self.mount_point,
child.expect(pexpect.EOF) run_as_root=True, root_helper="sudo")
except exception.ProcessExecutionError:
msg = _("Could not mount '%s'.") % self.mount_point
log_and_raise(msg)
def write_to_fstab(self): def write_to_fstab(self):
fstab_line = ("%s\t%s\t%s\t%s\t0\t0" % fstab_line = ("%s\t%s\t%s\t%s\t0\t0" %
@ -205,6 +239,11 @@ class VolumeMountPoint(object):
fstab_content = fstab.read() fstab_content = fstab.read()
with NamedTemporaryFile(mode='w', delete=False) as tempfstab: with NamedTemporaryFile(mode='w', delete=False) as tempfstab:
tempfstab.write(fstab_content + fstab_line) tempfstab.write(fstab_content + fstab_line)
utils.execute("sudo", "install", "-o", "root", "-g", "root", "-m", try:
"644", tempfstab.name, "/etc/fstab") utils.execute("install", "-o", "root", "-g", "root",
"-m", "644", tempfstab.name, "/etc/fstab",
run_as_root=True, root_helper="sudo")
except exception.ProcessExecutionError:
msg = _("Could not add '%s' to fstab.") % self.mount_point
log_and_raise(msg)
os.remove(tempfstab.name) os.remove(tempfstab.name)

View File

@ -1534,7 +1534,7 @@ def persist_instance_fault(notification, event_qualifier):
save_instance_fault(instance_id, message, details) save_instance_fault(instance_id, message, details)
def save_instance_fault(instance_id, message, details): def save_instance_fault(instance_id, message, details, skip_delta=None):
if instance_id: if instance_id:
try: try:
# Make sure it's a valid id - sometimes the error is related # Make sure it's a valid id - sometimes the error is related
@ -1544,8 +1544,19 @@ def save_instance_fault(instance_id, message, details):
det = utils.format_output(details) det = utils.format_output(details)
try: try:
fault = DBInstanceFault.find_by(instance_id=instance_id) fault = DBInstanceFault.find_by(instance_id=instance_id)
fault.set_info(msg, det) skip = False
fault.save() # If we were passed in a skip_delta, only update the fault
# if the old one is at least skip_delta seconds in the past
if skip_delta:
skip_time = fault.updated + timedelta(seconds=skip_delta)
now = datetime.now()
skip = now < skip_time
if skip:
LOG.debug(
"Skipping fault message in favor of previous one")
else:
fault.set_info(msg, det)
fault.save()
except exception.ModelNotFoundError: except exception.ModelNotFoundError:
DBInstanceFault.create( DBInstanceFault.create(
instance_id=instance_id, instance_id=instance_id,

View File

@ -364,7 +364,8 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
finally: finally:
if error_message: if error_message:
inst_models.save_instance_fault( inst_models.save_instance_fault(
self.id, error_message, error_details) self.id, error_message, error_details,
skip_delta=USAGE_SLEEP_TIME + 1)
def create_instance(self, flavor, image_id, databases, users, def create_instance(self, flavor, image_id, databases, users,
datastore_manager, packages, volume_size, datastore_manager, packages, volume_size,

View File

@ -12,194 +12,191 @@
# 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 os from mock import ANY, call, DEFAULT, patch, mock_open
from mock import Mock, MagicMock, patch, mock_open from trove.common import exception
import pexpect
from trove.common.exception import GuestError, ProcessExecutionError
from trove.common import utils from trove.common import utils
from trove.guestagent.common import operating_system from trove.guestagent.common import operating_system
from trove.guestagent import volume from trove.guestagent import volume
from trove.tests.unittests import trove_testtools from trove.tests.unittests import trove_testtools
def _setUp_fake_spawn(return_val=0):
fake_spawn = pexpect.spawn('echo')
fake_spawn.expect = Mock(return_value=return_val)
pexpect.spawn = Mock(return_value=fake_spawn)
return fake_spawn
class VolumeDeviceTest(trove_testtools.TestCase): class VolumeDeviceTest(trove_testtools.TestCase):
def setUp(self): def setUp(self):
super(VolumeDeviceTest, self).setUp() super(VolumeDeviceTest, self).setUp()
self.volumeDevice = volume.VolumeDevice('/dev/vdb') self.volumeDevice = volume.VolumeDevice('/dev/vdb')
self.exec_patcher = patch.object(
utils, 'execute', return_value=('has_journal', ''))
self.mock_exec = self.exec_patcher.start()
self.addCleanup(self.exec_patcher.stop)
self.ismount_patcher = patch.object(operating_system, 'is_mount')
self.mock_ismount = self.ismount_patcher.start()
self.addCleanup(self.ismount_patcher.stop)
def tearDown(self): def tearDown(self):
super(VolumeDeviceTest, self).tearDown() super(VolumeDeviceTest, self).tearDown()
@patch.object(pexpect, 'spawn', Mock())
def test_migrate_data(self): def test_migrate_data(self):
origin_execute = utils.execute with patch.multiple(self.volumeDevice,
utils.execute = Mock() mount=DEFAULT, unmount=DEFAULT) as mocks:
origin_os_path_exists = os.path.exists self.volumeDevice.migrate_data('/')
os.path.exists = Mock() self.assertEqual(1, mocks['mount'].call_count)
fake_spawn = _setUp_fake_spawn() self.assertEqual(1, mocks['unmount'].call_count)
self.assertEqual(1, self.mock_exec.call_count)
origin_unmount = self.volumeDevice.unmount calls = [
self.volumeDevice.unmount = MagicMock() call('rsync', '--safe-links', '--perms', '--recursive',
self.volumeDevice.migrate_data('/') '--owner', '--group', '--xattrs',
self.assertEqual(1, fake_spawn.expect.call_count) '--sparse', '/', '/mnt/volume',
self.assertEqual(1, utils.execute.call_count) root_helper='sudo', run_as_root=True),
self.assertEqual(1, self.volumeDevice.unmount.call_count) ]
utils.execute = origin_execute self.mock_exec.assert_has_calls(calls)
self.volumeDevice.unmount = origin_unmount
os.path.exists = origin_os_path_exists
def test__check_device_exists(self): def test__check_device_exists(self):
origin_execute = utils.execute
utils.execute = Mock()
self.volumeDevice._check_device_exists() self.volumeDevice._check_device_exists()
self.assertEqual(1, utils.execute.call_count) self.assertEqual(1, self.mock_exec.call_count)
utils.execute = origin_execute calls = [
call('blockdev', '--getsize64', '/dev/vdb', attempts=3,
root_helper='sudo', run_as_root=True)
]
self.mock_exec.assert_has_calls(calls)
@patch('trove.guestagent.volume.LOG') @patch('trove.guestagent.volume.LOG')
def test_fail__check_device_exists(self, mock_logging): def test_fail__check_device_exists(self, mock_logging):
with patch.object(utils, 'execute', side_effect=ProcessExecutionError): with patch.object(utils, 'execute',
self.assertRaises(GuestError, side_effect=exception.ProcessExecutionError):
self.assertRaises(exception.GuestError,
self.volumeDevice._check_device_exists) self.volumeDevice._check_device_exists)
@patch.object(pexpect, 'spawn', Mock())
def test__check_format(self): def test__check_format(self):
fake_spawn = _setUp_fake_spawn()
self.volumeDevice._check_format() self.volumeDevice._check_format()
self.assertEqual(1, fake_spawn.expect.call_count) self.assertEqual(1, self.mock_exec.call_count)
calls = [
call('dumpe2fs', '/dev/vdb', root_helper='sudo', run_as_root=True)
]
self.mock_exec.assert_has_calls(calls)
@patch.object(pexpect, 'spawn', Mock()) @patch('trove.guestagent.volume.LOG')
def test__check_format_2(self): def test__check_format_2(self, mock_logging):
fake_spawn = _setUp_fake_spawn(return_val=1) self.assertEqual(0, self.mock_exec.call_count)
proc_err = exception.ProcessExecutionError()
proc_err.stderr = 'Wrong magic number'
self.mock_exec.side_effect = proc_err
self.assertRaises(exception.GuestError,
self.volumeDevice._check_format)
self.assertEqual(0, fake_spawn.expect.call_count)
self.assertRaises(IOError, self.volumeDevice._check_format)
@patch.object(pexpect, 'spawn', Mock())
def test__format(self): def test__format(self):
fake_spawn = _setUp_fake_spawn()
self.volumeDevice._format() self.volumeDevice._format()
self.assertEqual(1, self.mock_exec.call_count)
self.assertEqual(1, fake_spawn.expect.call_count) calls = [
self.assertEqual(1, pexpect.spawn.call_count) call('mkfs', '--type', 'ext3', '-m', '5', '/dev/vdb',
root_helper='sudo', run_as_root=True)
]
self.mock_exec.assert_has_calls(calls)
def test_format(self): def test_format(self):
origin_check_device_exists = self.volumeDevice._check_device_exists
origin_format = self.volumeDevice._format
origin_check_format = self.volumeDevice._check_format
self.volumeDevice._check_device_exists = MagicMock()
self.volumeDevice._check_format = MagicMock()
self.volumeDevice._format = MagicMock()
self.volumeDevice.format() self.volumeDevice.format()
self.assertEqual(1, self.volumeDevice._check_device_exists.call_count) self.assertEqual(3, self.mock_exec.call_count)
self.assertEqual(1, self.volumeDevice._format.call_count) calls = [
self.assertEqual(1, self.volumeDevice._check_format.call_count) call('blockdev', '--getsize64', '/dev/vdb', attempts=3,
root_helper='sudo', run_as_root=True),
self.volumeDevice._check_device_exists = origin_check_device_exists call('mkfs', '--type', 'ext3', '-m', '5', '/dev/vdb',
self.volumeDevice._format = origin_format root_helper='sudo', run_as_root=True),
self.volumeDevice._check_format = origin_check_format call('dumpe2fs', '/dev/vdb', root_helper='sudo', run_as_root=True)
]
self.mock_exec.assert_has_calls(calls)
def test_mount(self): def test_mount(self):
origin_ = volume.VolumeMountPoint.mount with patch.multiple(volume.VolumeMountPoint,
volume.VolumeMountPoint.mount = Mock() mount=DEFAULT, write_to_fstab=DEFAULT) as mocks:
origin_os_path_exists = os.path.exists self.volumeDevice.mount('/dev/vba')
os.path.exists = Mock() self.assertEqual(1, mocks['mount'].call_count,
origin_write_to_fstab = volume.VolumeMountPoint.write_to_fstab "Wrong number of calls to mount()")
volume.VolumeMountPoint.write_to_fstab = Mock() self.assertEqual(1, mocks['write_to_fstab'].call_count,
"Wrong number of calls to write_to_fstab()")
self.volumeDevice.mount(Mock) self.mock_exec.assert_not_called()
self.assertEqual(1, volume.VolumeMountPoint.mount.call_count)
self.assertEqual(1, volume.VolumeMountPoint.write_to_fstab.call_count)
volume.VolumeMountPoint.mount = origin_
volume.VolumeMountPoint.write_to_fstab = origin_write_to_fstab
os.path.exists = origin_os_path_exists
def test_resize_fs(self): def test_resize_fs(self):
origin_check_device_exists = self.volumeDevice._check_device_exists with patch.object(operating_system, 'is_mount', return_value=True):
origin_execute = utils.execute mount_point = '/mnt/volume'
utils.execute = Mock() self.volumeDevice.resize_fs(mount_point)
self.volumeDevice._check_device_exists = MagicMock() self.assertEqual(4, self.mock_exec.call_count)
origin_os_path_exists = os.path.exists calls = [
os.path.exists = Mock() call('blockdev', '--getsize64', '/dev/vdb', attempts=3,
root_helper='sudo', run_as_root=True),
call("umount", mount_point, run_as_root=True,
root_helper='sudo'),
call('e2fsck', '-f', '-p', '/dev/vdb', root_helper='sudo',
run_as_root=True),
call('resize2fs', '/dev/vdb', root_helper='sudo',
run_as_root=True)
]
self.mock_exec.assert_has_calls(calls)
self.volumeDevice.resize_fs('/mnt/volume') @patch.object(utils, 'execute',
side_effect=exception.ProcessExecutionError)
self.assertEqual(1, self.volumeDevice._check_device_exists.call_count)
self.assertEqual(2, utils.execute.call_count)
self.volumeDevice._check_device_exists = origin_check_device_exists
os.path.exists = origin_os_path_exists
utils.execute = origin_execute
@patch.object(os.path, 'ismount', return_value=True)
@patch.object(utils, 'execute', side_effect=ProcessExecutionError)
@patch('trove.guestagent.volume.LOG') @patch('trove.guestagent.volume.LOG')
def test_fail_resize_fs(self, mock_logging, mock_execute, mock_mount): def test_fail_resize_fs(self, mock_logging, mock_execute):
with patch.object(self.volumeDevice, '_check_device_exists'): with patch.object(self.volumeDevice, '_check_device_exists'):
self.assertRaises(GuestError, self.assertRaises(exception.GuestError,
self.volumeDevice.resize_fs, '/mnt/volume') self.volumeDevice.resize_fs, '/mnt/volume')
self.assertEqual(1, self.assertEqual(1,
self.volumeDevice._check_device_exists.call_count) self.volumeDevice._check_device_exists.call_count)
self.assertEqual(1, mock_mount.call_count) self.assertEqual(2, self.mock_ismount.call_count)
def test_unmount_positive(self): def test_unmount_positive(self):
self._test_unmount() self._test_unmount()
def test_unmount_negative(self): def test_unmount_negative(self):
self._test_unmount(False) self._test_unmount(has_mount=False)
@patch.object(pexpect, 'spawn', Mock()) def _test_unmount(self, has_mount=True):
def _test_unmount(self, positive=True): with patch.object(operating_system, 'is_mount',
origin_is_mount = operating_system.is_mount return_value=has_mount):
operating_system.is_mount = MagicMock(return_value=positive) self.volumeDevice.unmount('/mnt/volume')
fake_spawn = _setUp_fake_spawn() if has_mount:
self.assertEqual(1, self.mock_exec.call_count)
else:
self.mock_exec.assert_not_called()
self.volumeDevice.unmount('/mnt/volume') def test_mount_points(self):
COUNT = 1 self.mock_exec.return_value = (
if not positive:
COUNT = 0
self.assertEqual(COUNT, fake_spawn.expect.call_count)
operating_system.is_mount = origin_is_mount
@patch.object(utils, 'execute')
def test_mount_points(self, mock_execute):
mock_execute.return_value = (
("/dev/vdb /var/lib/mysql xfs rw 0 0", "")) ("/dev/vdb /var/lib/mysql xfs rw 0 0", ""))
mount_point = self.volumeDevice.mount_points('/dev/vdb') mount_point = self.volumeDevice.mount_points('/dev/vdb')
self.assertEqual(['/var/lib/mysql'], mount_point) self.assertEqual(['/var/lib/mysql'], mount_point)
self.assertEqual(1, self.mock_exec.call_count)
calls = [
call("grep '^/dev/vdb ' /etc/mtab", check_exit_code=[0, 1],
shell=True)
]
self.mock_exec.assert_has_calls(calls)
def test_set_readahead_size(self): def test_set_readahead_size(self):
origin_check_device_exists = self.volumeDevice._check_device_exists
self.volumeDevice._check_device_exists = MagicMock()
mock_execute = MagicMock(return_value=None)
readahead_size = 2048 readahead_size = 2048
self.volumeDevice.set_readahead_size(readahead_size, self.volumeDevice.set_readahead_size(readahead_size)
execute_function=mock_execute) self.assertEqual(2, self.mock_exec.call_count)
blockdev = mock_execute.call_args_list[0] calls = [
call('blockdev', '--getsize64', '/dev/vdb', attempts=3,
blockdev.assert_called_with("sudo", "blockdev", "--setra", root_helper='sudo', run_as_root=True),
readahead_size, "/dev/vdb") call('blockdev', '--setra', readahead_size, '/dev/vdb',
self.volumeDevice._check_device_exists = origin_check_device_exists root_helper='sudo', run_as_root=True),
]
self.mock_exec.assert_has_calls(calls)
@patch('trove.guestagent.volume.LOG') @patch('trove.guestagent.volume.LOG')
def test_fail_set_readahead_size(self, mock_logging): def test_fail_set_readahead_size(self, mock_logging):
mock_execute = MagicMock(side_effect=ProcessExecutionError) self.mock_exec.side_effect = exception.ProcessExecutionError
readahead_size = 2048 readahead_size = 2048
with patch.object(self.volumeDevice, '_check_device_exists'): self.assertRaises(exception.GuestError,
self.assertRaises(GuestError, self.volumeDevice.set_readahead_size, self.volumeDevice.set_readahead_size,
readahead_size, execute_function=mock_execute) readahead_size)
self.volumeDevice._check_device_exists.assert_any_call() self.assertEqual(1, self.mock_exec.call_count)
calls = [
call('blockdev', '--getsize64', '/dev/vdb', attempts=3,
root_helper='sudo', run_as_root=True),
]
self.mock_exec.assert_has_calls(calls)
class VolumeMountPointTest(trove_testtools.TestCase): class VolumeMountPointTest(trove_testtools.TestCase):
@ -208,32 +205,35 @@ class VolumeMountPointTest(trove_testtools.TestCase):
super(VolumeMountPointTest, self).setUp() super(VolumeMountPointTest, self).setUp()
self.volumeMountPoint = volume.VolumeMountPoint('/mnt/device', self.volumeMountPoint = volume.VolumeMountPoint('/mnt/device',
'/dev/vdb') '/dev/vdb')
self.exec_patcher = patch.object(utils, 'execute',
return_value=('', ''))
self.mock_exec = self.exec_patcher.start()
self.addCleanup(self.exec_patcher.stop)
def tearDown(self): def tearDown(self):
super(VolumeMountPointTest, self).tearDown() super(VolumeMountPointTest, self).tearDown()
@patch.object(pexpect, 'spawn', Mock())
def test_mount(self): def test_mount(self):
origin_ = os.path.exists with patch.object(operating_system, 'exists', return_value=False):
os.path.exists = MagicMock(return_value=False)
fake_spawn = _setUp_fake_spawn()
with patch.object(utils, 'execute_with_timeout',
return_value=('0', '')):
self.volumeMountPoint.mount() self.volumeMountPoint.mount()
self.assertEqual(2, self.mock_exec.call_count)
self.assertEqual(1, os.path.exists.call_count) calls = [
self.assertEqual(1, utils.execute_with_timeout.call_count) call('mkdir', '-p', '/dev/vdb', root_helper='sudo',
self.assertEqual(1, fake_spawn.expect.call_count) run_as_root=True),
call('mount', '-t', 'ext3', '-o', 'defaults,noatime',
os.path.exists = origin_ '/mnt/device', '/dev/vdb', root_helper='sudo',
run_as_root=True)
]
self.mock_exec.assert_has_calls(calls)
def test_write_to_fstab(self): def test_write_to_fstab(self):
origin_execute = utils.execute mock_file = mock_open()
utils.execute = Mock() with patch('%s.open' % volume.__name__, mock_file, create=True):
m = mock_open()
with patch('%s.open' % volume.__name__, m, create=True):
self.volumeMountPoint.write_to_fstab() self.volumeMountPoint.write_to_fstab()
self.assertEqual(1, self.mock_exec.call_count)
self.assertEqual(1, utils.execute.call_count) calls = [
utils.execute = origin_execute call('install', '-o', 'root', '-g', 'root', '-m', '644',
ANY, '/etc/fstab', root_helper='sudo',
run_as_root=True)
]
self.mock_exec.assert_has_calls(calls)