tools: add qvm-backup tool
New qvm-backup tool can either use pre-existing backup profile (--profile), or - when running in dom0 - can create new one based on used options (--save-profile). This commit add a tool itself, update its man page, and add tests for it. Fixes QubesOS/qubes-issues#2931
This commit is contained in:
parent
d8af76ed60
commit
2d5d9d6d7d
@ -7,3 +7,4 @@ codecov
|
||||
python-daemon
|
||||
mock
|
||||
lxml
|
||||
PyYAML
|
||||
|
@ -3,26 +3,29 @@
|
||||
:program:`qvm-backup` -- Create a backup of Qubes
|
||||
=================================================
|
||||
|
||||
.. warning::
|
||||
|
||||
This page was autogenerated from command-line parser. It shouldn't be 1:1
|
||||
conversion, because it would add little value. Please revise it and add
|
||||
more descriptive help, which normally won't fit in standard ``--help``
|
||||
option.
|
||||
|
||||
After rewrite, please remove this admonition.
|
||||
|
||||
Synopsis
|
||||
--------
|
||||
|
||||
:command:`qvm-backup` [-h] [--verbose] [--quiet] [--force-root] [--exclude EXCLUDE_LIST] [--dest-vm *APPVM*] [--encrypt] [--no-encrypt] [--passphrase-file PASS_FILE] [--enc-algo CRYPTO_ALGORITHM] [--hmac-algo HMAC_ALGORITHM] [--compress] [--compress-filter COMPRESS_FILTER] [--tmpdir *TMPDIR*] backup_location [vms [vms ...]]
|
||||
:command:`qvm-backup` [-h] [--verbose] [--quiet] [--profile *PROFILE*] [--exclude EXCLUDE_LIST] [--dest-vm *APPVM*] [--encrypt] [--passphrase-file PASSPHRASE_FILE] [--compress] [--compress-filter *COMPRESSION*] [--save-profile SAVE_PROFILE] backup_location [vms [vms ...]]
|
||||
|
||||
|
||||
Options
|
||||
-------
|
||||
|
||||
.. option:: --profile
|
||||
|
||||
Specify backup profile to use. This option is mutually exclusive with all
|
||||
other options. This is also the only working mode when running from non-dom0.
|
||||
|
||||
.. option:: --save-profile
|
||||
|
||||
Save backup profile based on given options. This is possible only when
|
||||
running in dom0. Otherwise, prepared profile is printed on standard output
|
||||
and user needs to manually place it into /etc/qubes/backup in dom0.
|
||||
|
||||
.. option:: --help, -h
|
||||
|
||||
show this help message and exit
|
||||
show help message and exit
|
||||
|
||||
.. option:: --verbose, -v
|
||||
|
||||
@ -32,10 +35,6 @@ Options
|
||||
|
||||
decrease verbosity
|
||||
|
||||
.. option:: --force-root
|
||||
|
||||
force to run as root
|
||||
|
||||
.. option:: --exclude, -x
|
||||
|
||||
Exclude the specified VM from the backup (may be repeated)
|
||||
@ -46,24 +45,12 @@ Options
|
||||
|
||||
.. option:: --encrypt, -e
|
||||
|
||||
Encrypt the backup
|
||||
|
||||
.. option:: --no-encrypt
|
||||
|
||||
Skip encryption even if sending the backup to a VM
|
||||
Ignored, backup is always encrypted
|
||||
|
||||
.. option:: --passphrase-file, -p
|
||||
|
||||
Read passphrase from a file, or use '-' to read from stdin
|
||||
|
||||
.. option:: --enc-algo, -E
|
||||
|
||||
Specify a non-default encryption algorithm. For a list of supported algorithms, execute 'openssl list-cipher-algorithms' (implies -e)
|
||||
|
||||
.. option:: --hmac-algo, -H
|
||||
|
||||
Specify a non-default HMAC algorithm. For a list of supported algorithms, execute 'openssl list-message-digest-algorithms'
|
||||
|
||||
.. option:: --compress, -z
|
||||
|
||||
Compress the backup
|
||||
@ -72,17 +59,12 @@ Options
|
||||
|
||||
Specify a non-default compression filter program (default: gzip)
|
||||
|
||||
.. option:: --tmpdir
|
||||
|
||||
Specify a temporary directory (if you have at least 1GB free RAM in dom0, use of /tmp is advised) (default: /var/tmp)
|
||||
|
||||
Arguments
|
||||
---------
|
||||
|
||||
The first positional parameter is the backup location (directory path, or
|
||||
command to pipe backup to). After that you may specify the qubes you'd like to
|
||||
backup. If not specified, all qubes with `include_in_backups` property set are
|
||||
included.
|
||||
backup. If not specified, all qubes are included.
|
||||
|
||||
Authors
|
||||
-------
|
||||
|
237
qubesadmin/tests/tools/qvm_backup.py
Normal file
237
qubesadmin/tests/tools/qvm_backup.py
Normal file
@ -0,0 +1,237 @@
|
||||
# -*- encoding: utf8 -*-
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2017 Marek Marczykowski-Górecki
|
||||
# <marmarek@invisiblethingslab.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation; either version 2.1 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License along
|
||||
# with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
import io
|
||||
import os
|
||||
import unittest.mock as mock
|
||||
|
||||
import asyncio
|
||||
|
||||
import qubesadmin.tests
|
||||
import qubesadmin.tests.tools
|
||||
import qubesadmin.tools.qvm_backup as qvm_backup
|
||||
|
||||
class TC_00_qvm_backup(qubesadmin.tests.QubesTestCase):
|
||||
def test_000_write_backup_profile(self):
|
||||
args = qvm_backup.parser.parse_args(['/var/tmp'], app=self.app)
|
||||
profile = io.StringIO()
|
||||
qvm_backup.write_backup_profile(profile, args)
|
||||
expected_profile = (
|
||||
'destination_path: /var/tmp\n'
|
||||
'destination_vm: dom0\n'
|
||||
'include: [\'$type:AppVM\', \'$type:TemplateVM\', '
|
||||
'\'$type:StandaloneVM\']\n'
|
||||
)
|
||||
self.assertEqual(profile.getvalue(), expected_profile)
|
||||
|
||||
def test_001_write_backup_profile_include(self):
|
||||
self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
|
||||
b'0\0dom0 class=AdminVM state=Running\n' \
|
||||
b'vm1 class=AppVM state=Halted\n' \
|
||||
b'vm2 class=AppVM state=Halted\n' \
|
||||
b'vm3 class=AppVM state=Halted\n'
|
||||
args = qvm_backup.parser.parse_args(['/var/tmp', 'vm1', 'vm2'],
|
||||
app=self.app)
|
||||
profile = io.StringIO()
|
||||
qvm_backup.write_backup_profile(profile, args)
|
||||
expected_profile = (
|
||||
'destination_path: /var/tmp\n'
|
||||
'destination_vm: dom0\n'
|
||||
'include: [vm1, vm2]\n'
|
||||
)
|
||||
self.assertEqual(profile.getvalue(), expected_profile)
|
||||
self.assertAllCalled()
|
||||
|
||||
def test_002_write_backup_profile_exclude(self):
|
||||
args = qvm_backup.parser.parse_args([
|
||||
'-x', 'vm1', '-x', 'vm2', '/var/tmp'], app=self.app)
|
||||
profile = io.StringIO()
|
||||
qvm_backup.write_backup_profile(profile, args)
|
||||
expected_profile = (
|
||||
'destination_path: /var/tmp\n'
|
||||
'destination_vm: dom0\n'
|
||||
'exclude: [vm1, vm2]\n'
|
||||
'include: [\'$type:AppVM\', \'$type:TemplateVM\', '
|
||||
'\'$type:StandaloneVM\']\n'
|
||||
)
|
||||
self.assertEqual(profile.getvalue(), expected_profile)
|
||||
|
||||
def test_003_write_backup_with_passphrase(self):
|
||||
args = qvm_backup.parser.parse_args(['/var/tmp'], app=self.app)
|
||||
profile = io.StringIO()
|
||||
qvm_backup.write_backup_profile(profile, args, passphrase='test123')
|
||||
expected_profile = (
|
||||
'destination_path: /var/tmp\n'
|
||||
'destination_vm: dom0\n'
|
||||
'include: [\'$type:AppVM\', \'$type:TemplateVM\', '
|
||||
'\'$type:StandaloneVM\']\n'
|
||||
'passphrase_text: test123\n'
|
||||
)
|
||||
self.assertEqual(profile.getvalue(), expected_profile)
|
||||
|
||||
@mock.patch('qubesadmin.tools.qvm_backup.backup_profile_dir', '/tmp')
|
||||
@mock.patch('qubesadmin.tools.qvm_backup.input', create=True)
|
||||
@mock.patch('getpass.getpass')
|
||||
def test_010_main_save_profile_cancel(self, mock_getpass, mock_input):
|
||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||
mock_input.return_value = 'n'
|
||||
mock_getpass.return_value = 'some password'
|
||||
self.app.qubesd_connection_type = 'socket'
|
||||
self.app.expected_calls[('dom0', 'admin.backup.Info', 'test-profile',
|
||||
None)] = \
|
||||
b'0\0backup summary'
|
||||
profile_path = '/tmp/test-profile.conf'
|
||||
try:
|
||||
qvm_backup.main(['--save-profile', 'test-profile', '/var/tmp'],
|
||||
app=self.app)
|
||||
expected_profile = (
|
||||
'destination_path: /var/tmp\n'
|
||||
'destination_vm: dom0\n'
|
||||
'include: [\'$type:AppVM\', \'$type:TemplateVM\', '
|
||||
'\'$type:StandaloneVM\']\n'
|
||||
)
|
||||
with open(profile_path) as f:
|
||||
self.assertEqual(expected_profile, f.read())
|
||||
finally:
|
||||
os.unlink(profile_path)
|
||||
|
||||
@mock.patch('qubesadmin.tools.qvm_backup.backup_profile_dir', '/tmp')
|
||||
@mock.patch('qubesadmin.tools.qvm_backup.input', create=True)
|
||||
@mock.patch('getpass.getpass')
|
||||
def test_011_main_save_profile_confirm(self, mock_getpass, mock_input):
|
||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||
mock_input.return_value = 'y'
|
||||
mock_getpass.return_value = 'some password'
|
||||
self.app.qubesd_connection_type = 'socket'
|
||||
self.app.expected_calls[('dom0', 'admin.backup.Info', 'test-profile',
|
||||
None)] = \
|
||||
b'0\0backup summary'
|
||||
self.app.expected_calls[('dom0', 'admin.backup.Execute', 'test-profile',
|
||||
None)] = \
|
||||
b'0\0'
|
||||
profile_path = '/tmp/test-profile.conf'
|
||||
try:
|
||||
qvm_backup.main(['--save-profile', 'test-profile', '/var/tmp'],
|
||||
app=self.app)
|
||||
expected_profile = (
|
||||
'destination_path: /var/tmp\n'
|
||||
'destination_vm: dom0\n'
|
||||
'include: [\'$type:AppVM\', \'$type:TemplateVM\', '
|
||||
'\'$type:StandaloneVM\']\n'
|
||||
'passphrase_text: some password\n'
|
||||
)
|
||||
with open(profile_path) as f:
|
||||
self.assertEqual(expected_profile, f.read())
|
||||
finally:
|
||||
os.unlink(profile_path)
|
||||
|
||||
@mock.patch('qubesadmin.tools.qvm_backup.backup_profile_dir', '/tmp')
|
||||
@mock.patch('qubesadmin.tools.qvm_backup.input', create=True)
|
||||
@mock.patch('getpass.getpass')
|
||||
def test_012_main_existing_profile(self, mock_getpass, mock_input):
|
||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||
mock_input.return_value = 'y'
|
||||
mock_getpass.return_value = 'some password'
|
||||
self.app.qubesd_connection_type = 'socket'
|
||||
self.app.expected_calls[('dom0', 'admin.backup.Info', 'test-profile',
|
||||
None)] = \
|
||||
b'0\0backup summary'
|
||||
self.app.expected_calls[('dom0', 'admin.backup.Execute', 'test-profile',
|
||||
None)] = \
|
||||
b'0\0'
|
||||
self.app.expected_calls[('dom0', 'admin.Events', None,
|
||||
None)] = \
|
||||
b'0\0'
|
||||
try:
|
||||
patch = mock.patch(
|
||||
'qubesadmin.events.EventsDispatcher._get_events_reader')
|
||||
mock_events = patch.start()
|
||||
self.addCleanup(patch.stop)
|
||||
mock_events.side_effect = qubesadmin.tests.tools.MockEventsReader([
|
||||
b'1\0\0connection-established\0\0',
|
||||
b'1\0\0backup-progress\0backup_profile\0test-profile\0progress\x000'
|
||||
b'.25\0\0',
|
||||
])
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
qvm_backup.main(['--profile', 'test-profile'],
|
||||
app=self.app)
|
||||
self.assertFalse(os.path.exists('/tmp/test-profile.conf'))
|
||||
self.assertFalse(mock_getpass.called)
|
||||
|
||||
@mock.patch('qubesadmin.tools.qvm_backup.backup_profile_dir', '/tmp')
|
||||
@mock.patch('qubesadmin.tools.qvm_backup.input', create=True)
|
||||
@mock.patch('getpass.getpass')
|
||||
def test_013_main_new_profile_vm(self, mock_getpass, mock_input):
|
||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||
mock_input.return_value = 'y'
|
||||
mock_getpass.return_value = 'some password'
|
||||
self.app.qubesd_connection_type = 'qrexec'
|
||||
with qubesadmin.tests.tools.StdoutBuffer() as stdout:
|
||||
qvm_backup.main(['-x', 'vm1', '/var/tmp'],
|
||||
app=self.app)
|
||||
expected_output = (
|
||||
'To perform the backup according to selected options, create '
|
||||
'backup profile (/tmp/profile_name.conf) in dom0 with following '
|
||||
'content:\n'
|
||||
'destination_path: /var/tmp\n'
|
||||
'destination_vm: dom0\n'
|
||||
'exclude: [vm1]\n'
|
||||
'include: [\'$type:AppVM\', \'$type:TemplateVM\', '
|
||||
'\'$type:StandaloneVM\']\n'
|
||||
'# specify backup passphrase below\n'
|
||||
'passphrase_text: ...\n'
|
||||
)
|
||||
self.assertEqual(stdout.getvalue(), expected_output)
|
||||
|
||||
@mock.patch('qubesadmin.tools.qvm_backup.backup_profile_dir', '/tmp')
|
||||
@mock.patch('qubesadmin.tools.qvm_backup.input', create=True)
|
||||
@mock.patch('getpass.getpass')
|
||||
def test_014_main_passphrase_file(self, mock_getpass, mock_input):
|
||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||
mock_input.return_value = 'y'
|
||||
mock_getpass.return_value = 'some password'
|
||||
self.app.qubesd_connection_type = 'socket'
|
||||
self.app.expected_calls[('dom0', 'admin.backup.Info', 'test-profile',
|
||||
None)] = \
|
||||
b'0\0backup summary'
|
||||
self.app.expected_calls[('dom0', 'admin.backup.Execute', 'test-profile',
|
||||
None)] = \
|
||||
b'0\0'
|
||||
profile_path = '/tmp/test-profile.conf'
|
||||
try:
|
||||
stdin = io.StringIO()
|
||||
stdin.write('other passphrase\n')
|
||||
stdin.seek(0)
|
||||
with mock.patch('sys.stdin', stdin):
|
||||
qvm_backup.main(['--passphrase-file', '-', '--save-profile',
|
||||
'test-profile', '/var/tmp'],
|
||||
app=self.app)
|
||||
expected_profile = (
|
||||
'destination_path: /var/tmp\n'
|
||||
'destination_vm: dom0\n'
|
||||
'include: [\'$type:AppVM\', \'$type:TemplateVM\', '
|
||||
'\'$type:StandaloneVM\']\n'
|
||||
'passphrase_text: other passphrase\n'
|
||||
)
|
||||
with open(profile_path) as f:
|
||||
self.assertEqual(expected_profile, f.read())
|
||||
finally:
|
||||
os.unlink(profile_path)
|
221
qubesadmin/tools/qvm_backup.py
Normal file
221
qubesadmin/tools/qvm_backup.py
Normal file
@ -0,0 +1,221 @@
|
||||
# -*- encoding: utf8 -*-
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2017 Marek Marczykowski-Górecki
|
||||
# <marmarek@invisiblethingslab.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation; either version 2.1 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License along
|
||||
# with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
'''qvm-backup tool'''
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import getpass
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
try:
|
||||
import qubesadmin.events
|
||||
have_events = True
|
||||
except ImportError:
|
||||
have_events = False
|
||||
import qubesadmin.tools
|
||||
from qubesadmin.exc import QubesException
|
||||
|
||||
backup_profile_dir = '/etc/qubes/backup'
|
||||
|
||||
parser = qubesadmin.tools.QubesArgumentParser()
|
||||
|
||||
parser.add_argument("--yes", "-y", action="store_true",
|
||||
dest="yes", default=False,
|
||||
help="Do not ask for confirmation")
|
||||
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument('--profile', action='store',
|
||||
help='Perform backup defined by a given profile')
|
||||
no_profile = group.add_argument_group('Profile setup',
|
||||
'Manually specify profile options')
|
||||
no_profile.add_argument("--exclude", "-x", action="append",
|
||||
dest="exclude_list", default=[],
|
||||
help="Exclude the specified VM from the backup (may be "
|
||||
"repeated)")
|
||||
no_profile.add_argument("--dest-vm", "-d", action="store",
|
||||
dest="appvm", default=None,
|
||||
help="Specify the destination VM to which the backup "
|
||||
"will be sent (implies -e)")
|
||||
no_profile.add_argument("--encrypt", "-e", action="store_true",
|
||||
dest="encrypted", default=True,
|
||||
help="Ignored, backup is always encrypted")
|
||||
no_profile.add_argument("--passphrase-file", "-p", action="store",
|
||||
dest="passphrase_file", default=None,
|
||||
help="Read passphrase from a file, or use '-' to read "
|
||||
"from stdin")
|
||||
no_profile.add_argument("--compress", "-z", action="store_true",
|
||||
dest="compression", default=False,
|
||||
help="Compress the backup")
|
||||
no_profile.add_argument("--compress-filter", "-Z", action="store",
|
||||
dest="compression",
|
||||
help="Specify a non-default compression filter program "
|
||||
"(default: gzip)")
|
||||
no_profile.add_argument('--save-profile', action='store',
|
||||
help='Save profile under selected name for further use.'
|
||||
'Available only in dom0.')
|
||||
|
||||
no_profile.add_argument("backup_location", action="store", default=None,
|
||||
nargs='?',
|
||||
help="Backup location (directory path, or command to pipe backup to)")
|
||||
|
||||
no_profile.add_argument("vms", nargs="*", action=qubesadmin.tools.VmNameAction,
|
||||
help="Backup only those VMs")
|
||||
|
||||
|
||||
def write_backup_profile(output_stream, args, passphrase=None):
|
||||
'''Format backup profile and print it to *output_stream* (a file or
|
||||
stdout)
|
||||
|
||||
:param output_stream: file-like object ro print the profile to
|
||||
:param args: parsed arguments
|
||||
:param passphrase: passphrase to use
|
||||
'''
|
||||
|
||||
profile_data = {}
|
||||
if args.vms:
|
||||
profile_data['include'] = args.vms
|
||||
else:
|
||||
profile_data['include'] = [
|
||||
'$type:AppVM', '$type:TemplateVM', '$type:StandaloneVM']
|
||||
if args.exclude_list:
|
||||
profile_data['exclude'] = args.exclude_list
|
||||
if passphrase:
|
||||
profile_data['passphrase_text'] = passphrase
|
||||
if args.compression:
|
||||
profile_data['compression'] = args.compression
|
||||
if args.appvm:
|
||||
profile_data['destination_vm'] = args.appvm
|
||||
else:
|
||||
profile_data['destination_vm'] = 'dom0'
|
||||
profile_data['destination_path'] = args.backup_location
|
||||
|
||||
yaml.safe_dump(profile_data, output_stream)
|
||||
|
||||
|
||||
def print_progress(expected_profile, _subject, _event, backup_profile,
|
||||
progress):
|
||||
'''Event handler for reporting backup progress'''
|
||||
if backup_profile != expected_profile:
|
||||
return
|
||||
sys.stderr.write('\rMaking a backup... {:.02f}%'.format(float(progress)))
|
||||
|
||||
def main(args=None, app=None):
|
||||
'''Main function of qvm-backup tool'''
|
||||
args = parser.parse_args(args, app=app)
|
||||
profile_path = None
|
||||
if args.profile is None:
|
||||
if args.backup_location is None:
|
||||
parser.error('either --profile or \'backup_location\' is required')
|
||||
if args.app.qubesd_connection_type == 'socket':
|
||||
# when running in dom0, we can create backup profile, including
|
||||
# passphrase
|
||||
if args.save_profile:
|
||||
profile_name = args.save_profile
|
||||
else:
|
||||
# don't care about collisions because only the user in dom0 can
|
||||
# trigger this, and qrexec policy should not allow random VM
|
||||
# to execute the same backup in the meantime
|
||||
profile_name = 'backup-run-{}'.format(os.getpid())
|
||||
# first write the backup profile without passphrase, to display
|
||||
# summary
|
||||
profile_path = os.path.join(
|
||||
backup_profile_dir, profile_name + '.conf')
|
||||
with open(profile_path, 'w') as f_profile:
|
||||
write_backup_profile(f_profile, args)
|
||||
else:
|
||||
if args.save_profile:
|
||||
parser.error(
|
||||
'Cannot save backup profile when running not in dom0')
|
||||
# unreachable - parser.error terminate the process
|
||||
return 1
|
||||
print('To perform the backup according to selected options, '
|
||||
'create backup profile ({}) in dom0 with following '
|
||||
'content:'.format(
|
||||
os.path.join(backup_profile_dir, 'profile_name.conf')))
|
||||
write_backup_profile(sys.stdout, args)
|
||||
print('# specify backup passphrase below')
|
||||
print('passphrase_text: ...')
|
||||
return 1
|
||||
else:
|
||||
profile_name = args.profile
|
||||
|
||||
backup_summary = args.app.qubesd_call(
|
||||
'dom0', 'admin.backup.Info', profile_name)
|
||||
print(backup_summary.decode())
|
||||
|
||||
if not args.yes:
|
||||
if input("Do you want to proceed? [y/N] ").upper() != "Y":
|
||||
if args.profile is None and not args.save_profile:
|
||||
os.unlink(profile_path)
|
||||
return 0
|
||||
|
||||
if args.profile is None:
|
||||
if args.passphrase_file is not None:
|
||||
pass_f = open(args.passphrase_file) \
|
||||
if args.passphrase_file != "-" else sys.stdin
|
||||
passphrase = pass_f.readline().rstrip()
|
||||
if pass_f is not sys.stdin:
|
||||
pass_f.close()
|
||||
else:
|
||||
prompt = ("Please enter the passphrase that will be used to "
|
||||
"encrypt and verify the backup: ")
|
||||
passphrase = getpass.getpass(prompt)
|
||||
|
||||
if getpass.getpass("Enter again for verification: ") != passphrase:
|
||||
parser.error_runtime("Passphrase mismatch!")
|
||||
|
||||
with open(profile_path, 'w') as f_profile:
|
||||
write_backup_profile(f_profile, args, passphrase)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
if have_events:
|
||||
# pylint: disable=no-member
|
||||
events_dispatcher = qubesadmin.events.EventsDispatcher(args.app)
|
||||
events_dispatcher.add_handler('backup-progress',
|
||||
functools.partial(print_progress, profile_name))
|
||||
events_task = asyncio.ensure_future(
|
||||
events_dispatcher.listen_for_events())
|
||||
loop.add_signal_handler(signal.SIGINT,
|
||||
args.app.qubesd_call, 'dom0', 'admin.backup.Cancel', profile_name)
|
||||
try:
|
||||
loop.run_until_complete(loop.run_in_executor(None,
|
||||
args.app.qubesd_call, 'dom0', 'admin.backup.Execute', profile_name))
|
||||
except QubesException as err:
|
||||
print('\nBackup error: {}'.format(err), file=sys.stderr)
|
||||
return 1
|
||||
finally:
|
||||
if have_events:
|
||||
events_task.cancel()
|
||||
try:
|
||||
loop.run_until_complete(events_task)
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
loop.close()
|
||||
if args.profile is None and not args.save_profile:
|
||||
os.unlink(profile_path)
|
||||
print('\n')
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
Loading…
Reference in New Issue
Block a user