backup/restore: add option for unattended restore and extracting log
Allow running unattended, with qvm-backup-restore --passphrase-file. This require few modifications: - copy the passphrase file into the DisposableVM (that VM knows the passphrase anyway, so there is no extra data leak) - close the terminal when operation finishes Closing the terminal would eliminate almost all the feedback (operation log, errors, warnings etc), so write it into a file in DisposableVM and later extract it and show on the stdout. Similar to qvm-run, color it red as a content coming from a VM. QubesOS/qubes-issues#5310
This commit is contained in:
parent
e9120e3196
commit
7d6cb655f8
@ -101,6 +101,13 @@ Options
|
||||
- dom0 home directory (desktop environment settings)
|
||||
- PCI devices assignments
|
||||
|
||||
.. option:: --auto-close
|
||||
|
||||
When running with --paranoid-mode (see above), automatically close restore
|
||||
progress window after the restore process is finished and display restore log
|
||||
on the standard output. The log will be colored red if the standard output is
|
||||
a terminal.
|
||||
|
||||
Authors
|
||||
=======
|
||||
| Joanna Rutkowska <joanna at invisiblethingslab dot com>
|
||||
|
@ -19,8 +19,11 @@
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
"""Handle backup extraction using DisposableVM"""
|
||||
import collections
|
||||
import datetime
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import string
|
||||
|
||||
import subprocess
|
||||
@ -74,14 +77,6 @@ def skip(_option, _value):
|
||||
return []
|
||||
|
||||
|
||||
def handle_unsupported(option, value):
|
||||
"""Reject argument as unsupported"""
|
||||
if value:
|
||||
raise NotImplementedError(
|
||||
'{} option is not supported with --paranoid-mode'.format(
|
||||
option.opts[0]))
|
||||
return []
|
||||
|
||||
class RestoreInDisposableVM:
|
||||
"""Perform backup restore with actual archive extraction isolated
|
||||
within DisposableVM"""
|
||||
@ -105,10 +100,11 @@ class RestoreInDisposableVM:
|
||||
handle_store_true),
|
||||
'compression': Option(('--compression-filter', '-Z'), handle_store),
|
||||
'appvm': Option(('--dest-vm', '-d'), handle_store),
|
||||
'pass_file': Option(('--passphrase-file', '-p'), handle_unsupported),
|
||||
'pass_file': Option(('--passphrase-file', '-p'), handle_store),
|
||||
'location_is_service': Option(('--location-is-service',),
|
||||
handle_store_true),
|
||||
'paranoid_mode': Option(('--paranoid-mode', '--plan-b',), skip),
|
||||
'auto_close': Option(('--auto-close',), skip),
|
||||
# make the verification easier, those don't really matter
|
||||
'help': Option(('--help', '-h'), skip),
|
||||
'force_root': Option(('--force-root',), skip),
|
||||
@ -134,7 +130,17 @@ class RestoreInDisposableVM:
|
||||
#: tag added to a VM storing the backup archive
|
||||
self.storage_tag = 'backup-restore-storage'
|
||||
|
||||
self.terminal_app = ('xterm', '-hold', '-title', 'Backup restore', '-e')
|
||||
# FIXME: make it random, collision free
|
||||
# (when considering non-disposable case)
|
||||
self.backup_log_path = '/var/tmp/backup-restore.log'
|
||||
self.terminal_app = ('xterm', '-hold', '-title', 'Backup restore', '-e',
|
||||
'/bin/sh', '-c',
|
||||
'exec "$0" "$@" 2>&1 | tee {}'.format(
|
||||
self.backup_log_path))
|
||||
if args.auto_close:
|
||||
# filter-out '-hold'
|
||||
self.terminal_app = tuple(a for a in self.terminal_app
|
||||
if a != '-hold')
|
||||
|
||||
self.dispvm = None
|
||||
|
||||
@ -161,6 +167,18 @@ class RestoreInDisposableVM:
|
||||
self.dispvm.auto_cleanup = True
|
||||
self.dispvm.features['tag-created-vm-with'] = self.restored_tag
|
||||
|
||||
def transfer_pass_file(self, path):
|
||||
"""Copy passhprase file to the DisposableVM"""
|
||||
subprocess.check_call(
|
||||
['qvm-copy-to-vm', self.dispvm_name, path],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
return '/home/{}/QubesIncoming/{}/{}'.format(
|
||||
self.dispvm.default_user,
|
||||
os.uname()[1],
|
||||
os.path.basename(path)
|
||||
)
|
||||
|
||||
def register_backup_source(self):
|
||||
"""Tell backup archive holding VM we want this content.
|
||||
|
||||
@ -235,6 +253,23 @@ class RestoreInDisposableVM:
|
||||
datetime.date.strftime(datetime.date.today(), '%F')))
|
||||
domain.tags.discard('backup-restore-in-progress')
|
||||
|
||||
@staticmethod
|
||||
def sanitize_log(untrusted_log):
|
||||
"""Replace characters potentially dangerouns to terminal in
|
||||
a backup log"""
|
||||
allowed_set = set(range(0x20, 0x7e))
|
||||
allowed_set.update({0x0a})
|
||||
return bytes(c if c in allowed_set else ord('.') for c in untrusted_log)
|
||||
|
||||
def extract_log(self):
|
||||
"""Extract restore log from the DisposableVM"""
|
||||
untrusted_backup_log, _ = self.dispvm.run_with_args(
|
||||
'cat', self.backup_log_path,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL)
|
||||
backup_log = self.sanitize_log(untrusted_backup_log)
|
||||
return backup_log
|
||||
|
||||
def run(self):
|
||||
"""Run the backup restore operation"""
|
||||
lock = qubesadmin.utils.LockFile(LOCKFILE, True)
|
||||
@ -243,14 +278,19 @@ class RestoreInDisposableVM:
|
||||
self.create_dispvm()
|
||||
self.clear_old_tags()
|
||||
self.register_backup_source()
|
||||
args = self.prepare_inner_args()
|
||||
self.dispvm.start()
|
||||
self.dispvm.run_service_for_stdio('qubes.WaitForSession')
|
||||
if self.args.pass_file:
|
||||
self.args.pass_file = self.transfer_pass_file(
|
||||
self.args.pass_file)
|
||||
args = self.prepare_inner_args()
|
||||
self.dispvm.tags.add(self.dispvm_tag)
|
||||
# TODO: better error detection
|
||||
self.dispvm.run_with_args(*self.terminal_app,
|
||||
'qvm-backup-restore', *args,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
return self.extract_log()
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.returncode == 127:
|
||||
raise qubesadmin.exc.QubesException(
|
||||
@ -259,8 +299,13 @@ class RestoreInDisposableVM:
|
||||
'package there'.format(self.terminal_app[0],
|
||||
self.dispvm.template.template.name)
|
||||
)
|
||||
raise qubesadmin.exc.QubesException(
|
||||
'qvm-backup-restore failed with {}'.format(e.returncode))
|
||||
try:
|
||||
backup_log = self.extract_log()
|
||||
except: # pylint: disable=bare-except
|
||||
backup_log = None
|
||||
raise qubesadmin.exc.BackupRestoreError(
|
||||
'qvm-backup-restore failed with {}'.format(e.returncode),
|
||||
backup_log=backup_log)
|
||||
finally:
|
||||
if self.dispvm is not None:
|
||||
# first revoke permission, then cleanup
|
||||
|
@ -154,6 +154,12 @@ class DeviceAlreadyAttached(QubesException, KeyError):
|
||||
return QubesException.__str__(self)
|
||||
|
||||
|
||||
class BackupRestoreError(QubesException):
|
||||
'''Restoring a backup failed'''
|
||||
def __init__(self, msg, backup_log=None):
|
||||
super(BackupRestoreError, self).__init__(msg)
|
||||
self.backup_log = backup_log
|
||||
|
||||
# pylint: disable=too-many-ancestors
|
||||
class QubesDaemonNoResponseError(QubesDaemonCommunicationError):
|
||||
'''Got empty response from qubesd'''
|
||||
|
@ -21,6 +21,7 @@
|
||||
'''Console frontend for backup restore code'''
|
||||
|
||||
import getpass
|
||||
import os
|
||||
import sys
|
||||
|
||||
from qubesadmin.backup.restore import BackupRestore
|
||||
@ -89,6 +90,10 @@ parser.add_argument("-p", "--passphrase-file", action="store",
|
||||
dest="pass_file", default=None,
|
||||
help="Read passphrase from file, or use '-' to read from stdin")
|
||||
|
||||
parser.add_argument('--auto-close', action="store_true",
|
||||
help="Auto-close restore window and display log on the stdout "
|
||||
"(applies to --paranoid-mode)")
|
||||
|
||||
parser.add_argument("--location-is-service", action="store_true",
|
||||
help="Interpret backup location as a qrexec service name,"
|
||||
"possibly with an argument separated by +.Requires -d option.")
|
||||
@ -206,6 +211,18 @@ def handle_broken(app, args, restore_info):
|
||||
"files should be copied or moved out of the new "
|
||||
"directory before using them.")
|
||||
|
||||
|
||||
def print_backup_log(backup_log):
|
||||
"""Print a log on stdout, coloring it red if it's a terminal"""
|
||||
if os.isatty(sys.stdout.fileno()):
|
||||
sys.stdout.write('\033[0;31m')
|
||||
sys.stdout.flush()
|
||||
sys.stdout.buffer.write(backup_log)
|
||||
if os.isatty(sys.stdout.fileno()):
|
||||
sys.stdout.write('\033[0m')
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def main(args=None, app=None):
|
||||
'''Main function of qvm-backup-restore'''
|
||||
# pylint: disable=too-many-return-statements
|
||||
@ -228,7 +245,14 @@ def main(args=None, app=None):
|
||||
"manually.")
|
||||
restore_in_dispvm = RestoreInDisposableVM(args.app, args)
|
||||
try:
|
||||
restore_in_dispvm.run()
|
||||
backup_log = restore_in_dispvm.run()
|
||||
if args.auto_close:
|
||||
print_backup_log(backup_log)
|
||||
except qubesadmin.exc.BackupRestoreError as e:
|
||||
if e.backup_log is not None:
|
||||
print_backup_log(e.backup_log)
|
||||
parser.error_runtime(str(e))
|
||||
return 1
|
||||
except qubesadmin.exc.QubesException as e:
|
||||
parser.error_runtime(str(e))
|
||||
return 1
|
||||
|
Loading…
Reference in New Issue
Block a user