parent
81246cac64
commit
6dbce8259f
2
Makefile
2
Makefile
@ -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 \
|
||||||
|
@ -8,3 +8,4 @@ lxml
|
|||||||
pylint
|
pylint
|
||||||
sphinx
|
sphinx
|
||||||
pydbus
|
pydbus
|
||||||
|
PyYAML
|
||||||
|
@ -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()
|
||||||
|
@ -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'
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user