Skip to content

Commit

Permalink
storage/lvm: allow starting a VM even if not all its volumes are active
Browse files Browse the repository at this point in the history
Normally, VM gets snapshots of its volumes anyway, so it makes little
sense to enforce they are already active. This requires several changes:
1. Including inactive volumes in the size_cache (those will have 0 as
   usage, because inactive do not report usage).
2. Do not check if volume is active in the verify() function.
3. With the above changed, adjust checking for volume existence by
   looking at size_cache instead of /dev/ node existence. For now do
   this in code paths related to VM startup.
4. In the rare case of not using snapshot (or new volatile volume) do
   activate the volume if needed.
5. Refresh size_cache after changing volumes.

Do not deactivate volumes on stop to not lose information about usage.

All that should solve an issue when qubesd (and possibly some VMs) are
started while LVM is still activating volumes. A simpler (and more
reliable) solution would be to order qubesd.service after activating all
(present at boot) volumes, but unfortunately current LVM + systemd
integration doesn't provide anything to set such ordering.
  • Loading branch information
marmarek committed Nov 4, 2024
1 parent 6935f26 commit 6dffbf9
Showing 1 changed file with 29 additions and 11 deletions.
40 changes: 29 additions & 11 deletions qubes/storage/lvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,14 +221,17 @@ def _parse_lvm_cache(lvm_output):
size, usage_percent = row['lv_size'], row['data_percent']
except KeyError:
continue
if '' in [pool_name, name, size, usage_percent]:
if '' in [pool_name, name, size]:
continue
pool_lv, attr, origin = row['pool_lv'], row['lv_attr'], row['origin']
metadata_size = row['lv_metadata_size']
metadata_percent = row['metadata_percent']
name = pool_name + "/" + name
size = int(size[:-1]) # Remove 'B' suffix
usage = int(size / 100 * float(usage_percent))
if usage_percent:
usage = int(size / 100 * float(usage_percent))
else:
usage = 0

Check warning on line 234 in qubes/storage/lvm.py

View check run for this annotation

Codecov / codecov/patch

qubes/storage/lvm.py#L234

Added line #L234 was not covered by tests
if metadata_size:
metadata_size = int(metadata_size[:-1])
metadata_usage = int(metadata_size / 100 * float(metadata_percent))
Expand Down Expand Up @@ -384,6 +387,18 @@ async def _remove_revisions(self, revisions=None):
except qubes.storage.StoragePoolException:
pass

async def _activate(self):
"""
Ensure the volume is active.
:return: None
"""
vol_info = size_cache[self._vid_current]
if vol_info['attr'][4] == 'a':
return
cmd = ["activate", self._vid_current]
await qubes_lvm_coro(cmd, self.log)

Check warning on line 400 in qubes/storage/lvm.py

View check run for this annotation

Codecov / codecov/patch

qubes/storage/lvm.py#L399-L400

Added lines #L399 - L400 were not covered by tests

async def _commit(self, vid_to_commit=None, keep=False):
'''
Commit temporary volume into current one. By default
Expand All @@ -407,7 +422,7 @@ async def _commit(self, vid_to_commit=None, keep=False):
vid_to_commit = self._vid_snap

assert self._lock.locked()
if not os.path.exists('/dev/' + vid_to_commit):
if vid_to_commit not in size_cache:
# nothing to commit
return False

Expand Down Expand Up @@ -453,7 +468,7 @@ async def create(self):
async def remove(self):
assert self.vid
try:
if os.path.exists('/dev/' + self._vid_snap):
if self._vid_snap in size_cache:
cmd = ['remove', self._vid_snap]
await qubes_lvm_coro(cmd, self.log)
except AttributeError:
Expand Down Expand Up @@ -499,12 +514,14 @@ async def import_volume(self, src_volume):
src_volume.pool.thin_pool == self.pool.thin_pool: # NOQA
# pylint: disable=protected-access
await self._commit(src_volume._vid_current, keep=True)
await reset_cache_coro()
else:
cmd = ['create',
self.pool._pool_id, # pylint: disable=protected-access
self._vid_import.split('/')[1],
str(src_volume.size)]
await qubes_lvm_coro(cmd, self.log)
await reset_cache_coro()
src_path = await qubes.utils.coro_maybe(src_volume.export())
try:
cmd = [_dd, 'if=' + src_path, 'of=/dev/' + self._vid_import,
Expand All @@ -525,6 +542,7 @@ async def import_volume(self, src_volume):
'Failed to import volume {!r}, dd exit code: {}'.format(
src_volume, p.returncode))
await self._commit(self._vid_import)
await reset_cache_coro()

return self

Expand Down Expand Up @@ -552,14 +570,15 @@ async def import_data_end(self, success):
'No import operation in progress on {}'.format(self.vid))
if success:
await self._commit(self._vid_import)
await reset_cache_coro()
else:
cmd = ['remove', self._vid_import]
await qubes_lvm_coro(cmd, self.log)
await reset_cache_coro()

def abort_if_import_in_progress(self):
try:
devpath = '/dev/' + self._vid_import
if os.path.exists(devpath):
if self._vid_import in size_cache:
raise qubes.storage.StoragePoolException(
'Import operation in progress on {}'.format(self.vid))
except AttributeError: # self._vid_import
Expand All @@ -568,7 +587,7 @@ def abort_if_import_in_progress(self):

def is_dirty(self):
if self.save_on_stop:
return os.path.exists('/dev/' + self._vid_snap)
return self._vid_snap in size_cache
return False

def is_outdated(self):
Expand Down Expand Up @@ -673,6 +692,8 @@ async def start(self):
if self.snap_on_start or self.save_on_stop:
if not self.save_on_stop or not self.is_dirty():
await self._snapshot()
else:
await self._activate()
else:
await self._reset()
finally:
Expand Down Expand Up @@ -706,10 +727,7 @@ async def verify(self):
else:
vid = self._vid_current
try:
vol_info = size_cache[vid]
if vol_info['attr'][4] != 'a':
raise qubes.storage.StoragePoolException(
'volume {} not active'.format(vid))
size_cache[vid]
except KeyError:
raise qubes.storage.StoragePoolException(
'volume {} missing'.format(vid))
Expand Down

0 comments on commit 6dffbf9

Please sign in to comment.