api/admin: plug backup into Admin API

Fixes QubesOS/qubes-issues#2931
This commit is contained in:
Marek Marczykowski-Górecki 2017-07-20 03:07:46 +02:00
parent 81246cac64
commit 6dbce8259f
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
6 changed files with 174 additions and 3 deletions

View File

@ -12,7 +12,7 @@ ADMIN_API_METHODS_SIMPLE = \
admin.Events \ admin.Events \
admin.backup.Execute \ admin.backup.Execute \
admin.backup.Info \ admin.backup.Info \
admin.backup.Restore \ admin.backup.Cancel \
admin.label.Create \ admin.label.Create \
admin.label.Get \ admin.label.Get \
admin.label.List \ admin.label.List \

View File

@ -8,3 +8,4 @@ lxml
pylint pylint
sphinx sphinx
pydbus pydbus
PyYAML

View File

@ -23,17 +23,24 @@ Qubes OS Management API
''' '''
import asyncio import asyncio
import string import functools
import itertools import itertools
import pkg_resources import os
import string
import libvirt import libvirt
import pkg_resources
import yaml
import qubes.api import qubes.api
import qubes.backup
import qubes.config
import qubes.devices import qubes.devices
import qubes.firewall import qubes.firewall
import qubes.storage import qubes.storage
import qubes.utils import qubes.utils
import qubes.vm import qubes.vm
import qubes.vm.adminvm
import qubes.vm.qubesvm import qubes.vm.qubesvm
@ -1093,3 +1100,152 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
self.fire_event_for_permission() self.fire_event_for_permission()
self.dest.fire_event('firewall-changed') self.dest.fire_event('firewall-changed')
@asyncio.coroutine
def _load_backup_profile(self, profile_name, skip_passphrase=False):
'''Load backup profile and return :py:class:`qubes.backup.Backup`
instance
:param profile_name: name of the profile
:param skip_passphrase: do not load passphrase - only backup summary
can be retrieved when this option is in use
'''
profile_path = os.path.join(
qubes.config.backup_profile_dir, profile_name + '.conf')
with open(profile_path) as profile_file:
profile_data = yaml.safe_load(profile_file)
try:
dest_vm = profile_data['destination_vm']
dest_path = profile_data['destination_path']
include_vms = profile_data['include']
exclude_vms = profile_data.get('exclude', [])
compression = profile_data.get('compression', True)
except KeyError as err:
raise qubes.exc.QubesException(
'Invalid backup profile - missing {}'.format(err))
try:
dest_vm = self.app.domains[dest_vm]
except KeyError:
raise qubes.exc.QubesException(
'Invalid destination_vm specified in backup profile')
if isinstance(dest_vm, qubes.vm.adminvm.AdminVM):
dest_vm = None
if skip_passphrase:
passphrase = None
elif 'passphrase_text' in profile_data:
passphrase = profile_data['passphrase_text']
elif 'passphrase_vm' in profile_data:
passphrase_vm_name = profile_data['passphrase_vm']
try:
passphrase_vm = self.app.domains[passphrase_vm_name]
except KeyError:
raise qubes.exc.QubesException(
'Invalid backup profile - invalid passphrase_vm')
# TODO .decode()?
passphrase, _ = yield from passphrase_vm.run_service_for_stdio(
'qubes.BackupPassphrase+' + self.arg)
else:
raise qubes.exc.QubesException(
'Invalid backup profile - you need to '
'specify passphrase_text or passphrase_vm')
# handle include
vms_to_backup = set(vm for vm in self.app.domains
if any(qubes.utils.match_vm_name_with_special(vm, name)
for name in include_vms))
# handle exclude
vms_to_backup.difference_update(vm for vm in self.app.domains
if any(qubes.utils.match_vm_name_with_special(vm, name)
for name in exclude_vms))
kwargs = {
'target_vm': dest_vm,
'target_dir': dest_path,
'compressed': bool(compression),
'passphrase': passphrase,
}
if isinstance(compression, str):
kwargs['compression_filter'] = compression
backup = qubes.backup.Backup(self.app, vms_to_backup, **kwargs)
return backup
def _backup_progress_callback(self, profile_name, progress):
self.app.fire_event('backup-progress', backup_profile=profile_name,
progress=progress)
@qubes.api.method('admin.backup.Execute', no_payload=True,
scope='global', read=True, execute=True)
@asyncio.coroutine
def backup_execute(self):
assert self.dest.name == 'dom0'
assert self.arg
assert '/' not in self.arg
self.fire_event_for_permission()
profile_path = os.path.join(qubes.config.backup_profile_dir,
self.arg + '.conf')
if not os.path.exists(profile_path):
raise qubes.api.PermissionDenied(
'Backup profile {} does not exist'.format(self.arg))
if not hasattr(self.app, 'api_admin_running_backups'):
self.app.api_admin_running_backups = {}
backup = yield from self._load_backup_profile(self.arg)
backup.progress_callback = functools.partial(
self._backup_progress_callback, self.arg)
# forbid running the same backup operation twice at the time
assert self.arg not in self.app.api_admin_running_backups
backup_task = asyncio.ensure_future(backup.backup_do())
self.app.api_admin_running_backups[self.arg] = backup_task
try:
yield from backup_task
finally:
del self.app.api_admin_running_backups[self.arg]
@qubes.api.method('admin.backup.Cancel', no_payload=True,
scope='global', execute=True)
@asyncio.coroutine
def backup_cancel(self):
assert self.dest.name == 'dom0'
assert self.arg
assert '/' not in self.arg
self.fire_event_for_permission()
if not hasattr(self.app, 'api_admin_running_backups'):
self.app.api_admin_running_backups = {}
if self.arg not in self.app.api_admin_running_backups:
raise qubes.exc.QubesException('Backup operation not running')
self.app.api_admin_running_backups[self.arg].cancel()
@qubes.api.method('admin.backup.Info', no_payload=True,
scope='local', read=True)
@asyncio.coroutine
def backup_info(self):
assert self.dest.name == 'dom0'
assert self.arg
assert '/' not in self.arg
self.fire_event_for_permission()
profile_path = os.path.join(qubes.config.backup_profile_dir,
self.arg + '.conf')
if not os.path.exists(profile_path):
raise qubes.api.PermissionDenied(
'Backup profile {} does not exist'.format(self.arg))
backup = yield from self._load_backup_profile(self.arg,
skip_passphrase=True)
return backup.get_backup_summary()

View File

@ -106,3 +106,6 @@ max_dispid = 10000
#: built-in standard labels, if creating new one, allocate them above this #: built-in standard labels, if creating new one, allocate them above this
# number, at least until label index is removed from API # number, at least until label index is removed from API
max_default_label = 8 max_default_label = 8
#: profiles for admin.backup.* calls
backup_profile_dir = '/etc/qubes/backup'

View File

@ -176,3 +176,12 @@ def systemd_notify():
sock.connect(nofity_socket) sock.connect(nofity_socket)
sock.sendall(b'READY=1') sock.sendall(b'READY=1')
sock.close() sock.close()
def match_vm_name_with_special(vm, name):
'''Check if *vm* matches given name, which may be specified as $tag:...
or $type:...'''
if name.startswith('$tag:'):
return name[len('$tag:'):] in vm.tags
elif name.startswith('$type:'):
return name[len('$type:'):] == vm.__class__.__name__
return name == vm.name

View File

@ -65,6 +65,7 @@ BuildRequires: python3-sphinx
BuildRequires: python3-lxml BuildRequires: python3-lxml
BuildRequires: libvirt-python3 BuildRequires: libvirt-python3
BuildRequires: python3-dbus BuildRequires: python3-dbus
BuildRequires: python3-PyYAML
Requires(post): systemd-units Requires(post): systemd-units
Requires(preun): systemd-units Requires(preun): systemd-units
@ -78,6 +79,7 @@ Requires: python3-lxml
Requires: python3-pydbus Requires: python3-pydbus
Requires: python3-qubesdb Requires: python3-qubesdb
Requires: python3-setuptools Requires: python3-setuptools
Requires: python3-PyYAML
Requires: python3-xen Requires: python3-xen
Requires: libvirt-python3 Requires: libvirt-python3