From 6dffbf9363a5539d89fccdc007ef7fd199ccb5c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Mon, 4 Nov 2024 04:22:10 +0100 Subject: [PATCH] storage/lvm: allow starting a VM even if not all its volumes are active 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. --- qubes/storage/lvm.py | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/qubes/storage/lvm.py b/qubes/storage/lvm.py index ccb1aba6d..47ff15657 100644 --- a/qubes/storage/lvm.py +++ b/qubes/storage/lvm.py @@ -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 if metadata_size: metadata_size = int(metadata_size[:-1]) metadata_usage = int(metadata_size / 100 * float(metadata_percent)) @@ -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) + async def _commit(self, vid_to_commit=None, keep=False): ''' Commit temporary volume into current one. By default @@ -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 @@ -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: @@ -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, @@ -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 @@ -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 @@ -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): @@ -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: @@ -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))