From 20590bff570939b87c76de21fd2f2d324461a7ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Thu, 29 Sep 2016 01:55:34 +0200 Subject: [PATCH] backup: adjust LVM volume size when restoring its content. Old backup metadata (old qubes.xml) does not contain info about individual volume sizes. So, extract it from tar header (using verbose output during restore) and resize volume accordingly. Without this, restoring volumes larger than default would be impossible. --- qubes/backup.py | 67 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/qubes/backup.py b/qubes/backup.py index 8bbb05e0..119e08a8 100644 --- a/qubes/backup.py +++ b/qubes/backup.py @@ -26,6 +26,7 @@ import itertools import logging from qubes.utils import size_to_human import sys +import stat import os import fcntl import subprocess @@ -931,9 +932,33 @@ class ExtractWorker2(Process): debug_msg = filter(msg_re.match, new_lines) self.log.debug('tar2_stderr: {}'.format('\n'.join(debug_msg))) new_lines = filter(lambda x: not msg_re.match(x), new_lines) - + if self.adjust_output_size: + # search for first file size reported by tar, after setting + # self.adjust_output_size (so don't look at self.tar2_stderr) + # this is used only when extracting single-file archive, so don't + # bother with checking file name + file_size_re = re.compile(r"^[^ ]+ [^ ]+/[^ ]+ *([0-9]+) .*") + for line in new_lines: + match = file_size_re.match(line) + if match: + file_size = match.groups()[0] + self.resize_lvm(self.adjust_output_size, file_size) + self.adjust_output_size = None self.tar2_stderr += new_lines + def resize_lvm(self, dev, size): + # FIXME: HACK + try: + subprocess.check_call( + ['sudo', 'lvresize', '-f', '-L', str(size) + 'B', dev], + stdout=open(os.devnull, 'w'), stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + if e.returncode == 3: + # already at the right size + pass + else: + raise + def run(self): try: self.__run__() @@ -966,6 +991,15 @@ class ExtractWorker2(Process): # is extracted there if dirname in self.relocate: old = old.replace(dirname, self.relocate[dirname], 1) + try: + stat_buf = os.stat(new) + if stat.S_ISBLK(stat_buf.st_mode): + # output file is block device (LVM) - adjust its + # size, otherwise it may fail + # from lack of space + self.resize_lvm(new, stat_buf.st_size) + except OSError: # ENOENT + pass subprocess.check_call( ['dd', 'if='+old, 'of='+new, 'conv=sparse']) os.unlink(old) @@ -1005,16 +1039,26 @@ class ExtractWorker2(Process): self.handle_dir_relocations( os.path.dirname(inner_name)) self.tar2_current_file = None + self.adjust_output_size = None inner_name = os.path.relpath(filename.rstrip('.000')) redirect_stdout = None if self.relocate and inner_name in self.relocate: # TODO: add `dd conv=sparse` when removing tar layer tar2_cmdline = ['tar', - '-%sMOf' % ("t" if self.verify_only else "x"), + '-%sMvvOf' % ("t" if self.verify_only else "x"), self.restore_pipe, inner_name] output_file = self.relocate[inner_name] + try: + stat_buf = os.stat(output_file) + if stat.S_ISBLK(stat_buf.st_mode): + # output file is block device (LVM) - adjust its + # size during extraction, otherwise it may fail + # from lack of space + self.adjust_output_size = output_file + except OSError: # ENOENT + pass redirect_stdout = open(output_file, 'w') elif self.relocate and \ os.path.dirname(inner_name) in self.relocate: @@ -1112,6 +1156,7 @@ class ExtractWorker2(Process): self.tar2_process.terminate() self.tar2_process.wait() self.tar2_process = None + self.adjust_output_size = None self.log.error("Error while processing '{}': {}".format( self.tar2_current_file, details)) @@ -1136,6 +1181,7 @@ class ExtractWorker2(Process): "\n".join(self.tar2_stderr)))) else: # Finished extracting the tar file + self.collect_tar_output() self.tar2_process = None # if that was whole-directory archive, handle # relocated files now @@ -1144,6 +1190,7 @@ class ExtractWorker2(Process): if os.path.basename(inner_name) == '.': self.handle_dir_relocations( os.path.dirname(inner_name)) + self.adjust_output_size = None self.log.debug("Finished extracting thread") @@ -1188,6 +1235,7 @@ class ExtractWorker3(ExtractWorker2): "\n ".join(self.tar2_stderr))) else: # Finished extracting the tar file + self.collect_tar_output() self.tar2_process = None # if that was whole-directory archive, handle # relocated files now @@ -1197,15 +1245,25 @@ class ExtractWorker3(ExtractWorker2): self.handle_dir_relocations( os.path.dirname(inner_name)) self.tar2_current_file = None + self.adjust_output_size = None inner_name = os.path.relpath(filename.rstrip('.000')) redirect_stdout = None if self.relocate and inner_name in self.relocate: # TODO: add dd conv=sparse when removing tar layer tar2_cmdline = ['tar', - '-%sO' % ("t" if self.verify_only else "x"), + '-%svvO' % ("t" if self.verify_only else "x"), inner_name] output_file = self.relocate[inner_name] + try: + stat_buf = os.stat(output_file) + if stat.S_ISBLK(stat_buf.st_mode): + # output file is block device (LVM) - adjust its + # size during extraction, otherwise it may fail + # from lack of space + self.adjust_output_size = output_file + except OSError: # ENOENT + pass redirect_stdout = open(output_file, 'w') elif self.relocate and \ os.path.dirname(inner_name) in self.relocate: @@ -1293,6 +1351,7 @@ class ExtractWorker3(ExtractWorker2): self.tar2_process.terminate() self.tar2_process.wait() self.tar2_process = None + self.adjust_output_size = None self.log.error("Error while processing '{}': {}".format( self.tar2_current_file, details)) @@ -1320,6 +1379,7 @@ class ExtractWorker3(ExtractWorker2): "\n".join(self.tar2_stderr)))) else: # Finished extracting the tar file + self.collect_tar_output() self.tar2_process = None # if that was whole-directory archive, handle # relocated files now @@ -1328,6 +1388,7 @@ class ExtractWorker3(ExtractWorker2): if os.path.basename(inner_name) == '.': self.handle_dir_relocations( os.path.dirname(inner_name)) + self.adjust_output_size = None self.log.debug("Finished extracting thread")