Merge branch 'bug3164'

* bug3164:
  tests: add regression test for #3164
  storage/lvm: make sure volume cache is refreshed after changes
  storage/lvm: fix Volume.verify()
  storage/lvm: remove old volume only after successfully cloning new one
This commit is contained in:
Marek Marczykowski-Górecki 2017-10-16 04:28:26 +02:00
commit 836d9f902a
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
3 changed files with 98 additions and 22 deletions

View File

@ -275,6 +275,9 @@ class Volume(object):
def verify(self):
''' Verifies the volume.
This function is supposed to either return :py:obj:`True`, or raise
an exception.
This can be implemented as a coroutine.'''
raise self._not_implemented("verify")

View File

@ -270,10 +270,22 @@ class ThinVolume(qubes.storage.Volume):
qubes_lvm(cmd, self.log)
self._remove_revisions()
cmd = ['remove', self.vid]
qubes_lvm(cmd, self.log)
cmd = ['clone', self._vid_snap, self.vid]
# TODO: when converting this function to coroutine, this _must_ be
# under a lock
# remove old volume only after _successful_ clone of the new one
cmd = ['rename', self.vid, self.vid + '-tmp']
qubes_lvm(cmd, self.log)
try:
cmd = ['clone', self._vid_snap, self.vid]
qubes_lvm(cmd, self.log)
except:
# restore original volume
cmd = ['rename', self.vid + '-tmp', self.vid]
qubes_lvm(cmd, self.log)
raise
else:
cmd = ['remove', self.vid + '-tmp']
qubes_lvm(cmd, self.log)
def create(self):
@ -419,34 +431,47 @@ class ThinVolume(qubes.storage.Volume):
def start(self):
if self.snap_on_start or self.save_on_stop:
if not self.save_on_stop or not self.is_dirty():
self._snapshot()
else:
self._reset()
reset_cache()
try:
if self.snap_on_start or self.save_on_stop:
if not self.save_on_stop or not self.is_dirty():
self._snapshot()
else:
self._reset()
finally:
reset_cache()
return self
def stop(self):
if self.save_on_stop:
self._commit()
if self.snap_on_start or self.save_on_stop:
cmd = ['remove', self._vid_snap]
qubes_lvm(cmd, self.log)
else:
cmd = ['remove', self.vid]
qubes_lvm(cmd, self.log)
reset_cache()
try:
if self.save_on_stop:
self._commit()
if self.snap_on_start or self.save_on_stop:
cmd = ['remove', self._vid_snap]
qubes_lvm(cmd, self.log)
else:
cmd = ['remove', self.vid]
qubes_lvm(cmd, self.log)
finally:
reset_cache()
return self
def verify(self):
''' Verifies the volume. '''
if not self.save_on_stop and not self.snap_on_start:
# volatile volumes don't need any files
return True
if self.source is not None:
vid = str(self.source)
else:
vid = self.vid
try:
vol_info = size_cache[self.vid]
return vol_info['attr'][4] == 'a'
vol_info = size_cache[vid]
if vol_info['attr'][4] != 'a':
raise qubes.storage.StoragePoolException(
'volume {} not active'.format(vid))
except KeyError:
return False
raise qubes.storage.StoragePoolException(
'volume {} missing'.format(vid))
def block_device(self):
@ -493,6 +518,8 @@ def qubes_lvm(cmd, log=logging.getLogger('qubes.storage.lvm')):
lvm_cmd = ["lvextend", "-L%s" % size, cmd[1]]
elif action == 'activate':
lvm_cmd = ['lvchange', '-ay', cmd[1]]
elif action == 'rename':
lvm_cmd = ['lvrename', cmd[1], cmd[2]]
else:
raise NotImplementedError('unsupported action: ' + action)
if lvm_is_very_old:

View File

@ -30,9 +30,12 @@ import tempfile
import time
import unittest
import collections
import qubes
import qubes.firewall
import qubes.tests
import qubes.storage
import qubes.vm.appvm
import qubes.vm.qubesvm
import qubes.vm.standalonevm
@ -79,6 +82,49 @@ class TC_00_Basic(qubes.tests.SystemTestCase):
self.loop.run_until_complete(asyncio.sleep(0.1))
self.assertTrue(flag)
def _test_200_on_domain_start(self, vm, event, **_kwargs):
'''Simulate domain crash just after startup'''
vm.libvirt_domain.destroy()
def test_200_shutdown_event_race(self):
'''Regression test for 3164'''
vmname = self.make_vm_name('appvm')
self.vm = self.app.add_new_vm(qubes.vm.appvm.AppVM,
name=vmname, template=self.app.default_template,
label='red')
# help the luck a little - don't wait for qrexec to easier win the race
self.vm.features['qrexec'] = False
self.loop.run_until_complete(self.vm.create_on_disk())
# another way to help the luck a little - make sure the private
# volume is first in (normally unordered) dict - this way if any
# volume action fails, it will be at or after private volume - not
# before (preventing private volume action)
old_volumes = self.vm.volumes
self.vm.volumes = collections.OrderedDict()
self.vm.volumes['private'] = old_volumes.pop('private')
self.vm.volumes.update(old_volumes.items())
del old_volumes
self.loop.run_until_complete(self.vm.start())
# kill it the way it does not give a chance for domain-shutdown it
# execute
self.vm.libvirt_domain.destroy()
# now, lets try to start the VM again, before domain-shutdown event
# got handled (#3164), and immediately trigger second domain-shutdown
self.vm.add_handler('domain-start', self._test_200_on_domain_start)
self.loop.run_until_complete(self.vm.start())
# and give a chance for both domain-shutdown handlers to execute
self.loop.run_until_complete(asyncio.sleep(1))
with self.assertNotRaises(qubes.exc.QubesException):
# if the above caused two domain-shutdown handlers being called
# one after another, private volume is gone
self.loop.run_until_complete(self.vm.storage.verify())
class TC_01_Properties(qubes.tests.SystemTestCase):
# pylint: disable=attribute-defined-outside-init
def setUp(self):