storage/reflink: strictly increasing revision ID

Don't rely on timestamps to sort revisions - the clock can go backwards
due to time sync. Instead, use a monotonically increasing natural number
as the revision ID.

Old revision example: private.img@2018-01-02-03T04:05:06Z (ignored now)
New revision example: private.img.123@2018-01-02-03T04:05:06Z
This commit is contained in:
Rusty Bird 2018-03-21 16:00:13 +00:00
parent b66a2e9a51
commit 6a303760e9
No known key found for this signature in database
GPG Key ID: 469D78F47AAF2ADF

View File

@ -181,25 +181,25 @@ class ReflinkVolume(qubes.storage.Volume):
if _get_file_disk_usage(self._path_clean) == 0: if _get_file_disk_usage(self._path_clean) == 0:
return return
ctime = os.path.getctime(self._path_clean) ctime = os.path.getctime(self._path_clean)
revision = qubes.storage.isodate(int(ctime)) + 'Z' timestamp = qubes.storage.isodate(int(ctime))
_copy_file(self._path_clean, self._path_revision(revision)) _copy_file(self._path_clean,
self._path_revision(self._next_revision_number, timestamp))
def _prune_revisions(self, keep=None): def _prune_revisions(self, keep=None):
if keep is None: if keep is None:
keep = self.revisions_to_keep keep = self.revisions_to_keep
# pylint: disable=invalid-unary-operand-type # pylint: disable=invalid-unary-operand-type
for revision in list(self.revisions.keys())[:(-keep) or None]: for number, timestamp in list(self.revisions.items())[:-keep or None]:
_remove_file(self._path_revision(revision)) _remove_file(self._path_revision(number, timestamp))
def revert(self, revision=None): def revert(self, revision=None):
if revision is None: if revision is None:
revision = list(self.revisions.keys())[-1] number, timestamp = list(self.revisions.items())[-1]
elif not os.path.exists(self._path_revision(revision)): else:
raise qubes.storage.StoragePoolException( number, timestamp = revision, None
'Missing revision {!r} for volume {!s}'.format( path_revision = self._path_revision(number, timestamp)
revision, self.vid))
self._add_revision() self._add_revision()
_rename_file(self._path_revision(revision), self._path_clean) _rename_file(path_revision, self._path_clean)
return self return self
def resize(self, size): def resize(self, size):
@ -263,8 +263,10 @@ class ReflinkVolume(qubes.storage.Volume):
self.import_data_end(True) self.import_data_end(True)
return self return self
def _path_revision(self, revision): def _path_revision(self, number, timestamp=None):
return self._path_clean + '@' + revision if timestamp is None:
timestamp = self.revisions[number]
return self._path_clean + '.' + number + '@' + timestamp + 'Z'
@property @property
def _path_clean(self): def _path_clean(self):
@ -278,15 +280,20 @@ class ReflinkVolume(qubes.storage.Volume):
def path(self): def path(self):
return self._path_dirty return self._path_dirty
@property
def _next_revision_number(self):
numbers = self.revisions.keys()
if numbers:
return str(int(list(numbers)[-1]) + 1)
return '1'
@property @property
def revisions(self): def revisions(self):
revision_to_timestamp = collections.OrderedDict() prefix = self._path_clean + '.'
prefix = self._path_revision('') paths = glob.glob(glob.escape(prefix) + '*@*Z')
for filename in sorted(glob.glob(glob.escape(prefix) + '*Z')): items = sorted((path[len(prefix):-1].split('@') for path in paths),
revision = filename[len(prefix):] key=lambda item: int(item[0]))
timestamp = revision[:-1] return collections.OrderedDict(items)
revision_to_timestamp[revision] = timestamp
return revision_to_timestamp
@property @property
def usage(self): def usage(self):