Skip to content

Commit b5dd800

Browse files
committed
lvm2 pool: Export immutable snapshot of volume
Volumes returned by `export()` must be immutable, since otherwise the backup will be inconsistent. Ensure this by exporting a snapshot of the volume, no the volume itself. Fixes QubesOS/qubes-issues#7198.
1 parent 4bfd808 commit b5dd800

1 file changed

Lines changed: 28 additions & 6 deletions

File tree

qubes/storage/lvm.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ def list_volumes(self):
156156
continue
157157
if vol_info['pool_lv'] != self.thin_pool:
158158
continue
159-
if vid.endswith('-snap') or vid.endswith('-import'):
159+
if vid.endswith('-snap') or vid.endswith('-import') or \
160+
vid.endswith('+export'):
160161
# implementation detail volume
161162
continue
162163
if vid.endswith('-back'):
@@ -292,6 +293,7 @@ def __init__(self, volume_group, **kwargs):
292293
self._vid_snap = self.vid + '-snap'
293294
if self.save_on_stop:
294295
self._vid_import = self.vid + '-import'
296+
self._is_exporting = False
295297

296298
@property
297299
def path(self):
@@ -470,13 +472,31 @@ async def remove(self):
470472
# pylint: disable=protected-access
471473
self.pool._volume_objects_cache.pop(self.vid, None)
472474

473-
async def export(self):
475+
@property
476+
def _vid_export(self) -> str:
477+
return self.vid + '+export'
478+
479+
@property
480+
def _path_export(self) -> str:
481+
return '/dev/' + self._vid_export
482+
483+
async def export(self) -> str:
474484
''' Returns an object that can be `open()`. '''
475485
# make sure the device node is available
476-
cmd = ['activate', self.path]
477-
await qubes_lvm_coro(cmd, self.log)
478-
devpath = self.path
479-
return devpath
486+
vid_export = self._vid_export
487+
export_path = self._path_export
488+
if not os.path.exists(export_path):
489+
cmd = ['clone', self._vid_current, vid_export]
490+
await qubes_lvm_coro(cmd, self.log)
491+
self._is_exporting = True
492+
return export_path
493+
494+
async def export_end(self, path: str) -> None:
495+
assert path == self._path_export, \
496+
f'Refusing to remove incorrect path {path!r} ' \
497+
f'(expected {self._path_export!r})'
498+
await self._remove_if_exists(self._vid_export)
499+
self._is_exporting = False
480500

481501
@qubes.storage.Volume.locked
482502
async def import_volume(self, src_volume):
@@ -681,6 +701,8 @@ async def stop(self):
681701
changed = await self._remove_if_exists(self._vid_snap)
682702
else:
683703
changed = await self._remove_if_exists(self.vid)
704+
if not self._is_exporting:
705+
await self._remove_if_exists(self._vid_export)
684706
finally:
685707
if changed:
686708
await reset_cache_coro()

0 commit comments

Comments
 (0)