714c5a3ede
The primary goal of this change is to remove os.path.ismount mocks like: @mock.patch('os.path.ismount', side_effect=[False, True, True, True]) The 2 main problems with these mocks are 1) they're opaque and hard to validate, and 2) they need to be rewritten if the code under test changes the call order, which makes it harder to validate such changes. We replace these mocks with MountFixture, which simply correlates calls to mount and unmount. Change-Id: I3c51b99ca28093013b90da1a7360a6728eaf224d
648 lines
24 KiB
Python
648 lines
24 KiB
Python
# Copyright 2017 Red Hat, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import os.path
|
|
import threading
|
|
import time
|
|
|
|
import eventlet
|
|
import fixtures
|
|
import mock
|
|
from oslo_concurrency import processutils
|
|
from oslo_utils.fixture import uuidsentinel as uuids
|
|
|
|
from nova import exception
|
|
from nova import test
|
|
from nova.virt.libvirt import config as libvirt_config
|
|
from nova.virt.libvirt import guest as libvirt_guest
|
|
from nova.virt.libvirt import host as libvirt_host
|
|
from nova.virt.libvirt.volume import mount
|
|
|
|
|
|
# We wait on events in a few cases. In normal execution the wait period should
|
|
# be in the order of fractions of a millisecond. However, if we've hit a bug we
|
|
# might deadlock and never return. To be nice to our test environment, we cut
|
|
# this short at MAX_WAIT seconds. This should be large enough that normal
|
|
# jitter won't trigger it, but not so long that it's annoying to wait for.
|
|
MAX_WAIT = 2
|
|
|
|
|
|
class ThreadController(object):
|
|
"""Helper class for executing a test thread incrementally by waiting at
|
|
named waitpoints.
|
|
|
|
def test(ctl):
|
|
things()
|
|
ctl.waitpoint('foo')
|
|
more_things()
|
|
ctl.waitpoint('bar')
|
|
final_things()
|
|
|
|
ctl = ThreadController(test)
|
|
ctl.runto('foo')
|
|
assert(things)
|
|
ctl.runto('bar')
|
|
assert(more_things)
|
|
ctl.finish()
|
|
assert(final_things)
|
|
|
|
This gets more interesting when the waitpoints are mocked into non-test
|
|
code.
|
|
"""
|
|
|
|
# A map of threads to controllers
|
|
all_threads = {}
|
|
|
|
def __init__(self, fn):
|
|
"""Create a ThreadController.
|
|
|
|
:param fn: A test function which takes a ThreadController as its
|
|
only argument
|
|
"""
|
|
|
|
# All updates to wait_at and waiting are guarded by wait_lock
|
|
self.wait_lock = threading.Condition()
|
|
# The name of the next wait point
|
|
self.wait_at = None
|
|
# True when waiting at a waitpoint
|
|
self.waiting = False
|
|
# Incremented every time we continue from a waitpoint
|
|
self.epoch = 1
|
|
# The last epoch we waited at
|
|
self.last_epoch = 0
|
|
|
|
self.start_event = eventlet.event.Event()
|
|
self.running = False
|
|
self.complete = False
|
|
|
|
# We must not execute fn() until the thread has been registered in
|
|
# all_threads. eventlet doesn't give us an API to do this directly,
|
|
# so we defer with an Event
|
|
def deferred_start():
|
|
self.start_event.wait()
|
|
fn()
|
|
|
|
with self.wait_lock:
|
|
self.complete = True
|
|
self.wait_lock.notify_all()
|
|
|
|
self.thread = eventlet.greenthread.spawn(deferred_start)
|
|
self.all_threads[self.thread] = self
|
|
|
|
@classmethod
|
|
def current(cls):
|
|
return cls.all_threads.get(eventlet.greenthread.getcurrent())
|
|
|
|
def _ensure_running(self):
|
|
if not self.running:
|
|
self.running = True
|
|
self.start_event.send()
|
|
|
|
def waitpoint(self, name):
|
|
"""Called by the test thread. Wait at a waitpoint called name"""
|
|
with self.wait_lock:
|
|
wait_since = time.time()
|
|
while name == self.wait_at:
|
|
self.waiting = True
|
|
self.wait_lock.notify_all()
|
|
self.wait_lock.wait(1)
|
|
assert(time.time() - wait_since < MAX_WAIT)
|
|
|
|
self.epoch += 1
|
|
self.waiting = False
|
|
self.wait_lock.notify_all()
|
|
|
|
def runto(self, name):
|
|
"""Called by the control thread. Cause the test thread to run until
|
|
reaching a waitpoint called name. When runto() exits, the test
|
|
thread is guaranteed to have reached this waitpoint.
|
|
"""
|
|
with self.wait_lock:
|
|
# Set a new wait point
|
|
self.wait_at = name
|
|
self.wait_lock.notify_all()
|
|
|
|
# We deliberately don't do this first to avoid a race the first
|
|
# time we call runto()
|
|
self._ensure_running()
|
|
|
|
# Wait until the test thread is at the wait point
|
|
wait_since = time.time()
|
|
while self.epoch == self.last_epoch or not self.waiting:
|
|
self.wait_lock.wait(1)
|
|
assert(time.time() - wait_since < MAX_WAIT)
|
|
|
|
self.last_epoch = self.epoch
|
|
|
|
def start(self):
|
|
"""Called by the control thread. Cause the test thread to start
|
|
running, but to not wait for it to complete.
|
|
"""
|
|
self._ensure_running()
|
|
|
|
def finish(self):
|
|
"""Called by the control thread. Cause the test thread to run to
|
|
completion. When finish() exits, the test thread is guaranteed to
|
|
have completed.
|
|
"""
|
|
self._ensure_running()
|
|
|
|
wait_since = time.time()
|
|
with self.wait_lock:
|
|
self.wait_at = None
|
|
self.wait_lock.notify_all()
|
|
while not self.complete:
|
|
self.wait_lock.wait(1)
|
|
assert(time.time() - wait_since < MAX_WAIT)
|
|
|
|
self.thread.wait()
|
|
|
|
|
|
class MountFixture(fixtures.Fixture):
|
|
def __init__(self, test):
|
|
super(MountFixture, self).__init__()
|
|
self.test = test
|
|
|
|
self.mounts = set()
|
|
test.mounts = self.mounts
|
|
|
|
def setUp(self):
|
|
super(MountFixture, self).setUp()
|
|
|
|
self.test.mock_mount = self.useFixture(fixtures.MockPatch(
|
|
'nova.privsep.fs.mount', side_effect=self._fake_mount)).mock
|
|
self.test.mock_umount = self.useFixture(fixtures.MockPatch(
|
|
'nova.privsep.fs.umount', side_effect=self._fake_umount)).mock
|
|
self.test.mock_ismount = self.useFixture(fixtures.MockPatch(
|
|
'os.path.ismount', side_effect=self._fake_ismount)).mock
|
|
|
|
self.test.mock_ensure_tree = self.useFixture(fixtures.MockPatch(
|
|
'oslo_utils.fileutils.ensure_tree')).mock
|
|
self.test.mock_rmdir = self.useFixture(fixtures.MockPatch(
|
|
'nova.privsep.path.rmdir')).mock
|
|
|
|
def _fake_mount(self, fstype, export, mountpoint, options):
|
|
self.mounts.add(mountpoint)
|
|
|
|
def _fake_umount(self, mountpoint):
|
|
self.mounts.remove(mountpoint)
|
|
|
|
def _fake_ismount(self, mountpoint):
|
|
return mountpoint in self.mounts
|
|
|
|
|
|
class HostMountStateTestCase(test.NoDBTestCase):
|
|
def setUp(self):
|
|
super(HostMountStateTestCase, self).setUp()
|
|
|
|
self.mountfixture = self.useFixture(MountFixture(self))
|
|
|
|
def test_init(self):
|
|
# Test that we initialise the state of MountManager correctly at
|
|
# startup
|
|
def fake_disk(disk):
|
|
libvirt_disk = libvirt_config.LibvirtConfigGuestDisk()
|
|
libvirt_disk.source_type = disk[0]
|
|
libvirt_disk.source_path = os.path.join(*disk[1])
|
|
return libvirt_disk
|
|
|
|
def mock_guest(uuid, disks):
|
|
guest = mock.create_autospec(libvirt_guest.Guest)
|
|
guest.uuid = uuid
|
|
guest.get_all_disks.return_value = map(fake_disk, disks)
|
|
return guest
|
|
|
|
local_dir = '/local'
|
|
mountpoint_a = '/mnt/a'
|
|
mountpoint_b = '/mnt/b'
|
|
self.mounts.add(mountpoint_a)
|
|
self.mounts.add(mountpoint_b)
|
|
|
|
guests = map(mock_guest, [uuids.instance_a, uuids.instance_b], [
|
|
# Local file root disk and a volume on each of mountpoints a and b
|
|
[
|
|
('file', (local_dir, uuids.instance_a, 'disk')),
|
|
('file', (mountpoint_a, 'vola1')),
|
|
('file', (mountpoint_b, 'volb1')),
|
|
],
|
|
|
|
# Local LVM root disk and a volume on each of mountpoints a and b
|
|
[
|
|
('block', ('/dev', 'vg', uuids.instance_b + '_disk')),
|
|
('file', (mountpoint_a, 'vola2')),
|
|
('file', (mountpoint_b, 'volb2')),
|
|
]
|
|
])
|
|
|
|
host = mock.create_autospec(libvirt_host.Host)
|
|
host.list_guests.return_value = guests
|
|
|
|
m = mount._HostMountState(host, 0)
|
|
|
|
self.assertEqual([mountpoint_a, mountpoint_b],
|
|
sorted(m.mountpoints.keys()))
|
|
|
|
self.assertSetEqual(set([('vola1', uuids.instance_a),
|
|
('vola2', uuids.instance_b)]),
|
|
m.mountpoints[mountpoint_a].attachments)
|
|
self.assertSetEqual(set([('volb1', uuids.instance_a),
|
|
('volb2', uuids.instance_b)]),
|
|
m.mountpoints[mountpoint_b].attachments)
|
|
|
|
@staticmethod
|
|
def _get_clean_hostmountstate():
|
|
# list_guests returns no guests: _HostMountState initial state is
|
|
# clean.
|
|
host = mock.create_autospec(libvirt_host.Host)
|
|
host.list_guests.return_value = []
|
|
return mount._HostMountState(host, 0)
|
|
|
|
def _sentinel_mount(self, m, vol, mountpoint=mock.sentinel.mountpoint,
|
|
instance=None):
|
|
if instance is None:
|
|
instance = mock.sentinel.instance
|
|
instance.uuid = uuids.instance
|
|
|
|
m.mount(mock.sentinel.fstype, mock.sentinel.export,
|
|
vol, mountpoint, instance,
|
|
[mock.sentinel.option1, mock.sentinel.option2])
|
|
|
|
def _sentinel_umount(self, m, vol, mountpoint=mock.sentinel.mountpoint,
|
|
instance=mock.sentinel.instance):
|
|
m.umount(vol, mountpoint, instance)
|
|
|
|
def test_mount_umount(self):
|
|
# Mount 2 different volumes from the same export. Test that we only
|
|
# mount and umount once.
|
|
m = self._get_clean_hostmountstate()
|
|
|
|
# Mount vol_a from export
|
|
self._sentinel_mount(m, mock.sentinel.vol_a)
|
|
self.mock_ensure_tree.assert_has_calls([
|
|
mock.call(mock.sentinel.mountpoint)])
|
|
self.mock_mount.assert_has_calls([
|
|
mock.call(mock.sentinel.fstype,
|
|
mock.sentinel.export, mock.sentinel.mountpoint,
|
|
[mock.sentinel.option1, mock.sentinel.option2])])
|
|
|
|
# Mount vol_b from export. We shouldn't have mounted again
|
|
self._sentinel_mount(m, mock.sentinel.vol_b)
|
|
self.mock_ensure_tree.assert_has_calls([
|
|
mock.call(mock.sentinel.mountpoint)])
|
|
self.mock_mount.assert_has_calls([
|
|
mock.call(mock.sentinel.fstype,
|
|
mock.sentinel.export, mock.sentinel.mountpoint,
|
|
[mock.sentinel.option1, mock.sentinel.option2])])
|
|
|
|
# Unmount vol_a. We shouldn't have unmounted
|
|
self._sentinel_umount(m, mock.sentinel.vol_a)
|
|
self.mock_ensure_tree.assert_has_calls([
|
|
mock.call(mock.sentinel.mountpoint)])
|
|
self.mock_mount.assert_has_calls([
|
|
mock.call(mock.sentinel.fstype,
|
|
mock.sentinel.export, mock.sentinel.mountpoint,
|
|
[mock.sentinel.option1, mock.sentinel.option2])])
|
|
|
|
# Unmount vol_b. We should have umounted.
|
|
self._sentinel_umount(m, mock.sentinel.vol_b)
|
|
self.mock_ensure_tree.assert_has_calls([
|
|
mock.call(mock.sentinel.mountpoint)])
|
|
self.mock_mount.assert_has_calls([
|
|
mock.call(mock.sentinel.fstype,
|
|
mock.sentinel.export, mock.sentinel.mountpoint,
|
|
[mock.sentinel.option1, mock.sentinel.option2])])
|
|
self.mock_umount.assert_has_calls([
|
|
mock.call(mock.sentinel.mountpoint)])
|
|
self.mock_rmdir.assert_has_calls([
|
|
mock.call(mock.sentinel.mountpoint)])
|
|
|
|
def test_mount_umount_multi_attach(self):
|
|
# Mount a volume from a single export for 2 different instances. Test
|
|
# that we only mount and umount once.
|
|
m = self._get_clean_hostmountstate()
|
|
|
|
instance_a = mock.sentinel.instance_a
|
|
instance_a.uuid = uuids.instance_a
|
|
instance_b = mock.sentinel.instance_b
|
|
instance_b.uuid = uuids.instance_b
|
|
|
|
# Mount vol_a for instance_a
|
|
self._sentinel_mount(m, mock.sentinel.vol_a, instance=instance_a)
|
|
self.mock_mount.assert_has_calls([
|
|
mock.call(mock.sentinel.fstype, mock.sentinel.export,
|
|
mock.sentinel.mountpoint,
|
|
[mock.sentinel.option1, mock.sentinel.option2])])
|
|
self.mock_mount.reset_mock()
|
|
|
|
# Mount vol_a for instance_b. We shouldn't have mounted again
|
|
self._sentinel_mount(m, mock.sentinel.vol_a, instance=instance_b)
|
|
self.mock_mount.assert_not_called()
|
|
|
|
# Unmount vol_a for instance_a. We shouldn't have unmounted
|
|
self._sentinel_umount(m, mock.sentinel.vol_a, instance=instance_a)
|
|
self.mock_umount.assert_not_called()
|
|
|
|
# Unmount vol_a for instance_b. We should have umounted.
|
|
self._sentinel_umount(m, mock.sentinel.vol_a, instance=instance_b)
|
|
self.mock_umount.assert_has_calls(
|
|
[mock.call(mock.sentinel.mountpoint)])
|
|
|
|
def test_mount_concurrent(self):
|
|
# This is 2 tests in 1, because the first test is the precondition
|
|
# for the second.
|
|
|
|
# The first test is that if 2 threads call mount simultaneously,
|
|
# only one of them will do the mount
|
|
|
|
# The second test is that we correctly handle the case where we
|
|
# delete a lock after umount. During the umount of the first test,
|
|
# which will delete the lock when it completes, we start 2 more
|
|
# threads which both call mount. These threads are holding a lock
|
|
# which is about to be deleted. We test that they still don't race,
|
|
# and only one of them calls mount.
|
|
m = self._get_clean_hostmountstate()
|
|
|
|
def mount_a():
|
|
# Mount vol_a from export
|
|
self._sentinel_mount(m, mock.sentinel.vol_a)
|
|
ThreadController.current().waitpoint('mounted')
|
|
self._sentinel_umount(m, mock.sentinel.vol_a)
|
|
|
|
def mount_b():
|
|
# Mount vol_b from export
|
|
self._sentinel_mount(m, mock.sentinel.vol_b)
|
|
self._sentinel_umount(m, mock.sentinel.vol_b)
|
|
|
|
def mount_c():
|
|
self._sentinel_mount(m, mock.sentinel.vol_c)
|
|
|
|
def mount_d():
|
|
self._sentinel_mount(m, mock.sentinel.vol_d)
|
|
|
|
ctl_a = ThreadController(mount_a)
|
|
ctl_b = ThreadController(mount_b)
|
|
ctl_c = ThreadController(mount_c)
|
|
ctl_d = ThreadController(mount_d)
|
|
|
|
def trap_mount(*args, **kwargs):
|
|
# Conditionally wait at a waitpoint named after the command
|
|
# we're executing
|
|
self.mountfixture._fake_mount(*args, **kwargs)
|
|
ThreadController.current().waitpoint('mount')
|
|
|
|
def trap_umount(*args, **kwargs):
|
|
# Conditionally wait at a waitpoint named after the command
|
|
# we're executing
|
|
self.mountfixture._fake_umount(*args, **kwargs)
|
|
ThreadController.current().waitpoint('umount')
|
|
|
|
self.mock_mount.side_effect = trap_mount
|
|
self.mock_umount.side_effect = trap_umount
|
|
|
|
# Run the first thread until it's blocked while calling mount
|
|
ctl_a.runto('mount')
|
|
self.mock_ensure_tree.assert_has_calls([
|
|
mock.call(mock.sentinel.mountpoint)])
|
|
self.mock_mount.assert_has_calls([
|
|
mock.call(mock.sentinel.fstype, mock.sentinel.export,
|
|
mock.sentinel.mountpoint,
|
|
[mock.sentinel.option1, mock.sentinel.option2])])
|
|
|
|
# Start the second mount, and ensure it's got plenty of opportunity
|
|
# to race.
|
|
ctl_b.start()
|
|
time.sleep(0.01)
|
|
self.mock_ensure_tree.assert_has_calls([
|
|
mock.call(mock.sentinel.mountpoint)])
|
|
self.mock_mount.assert_has_calls([
|
|
mock.call(mock.sentinel.fstype, mock.sentinel.export,
|
|
mock.sentinel.mountpoint,
|
|
[mock.sentinel.option1, mock.sentinel.option2])])
|
|
self.mock_umount.assert_not_called()
|
|
|
|
# Allow ctl_a to complete its mount
|
|
ctl_a.runto('mounted')
|
|
self.mock_ensure_tree.assert_has_calls([
|
|
mock.call(mock.sentinel.mountpoint)])
|
|
self.mock_mount.assert_has_calls([
|
|
mock.call(mock.sentinel.fstype, mock.sentinel.export,
|
|
mock.sentinel.mountpoint,
|
|
[mock.sentinel.option1, mock.sentinel.option2])])
|
|
self.mock_umount.assert_not_called()
|
|
|
|
# Allow ctl_b to finish. We should not have done a umount
|
|
ctl_b.finish()
|
|
self.mock_ensure_tree.assert_has_calls([
|
|
mock.call(mock.sentinel.mountpoint)])
|
|
self.mock_mount.assert_has_calls([
|
|
mock.call(mock.sentinel.fstype, mock.sentinel.export,
|
|
mock.sentinel.mountpoint,
|
|
[mock.sentinel.option1, mock.sentinel.option2])])
|
|
self.mock_umount.assert_not_called()
|
|
|
|
# Allow ctl_a to start umounting. We haven't executed rmdir yet,
|
|
# because we've blocked during umount
|
|
ctl_a.runto('umount')
|
|
self.mock_ensure_tree.assert_has_calls([
|
|
mock.call(mock.sentinel.mountpoint)])
|
|
self.mock_mount.assert_has_calls([
|
|
mock.call(mock.sentinel.fstype, mock.sentinel.export,
|
|
mock.sentinel.mountpoint,
|
|
[mock.sentinel.option1, mock.sentinel.option2])])
|
|
self.mock_umount.assert_has_calls(
|
|
[mock.call(mock.sentinel.mountpoint)])
|
|
self.mock_rmdir.assert_not_called()
|
|
|
|
# While ctl_a is umounting, simultaneously start both ctl_c and
|
|
# ctl_d, and ensure they have an opportunity to race
|
|
ctl_c.start()
|
|
ctl_d.start()
|
|
time.sleep(0.01)
|
|
|
|
# Allow a, c, and d to complete
|
|
for ctl in (ctl_a, ctl_c, ctl_d):
|
|
ctl.finish()
|
|
|
|
# We should have completed the previous umount, then remounted
|
|
# exactly once
|
|
self.mock_ensure_tree.assert_has_calls([
|
|
mock.call(mock.sentinel.mountpoint)])
|
|
self.mock_mount.assert_has_calls([
|
|
mock.call(mock.sentinel.fstype, mock.sentinel.export,
|
|
mock.sentinel.mountpoint,
|
|
[mock.sentinel.option1, mock.sentinel.option2]),
|
|
mock.call(mock.sentinel.fstype, mock.sentinel.export,
|
|
mock.sentinel.mountpoint,
|
|
[mock.sentinel.option1, mock.sentinel.option2])])
|
|
self.mock_umount.assert_has_calls(
|
|
[mock.call(mock.sentinel.mountpoint)])
|
|
|
|
def test_mount_concurrent_no_interfere(self):
|
|
# Test that concurrent calls to mount volumes in different exports
|
|
# run concurrently
|
|
m = self._get_clean_hostmountstate()
|
|
|
|
def mount_a():
|
|
# Mount vol on mountpoint a
|
|
self._sentinel_mount(m, mock.sentinel.vol,
|
|
mock.sentinel.mountpoint_a)
|
|
ThreadController.current().waitpoint('mounted')
|
|
self._sentinel_umount(m, mock.sentinel.vol,
|
|
mock.sentinel.mountpoint_a)
|
|
|
|
def mount_b():
|
|
# Mount vol on mountpoint b
|
|
self._sentinel_mount(m, mock.sentinel.vol,
|
|
mock.sentinel.mountpoint_b)
|
|
self._sentinel_umount(m, mock.sentinel.vol,
|
|
mock.sentinel.mountpoint_b)
|
|
|
|
ctl_a = ThreadController(mount_a)
|
|
ctl_b = ThreadController(mount_b)
|
|
|
|
ctl_a.runto('mounted')
|
|
self.mock_mount.assert_has_calls([
|
|
mock.call(mock.sentinel.fstype, mock.sentinel.export,
|
|
mock.sentinel.mountpoint_a,
|
|
[mock.sentinel.option1, mock.sentinel.option2])])
|
|
self.mock_mount.reset_mock()
|
|
|
|
ctl_b.finish()
|
|
self.mock_mount.assert_has_calls([
|
|
mock.call(mock.sentinel.fstype, mock.sentinel.export,
|
|
mock.sentinel.mountpoint_b,
|
|
[mock.sentinel.option1, mock.sentinel.option2])])
|
|
self.mock_umount.assert_has_calls(
|
|
[mock.call(mock.sentinel.mountpoint_b)])
|
|
self.mock_umount.reset_mock()
|
|
|
|
ctl_a.finish()
|
|
self.mock_umount.assert_has_calls(
|
|
[mock.call(mock.sentinel.mountpoint_a)])
|
|
|
|
def test_mount_after_failed_umount(self):
|
|
# Test that MountManager correctly tracks state when umount fails.
|
|
# Test that when umount fails a subsequent mount doesn't try to
|
|
# remount it.
|
|
|
|
m = self._get_clean_hostmountstate()
|
|
self.mock_umount.side_effect = processutils.ProcessExecutionError
|
|
|
|
# Mount vol_a
|
|
self._sentinel_mount(m, mock.sentinel.vol_a)
|
|
self.mock_mount.assert_has_calls([
|
|
mock.call(mock.sentinel.fstype, mock.sentinel.export,
|
|
mock.sentinel.mountpoint,
|
|
[mock.sentinel.option1, mock.sentinel.option2])])
|
|
self.mock_mount.reset_mock()
|
|
|
|
# Umount vol_a. The umount command will fail.
|
|
self._sentinel_umount(m, mock.sentinel.vol_a)
|
|
self.mock_umount.assert_has_calls(
|
|
[mock.call(mock.sentinel.mountpoint)])
|
|
|
|
# We should not have called rmdir, because umount failed
|
|
self.mock_rmdir.assert_not_called()
|
|
|
|
# Mount vol_a again. We should not have called mount, because umount
|
|
# failed.
|
|
self._sentinel_mount(m, mock.sentinel.vol_a)
|
|
self.mock_mount.assert_not_called()
|
|
|
|
# Prevent future failure of umount
|
|
self.mock_umount.side_effect = self.mountfixture._fake_umount
|
|
|
|
# Umount vol_a successfully
|
|
self._sentinel_umount(m, mock.sentinel.vol_a)
|
|
self.mock_umount.assert_has_calls(
|
|
[mock.call(mock.sentinel.mountpoint)])
|
|
|
|
@mock.patch.object(mount.LOG, 'error')
|
|
def test_umount_log_failure(self, mock_log):
|
|
self.mock_umount.side_effect = processutils.ProcessExecutionError(
|
|
None, None, None, 'umount', 'umount: device is busy.')
|
|
|
|
m = self._get_clean_hostmountstate()
|
|
|
|
self._sentinel_mount(m, mock.sentinel.vol_a)
|
|
self._sentinel_umount(m, mock.sentinel.vol_a)
|
|
|
|
mock_log.assert_called()
|
|
|
|
|
|
class MountManagerTestCase(test.NoDBTestCase):
|
|
class FakeHostMountState(object):
|
|
def __init__(self, host, generation):
|
|
self.host = host
|
|
self.generation = generation
|
|
|
|
ctl = ThreadController.current()
|
|
if ctl is not None:
|
|
ctl.waitpoint('init')
|
|
|
|
def setUp(self):
|
|
super(MountManagerTestCase, self).setUp()
|
|
|
|
self.useFixture(fixtures.MonkeyPatch(
|
|
'nova.virt.libvirt.volume.mount._HostMountState',
|
|
self.FakeHostMountState))
|
|
|
|
self.m = mount.get_manager()
|
|
self.m._reset_state()
|
|
|
|
def _get_state(self):
|
|
with self.m.get_state() as state:
|
|
return state
|
|
|
|
def test_host_up_down(self):
|
|
self.m.host_up(mock.sentinel.host)
|
|
state = self._get_state()
|
|
self.assertEqual(state.host, mock.sentinel.host)
|
|
self.assertEqual(state.generation, 0)
|
|
|
|
self.m.host_down()
|
|
self.assertRaises(exception.HypervisorUnavailable, self._get_state)
|
|
|
|
def test_host_up_waits_for_completion(self):
|
|
self.m.host_up(mock.sentinel.host)
|
|
|
|
def txn():
|
|
with self.m.get_state():
|
|
ThreadController.current().waitpoint('running')
|
|
|
|
# Start a thread which blocks holding a state object
|
|
ctl = ThreadController(txn)
|
|
ctl.runto('running')
|
|
|
|
# Host goes down
|
|
self.m.host_down()
|
|
|
|
# Call host_up in a separate thread because it will block, and give
|
|
# it plenty of time to race
|
|
host_up = eventlet.greenthread.spawn(self.m.host_up,
|
|
mock.sentinel.host)
|
|
time.sleep(0.01)
|
|
|
|
# Assert that we haven't instantiated a new state while there's an
|
|
# ongoing operation from the previous state
|
|
self.assertRaises(exception.HypervisorUnavailable, self._get_state)
|
|
|
|
# Allow the previous ongoing operation and host_up to complete
|
|
ctl.finish()
|
|
host_up.wait()
|
|
|
|
# Assert that we've got a new state generation
|
|
state = self._get_state()
|
|
self.assertEqual(1, state.generation)
|