Merge remote-tracking branch 'marmarek/core3-mgmt-methods1' into core3-devel

Conflicts:
	qubes/mgmt.py
This commit is contained in:
Wojtek Porczyk 2017-04-01 18:33:54 +02:00
commit 1b9479837a
18 changed files with 1562 additions and 78 deletions

3
ci/coveragerc Normal file
View File

@ -0,0 +1,3 @@
[run]
source = qubes
omit = qubes/tests/*

View File

@ -1,5 +1,6 @@
# WARNING: those requirements are used only for travis-ci.org # WARNING: those requirements are used only for travis-ci.org
# they SHOULD NOT be used under normal conditions; use system package manager # they SHOULD NOT be used under normal conditions; use system package manager
coverage
docutils docutils
jinja2 jinja2
lxml lxml

View File

@ -31,6 +31,7 @@ import builtins
import collections import collections
import os import os
import os.path import os.path
import string
import lxml.etree import lxml.etree
import qubes.config import qubes.config
@ -104,6 +105,10 @@ class Label(object):
self.color, self.color,
self.name) self.name)
def __eq__(self, other):
if isinstance(other, Label):
return self.name == other.name
return NotImplemented
@builtins.property @builtins.property
def icon_path(self): def icon_path(self):
@ -193,7 +198,7 @@ class property(object): # pylint: disable=redefined-builtin,invalid-name
self._setter = setter self._setter = setter
self._saver = saver if saver is not None else ( self._saver = saver if saver is not None else (
lambda self, prop, value: str(value)) lambda self, prop, value: str(value))
self._type = type self.type = type
self._default = default self._default = default
self._write_once = write_once self._write_once = write_once
self.order = order self.order = order
@ -245,8 +250,8 @@ class property(object): # pylint: disable=redefined-builtin,invalid-name
if self._setter is not None: if self._setter is not None:
value = self._setter(instance, self, value) value = self._setter(instance, self, value)
if self._type not in (None, type(value)): if self.type not in (None, type(value)):
value = self._type(value) value = self.type(value)
if has_oldvalue: if has_oldvalue:
instance.fire_event_pre('property-pre-set:' + self.__name__, instance.fire_event_pre('property-pre-set:' + self.__name__,
@ -317,6 +322,41 @@ class property(object): # pylint: disable=redefined-builtin,invalid-name
'property {!r} is write-once and already set'.format( 'property {!r} is write-once and already set'.format(
self.__name__)) self.__name__))
def sanitize(self, *, untrusted_newvalue):
'''Coarse sanitization of value to be set, before sending it to a
setter. Can raise QubesValueError if the value is invalid.
:param untrusted_newvalue: value to be validated
:return sanitized value
:raises qubes.exc.QubesValueError
'''
# do not treat type='str' as sufficient validation
if self.type is not None and self.type is not str:
# assume specific type will preform enough validation
if self.type is bool:
try:
untrusted_newvalue = untrusted_newvalue.decode('ascii')
except UnicodeDecodeError:
raise qubes.exc.QubesValueError
return self.bool(None, None, untrusted_newvalue)
else:
try:
return self.type(untrusted_newvalue)
except ValueError:
raise qubes.exc.QubesValueError
else:
# 'str' or not specified type
try:
untrusted_newvalue = untrusted_newvalue.decode('ascii',
errors='strict')
except UnicodeDecodeError:
raise qubes.exc.QubesValueError
allowed_set = string.printable
if not all(x in allowed_set for x in untrusted_newvalue):
raise qubes.exc.QubesValueError(
'Invalid characters in property value')
return untrusted_newvalue
# #
# exceptions # exceptions
@ -368,7 +408,7 @@ class property(object): # pylint: disable=redefined-builtin,invalid-name
return False return False
if lcvalue in ('1', 'yes', 'true', 'on'): if lcvalue in ('1', 'yes', 'true', 'on'):
return True return True
raise ValueError( raise qubes.exc.QubesValueError(
'Invalid literal for boolean property: {!r}'.format(value)) 'Invalid literal for boolean property: {!r}'.format(value))
return bool(value) return bool(value)

View File

@ -859,6 +859,7 @@ class Qubes(qubes.PropertyHolder):
7: qubes.Label(7, '0x75507b', 'purple'), 7: qubes.Label(7, '0x75507b', 'purple'),
8: qubes.Label(8, '0x000000', 'black'), 8: qubes.Label(8, '0x000000', 'black'),
} }
assert max(self.labels.keys()) == qubes.config.max_default_label
# check if the default LVM Thin pool qubes_dom0/pool00 exists # check if the default LVM Thin pool qubes_dom0/pool00 exists
if os.path.exists('/dev/mapper/qubes_dom0-pool00-tpool'): if os.path.exists('/dev/mapper/qubes_dom0-pool00-tpool'):
@ -872,7 +873,8 @@ class Qubes(qubes.PropertyHolder):
self.pools[name] = self._get_pool(**config) self.pools[name] = self._get_pool(**config)
self.domains.add( self.domains.add(
qubes.vm.adminvm.AdminVM(self, None, qid=0, name='dom0')) qubes.vm.adminvm.AdminVM(self, None, qid=0, name='dom0',
label='black'))
@classmethod @classmethod
def create_empty_store(cls, *args, **kwargs): def create_empty_store(cls, *args, **kwargs):
@ -1085,7 +1087,7 @@ class Qubes(qubes.PropertyHolder):
oldvalue=None): oldvalue=None):
# pylint: disable=unused-argument # pylint: disable=unused-argument
for vm in self.domains: for vm in self.domains:
if vm.provides_network and vm.property_is_default('netvm'): if vm.property_is_default('netvm'):
# fire property-del:netvm as it is responsible for resetting # fire property-del:netvm as it is responsible for resetting
# netvm to it's default value # netvm to it's default value
vm.fire_event('property-del:netvm', vm.fire_event('property-del:netvm',

View File

@ -109,3 +109,6 @@ defaults = {
max_qid = 254 max_qid = 254
max_netid = 254 max_netid = 254
max_dispid = 10000 max_dispid = 10000
#: built-in standard labels, if creating new one, allocate them above this
# number, at least until label index is removed from API
max_default_label = 8

View File

@ -23,8 +23,12 @@ Qubes OS Management API
''' '''
import asyncio import asyncio
import string
import functools
import qubes.vm.qubesvm import qubes.vm.qubesvm
import qubes.storage
class ProtocolError(AssertionError): class ProtocolError(AssertionError):
@ -46,6 +50,16 @@ def not_in_api(func):
func.not_in_api = True func.not_in_api = True
return func return func
def no_payload(func):
@functools.wraps(func)
def wrapper(self, untrusted_payload):
if untrusted_payload != b'':
raise ProtocolError('unexpected payload')
return func(self)
return wrapper
class QubesMgmt(object): class QubesMgmt(object):
'''Implementation of Qubes Management API calls '''Implementation of Qubes Management API calls
@ -121,14 +135,15 @@ class QubesMgmt(object):
# #
@asyncio.coroutine @asyncio.coroutine
def vm_list(self, untrusted_payload): @no_payload
def vm_list(self):
'''List all the domains''' '''List all the domains'''
assert self.dest.name == 'dom0'
assert not self.arg assert not self.arg
assert not untrusted_payload
del untrusted_payload
domains = self.fire_event_for_filter(self.app.domains) if self.dest.name == 'dom0':
domains = self.fire_event_for_filter(self.app.domains)
else:
domains = self.fire_event_for_filter([self.dest])
return ''.join('{} class={} state={}\n'.format( return ''.join('{} class={} state={}\n'.format(
vm.name, vm.name,
@ -137,40 +152,63 @@ class QubesMgmt(object):
for vm in sorted(domains)) for vm in sorted(domains))
@asyncio.coroutine @asyncio.coroutine
def vm_property_list(self, untrusted_payload): @no_payload
def vm_property_list(self):
'''List all properties on a qube''' '''List all properties on a qube'''
assert not self.arg assert not self.arg
assert not untrusted_payload
del untrusted_payload
properties = self.fire_event_for_filter(self.dest.property_list()) properties = self.fire_event_for_filter(self.dest.property_list())
return ''.join('{}\n'.format(prop.__name__) for prop in properties) return ''.join('{}\n'.format(prop.__name__) for prop in properties)
@asyncio.coroutine @asyncio.coroutine
def vm_property_get(self, untrusted_payload): @no_payload
def vm_property_get(self):
'''Get a value of one property''' '''Get a value of one property'''
assert self.arg in self.dest.property_list() assert self.arg in self.dest.property_list()
assert not untrusted_payload
del untrusted_payload
self.fire_event_for_permission() self.fire_event_for_permission()
property_def = self.dest.property_get_def(self.arg)
# explicit list to be sure that it matches protocol spec
if isinstance(property_def, qubes.vm.VMProperty):
property_type = 'vm'
elif property_def.type is int:
property_type = 'int'
elif property_def.type is bool:
property_type = 'bool'
elif self.arg == 'label':
property_type = 'label'
else:
property_type = 'str'
try: try:
value = getattr(self.dest, self.arg) value = getattr(self.dest, self.arg)
except AttributeError: except AttributeError:
return 'default=True ' return 'default=True type={} '.format(property_type)
else: else:
return 'default={} {}'.format( return 'default={} type={} {}'.format(
str(self.dest.property_is_default(self.arg)), str(self.dest.property_is_default(self.arg)),
str(value)) property_type,
str(value) if value is not None else '')
@asyncio.coroutine @asyncio.coroutine
def vm_property_help(self, untrusted_payload): def vm_property_set(self, untrusted_payload):
assert self.arg in self.dest.property_list()
property_def = self.dest.property_get_def(self.arg)
newvalue = property_def.sanitize(untrusted_newvalue=untrusted_payload)
self.fire_event_for_permission(newvalue=newvalue)
setattr(self.dest, self.arg, newvalue)
self.app.save()
@asyncio.coroutine
@no_payload
def vm_property_help(self):
'''Get help for one property''' '''Get help for one property'''
assert self.arg in self.dest.property_list() assert self.arg in self.dest.property_list()
assert not untrusted_payload
del untrusted_payload
self.fire_event_for_permission() self.fire_event_for_permission()
@ -182,12 +220,239 @@ class QubesMgmt(object):
return qubes.utils.format_doc(doc) return qubes.utils.format_doc(doc)
@asyncio.coroutine @asyncio.coroutine
def vm_property_reset(self, untrusted_payload): @no_payload
def vm_property_reset(self):
'''Reset a property to a default value''' '''Reset a property to a default value'''
assert self.arg in self.dest.property_list() assert self.arg in self.dest.property_list()
assert not untrusted_payload
del untrusted_payload
self.fire_event_for_permission() self.fire_event_for_permission()
delattr(self.dest, self.arg) delattr(self.dest, self.arg)
self.app.save()
@asyncio.coroutine
@no_payload
def vm_volume_list(self):
assert not self.arg
volume_names = self.fire_event_for_filter(self.dest.volumes.keys())
return ''.join('{}\n'.format(name) for name in volume_names)
@asyncio.coroutine
@no_payload
def vm_volume_info(self):
assert self.arg in self.dest.volumes.keys()
self.fire_event_for_permission()
volume = self.dest.volumes[self.arg]
# properties defined in API
volume_properties = [
'pool', 'vid', 'size', 'usage', 'rw', 'internal', 'source',
'save_on_stop', 'snap_on_start']
return ''.join('{}={}\n'.format(key, getattr(volume, key)) for key in
volume_properties)
@asyncio.coroutine
@no_payload
def vm_volume_listsnapshots(self):
assert self.arg in self.dest.volumes.keys()
volume = self.dest.volumes[self.arg]
revisions = [revision for revision in volume.revisions]
revisions = self.fire_event_for_filter(revisions)
return ''.join('{}\n'.format(revision) for revision in revisions)
@asyncio.coroutine
def vm_volume_revert(self, untrusted_payload):
assert self.arg in self.dest.volumes.keys()
untrusted_revision = untrusted_payload.decode('ascii').strip()
del untrusted_payload
volume = self.dest.volumes[self.arg]
snapshots = volume.revisions
assert untrusted_revision in snapshots
revision = untrusted_revision
self.fire_event_for_permission(revision=revision)
self.dest.storage.get_pool(volume).revert(revision)
self.app.save()
@asyncio.coroutine
def vm_volume_resize(self, untrusted_payload):
assert self.arg in self.dest.volumes.keys()
untrusted_size = untrusted_payload.decode('ascii').strip()
del untrusted_payload
assert untrusted_size.isdigit() # only digits, forbid '-' too
assert len(untrusted_size) <= 20 # limit to about 2^64
size = int(untrusted_size)
self.fire_event_for_permission(size=size)
self.dest.storage.resize(self.arg, size)
self.app.save()
@asyncio.coroutine
@no_payload
def pool_list(self):
assert not self.arg
assert self.dest.name == 'dom0'
pools = self.fire_event_for_filter(self.app.pools)
return ''.join('{}\n'.format(pool) for pool in pools)
@asyncio.coroutine
@no_payload
def pool_listdrivers(self):
assert self.dest.name == 'dom0'
assert not self.arg
drivers = self.fire_event_for_filter(qubes.storage.pool_drivers())
return ''.join('{} {}\n'.format(
driver,
' '.join(qubes.storage.driver_parameters(driver)))
for driver in drivers)
@asyncio.coroutine
@no_payload
def pool_info(self):
assert self.dest.name == 'dom0'
assert self.arg in self.app.pools.keys()
pool = self.app.pools[self.arg]
self.fire_event_for_permission(pool=pool)
return ''.join('{}={}\n'.format(prop, val)
for prop, val in sorted(pool.config.items()))
@asyncio.coroutine
def pool_add(self, untrusted_payload):
assert self.dest.name == 'dom0'
drivers = qubes.storage.pool_drivers()
assert self.arg in drivers
untrusted_pool_config = untrusted_payload.decode('ascii').splitlines()
del untrusted_payload
assert all(('=' in line) for line in untrusted_pool_config)
# pairs of (option, value)
untrusted_pool_config = [line.split('=', 1)
for line in untrusted_pool_config]
# reject duplicated options
assert len(set(x[0] for x in untrusted_pool_config)) == \
len([x[0] for x in untrusted_pool_config])
# and convert to dict
untrusted_pool_config = dict(untrusted_pool_config)
assert 'name' in untrusted_pool_config
untrusted_pool_name = untrusted_pool_config.pop('name')
allowed_chars = string.ascii_letters + string.digits + '-_.'
assert all(c in allowed_chars for c in untrusted_pool_name)
pool_name = untrusted_pool_name
assert pool_name not in self.app.pools
driver_parameters = qubes.storage.driver_parameters(self.arg)
assert all(key in driver_parameters for key in untrusted_pool_config)
pool_config = untrusted_pool_config
self.fire_event_for_permission(name=pool_name,
pool_config=pool_config)
self.app.add_pool(name=pool_name, driver=self.arg, **pool_config)
self.app.save()
@asyncio.coroutine
@no_payload
def pool_remove(self):
assert self.dest.name == 'dom0'
assert self.arg in self.app.pools.keys()
self.fire_event_for_permission()
self.app.remove_pool(self.arg)
self.app.save()
@asyncio.coroutine
@no_payload
def label_list(self):
assert self.dest.name == 'dom0'
assert not self.arg
labels = self.fire_event_for_filter(self.app.labels.values())
return ''.join('{}\n'.format(label.name) for label in labels)
@asyncio.coroutine
@no_payload
def label_get(self):
assert self.dest.name == 'dom0'
try:
label = self.app.get_label(self.arg)
except KeyError:
raise qubes.exc.QubesValueError
self.fire_event_for_permission(label=label)
return label.color
@asyncio.coroutine
def label_create(self, untrusted_payload):
assert self.dest.name == 'dom0'
# don't confuse label name with label index
assert not self.arg.isdigit()
allowed_chars = string.ascii_letters + string.digits + '-_.'
assert all(c in allowed_chars for c in self.arg)
try:
self.app.get_label(self.arg)
except KeyError:
# ok, no such label yet
pass
else:
raise qubes.exc.QubesValueError('label already exists')
untrusted_payload = untrusted_payload.decode('ascii').strip()
assert len(untrusted_payload) == 8
assert untrusted_payload.startswith('0x')
# besides prefix, only hex digits are allowed
assert all(x in string.hexdigits for x in untrusted_payload[2:])
# SEE: #2732
color = untrusted_payload
self.fire_event_for_permission(color=color)
# allocate new index, but make sure it's outside of default labels set
new_index = max(
qubes.config.max_default_label, *self.app.labels.keys()) + 1
label = qubes.Label(new_index, color, self.arg)
self.app.labels[new_index] = label
self.app.save()
@asyncio.coroutine
@no_payload
def label_remove(self):
assert self.dest.name == 'dom0'
try:
label = self.app.get_label(self.arg)
except KeyError:
raise qubes.exc.QubesValueError
# don't allow removing default labels
assert label.index > qubes.config.max_default_label
# FIXME: this should be in app.add_label()
for vm in self.app.domains:
if vm.label == label:
raise qubes.exc.QubesException('label still in use')
self.fire_event_for_permission(label=label)
del self.app.labels[label.index]
self.app.save()

View File

@ -635,6 +635,15 @@ def pool_drivers():
for ep in pkg_resources.iter_entry_points(STORAGE_ENTRY_POINT)] for ep in pkg_resources.iter_entry_points(STORAGE_ENTRY_POINT)]
def driver_parameters(name):
''' Get __init__ parameters from a driver with out `self` & `name`. '''
init_function = qubes.utils.get_entry_point_one(
qubes.storage.STORAGE_ENTRY_POINT, name).__init__
params = init_function.func_code.co_varnames
ignored_params = ['self', 'name']
return [p for p in params if p not in ignored_params]
def isodate(seconds=time.time()): def isodate(seconds=time.time()):
''' Helper method which returns an iso date ''' ''' Helper method which returns an iso date '''
return datetime.utcfromtimestamp(seconds).isoformat("T") return datetime.utcfromtimestamp(seconds).isoformat("T")

View File

@ -45,6 +45,7 @@ import unittest
from distutils import spawn from distutils import spawn
import lxml.etree import lxml.etree
import pkg_resources
import qubes.backup import qubes.backup
import qubes.config import qubes.config
@ -143,12 +144,22 @@ class TestEmitter(qubes.events.Emitter):
def fire_event(self, event, **kwargs): def fire_event(self, event, **kwargs):
effects = super(TestEmitter, self).fire_event(event, **kwargs) effects = super(TestEmitter, self).fire_event(event, **kwargs)
self.fired_events[(event, tuple(kwargs.items()))] += 1 ev_kwargs = frozenset(
(key,
frozenset(value.items()) if isinstance(value, dict) else value)
for key, value in kwargs.items()
)
self.fired_events[(event, ev_kwargs)] += 1
return effects return effects
def fire_event_pre(self, event, **kwargs): def fire_event_pre(self, event, **kwargs):
effects = super(TestEmitter, self).fire_event_pre(event, **kwargs) effects = super(TestEmitter, self).fire_event_pre(event, **kwargs)
self.fired_events[(event, tuple(kwargs.items()))] += 1 ev_kwargs = frozenset(
(key,
frozenset(value.items()) if isinstance(value, dict) else value)
for key, value in kwargs.items()
)
self.fired_events[(event, ev_kwargs)] += 1
return effects return effects
def expectedFailureIfTemplate(templates): def expectedFailureIfTemplate(templates):
@ -894,6 +905,7 @@ def load_tests(loader, tests, pattern): # pylint: disable=unused-argument
'qubes.tests.vm.adminvm', 'qubes.tests.vm.adminvm',
'qubes.tests.app', 'qubes.tests.app',
'qubes.tests.tarwriter', 'qubes.tests.tarwriter',
'qubes.tests.mgmt',
'qubes.tests.tools.qvm_device', 'qubes.tests.tools.qvm_device',
'qubes.tests.tools.qvm_firewall', 'qubes.tests.tools.qvm_firewall',
'qubes.tests.tools.qvm_ls', 'qubes.tests.tools.qvm_ls',

1111
qubes/tests/mgmt.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -340,20 +340,6 @@ class TC_90_QubesVM(QubesVMTestsMixin,qubes.tests.QubesTestCase):
vm = self.get_vm() vm = self.get_vm()
self._test_generic_bool_property(vm, 'include_in_backups', True) self._test_generic_bool_property(vm, 'include_in_backups', True)
def test_240_firewall_conf(self):
vm = self.get_vm()
self.assertPropertyDefaultValue(vm, 'firewall_conf', 'firewall.xml')
self.assertPropertyValue(vm, 'firewall_conf', 'other.xml',
'other.xml', 'other.xml')
del vm.firewall_conf
self.assertPropertyDefaultValue(vm, 'firewall_conf',
'firewall.xml')
@unittest.expectedFailure
def test_241_firewall_conf_invalid(self):
vm = self.get_vm()
self.assertPropertyInvalidValue(vm, 'firewall_conf', None)
@qubes.tests.skipUnlessDom0 @qubes.tests.skipUnlessDom0
def test_250_kernel(self): def test_250_kernel(self):
kernels = os.listdir(os.path.join( kernels = os.listdir(os.path.join(

View File

@ -64,6 +64,8 @@ class QubesDaemonProtocol(asyncio.Protocol):
asyncio.ensure_future(self.respond( asyncio.ensure_future(self.respond(
src, method, dest, arg, untrusted_payload=untrusted_payload)) src, method, dest, arg, untrusted_payload=untrusted_payload))
return True
@asyncio.coroutine @asyncio.coroutine
def respond(self, src, method, dest, arg, *, untrusted_payload): def respond(self, src, method, dest, arg, *, untrusted_payload):
try: try:
@ -116,7 +118,8 @@ class QubesDaemonProtocol(asyncio.Protocol):
def send_response(self, content): def send_response(self, content):
self.send_header(0x30) self.send_header(0x30)
self.transport.write(content.encode('utf-8')) if content is not None:
self.transport.write(content.encode('utf-8'))
def send_event(self, subject, event, **kwargs): def send_event(self, subject, event, **kwargs):
self.send_header(0x31) self.send_header(0x31)

View File

@ -27,6 +27,7 @@
import datetime import datetime
import os import os
import re
import subprocess import subprocess
import sys import sys
import xml.parsers.expat import xml.parsers.expat
@ -40,6 +41,31 @@ import qubes.log
import qubes.tools.qvm_ls import qubes.tools.qvm_ls
def validate_name(holder, prop, value):
''' Check if value is syntactically correct VM name '''
if not isinstance(value, str):
raise TypeError('VM name must be string, {!r} found'.format(
type(value).__name__))
if len(value) > 31:
if holder is not None and prop is not None:
raise qubes.exc.QubesPropertyValueError(holder, prop, value,
'{} value must be shorter than 32 characters'.format(
prop.__name__))
else:
raise qubes.exc.QubesValueError(
'VM name must be shorter than 32 characters')
# this regexp does not contain '+'; if it had it, we should specifically
# disallow 'lost+found' #1440
if re.match(r"^[a-zA-Z][a-zA-Z0-9_-]*$", value) is None:
if holder is not None and prop is not None:
raise qubes.exc.QubesPropertyValueError(holder, prop, value,
'{} value contains illegal characters'.format(prop.__name__))
else:
raise qubes.exc.QubesValueError(
'VM name contains illegal characters')
class Features(dict): class Features(dict):
'''Manager of the features. '''Manager of the features.
@ -332,3 +358,11 @@ class VMProperty(qubes.property):
self.vmclass.__name__)) self.vmclass.__name__))
super(VMProperty, self).__set__(instance, vm) super(VMProperty, self).__set__(instance, vm)
def sanitize(self, *, untrusted_newvalue):
try:
untrusted_vmname = untrusted_newvalue.decode('ascii')
except UnicodeDecodeError:
raise qubes.exc.QubesValueError
validate_name(None, self, untrusted_vmname)
return untrusted_vmname

View File

@ -143,6 +143,9 @@ class AdminVM(qubes.vm.qubesvm.QubesVM):
''' '''
raise qubes.exc.QubesVMError(self, 'Cannot suspend Dom0 fake domain!') raise qubes.exc.QubesVMError(self, 'Cannot suspend Dom0 fake domain!')
@property
def icon_path(self):
return None
# def __init__(self, **kwargs): # def __init__(self, **kwargs):
# super(QubesAdminVm, self).__init__(qid=0, name="dom0", netid=0, # super(QubesAdminVm, self).__init__(qid=0, name="dom0", netid=0,

View File

@ -91,8 +91,10 @@ class NetVMMixin(qubes.events.Emitter):
doc='''If this domain can act as network provider (formerly known as doc='''If this domain can act as network provider (formerly known as
NetVM or ProxyVM)''') NetVM or ProxyVM)''')
firewall_conf = qubes.property('firewall_conf', type=str,
default='firewall.xml') @property
def firewall_conf(self):
return 'firewall.xml'
# #
# used in networked appvms or proxyvms (netvm is not None) # used in networked appvms or proxyvms (netvm is not None)
@ -330,7 +332,7 @@ class NetVMMixin(qubes.events.Emitter):
@qubes.events.handler('property-del:netvm') @qubes.events.handler('property-del:netvm')
def on_property_del_netvm(self, event, prop, oldvalue=None): def on_property_del_netvm(self, event, name, oldvalue=None):
''' Sets the the NetVM to default NetVM ''' ''' Sets the the NetVM to default NetVM '''
# pylint: disable=unused-argument # pylint: disable=unused-argument
# we are changing to default netvm # we are changing to default netvm

View File

@ -29,8 +29,8 @@ import base64
import datetime import datetime
import os import os
import os.path import os.path
import re
import shutil import shutil
import string
import subprocess import subprocess
import sys import sys
import time import time
@ -78,18 +78,8 @@ def _setter_qid(self, prop, value):
def _setter_name(self, prop, value): def _setter_name(self, prop, value):
''' Helper for setting the domain name ''' ''' Helper for setting the domain name '''
if not isinstance(value, str): qubes.vm.validate_name(self, prop, value)
raise TypeError('{} value must be string, {!r} found'.format(
prop.__name__, type(value).__name__))
if len(value) > 31:
raise ValueError('{} value must be shorter than 32 characters'.format(
prop.__name__))
# this regexp does not contain '+'; if it had it, we should specifically
# disallow 'lost+found' #1440
if re.match(r"^[a-zA-Z][a-zA-Z0-9_-]*$", value) is None:
raise ValueError('{} value contains illegal characters'.format(
prop.__name__))
if self.is_running(): if self.is_running():
raise qubes.exc.QubesVMNotHaltedError( raise qubes.exc.QubesVMNotHaltedError(
self, 'Cannot change name of running VM') self, 'Cannot change name of running VM')
@ -102,7 +92,7 @@ def _setter_name(self, prop, value):
pass pass
if value in self.app.domains: if value in self.app.domains:
raise qubes.exc.QubesValueError( raise qubes.exc.QubesPropertyValueError(self, prop, value,
'VM named {} alread exists'.format(value)) 'VM named {} alread exists'.format(value))
return value return value
@ -114,6 +104,9 @@ def _setter_kernel(self, prop, value):
if value is None: if value is None:
return value return value
value = str(value) value = str(value)
if '/' in value:
raise qubes.exc.QubesPropertyValueError(self, prop, value,
'Kernel name cannot contain \'/\'')
dirname = os.path.join( dirname = os.path.join(
qubes.config.system_path['qubes_base_dir'], qubes.config.system_path['qubes_base_dir'],
qubes.config.system_path['qubes_kernels_base_dir'], qubes.config.system_path['qubes_kernels_base_dir'],
@ -148,6 +141,16 @@ def _setter_positive_int(self, prop, value):
return value return value
def _setter_default_user(self, prop, value):
''' Helper for setting default user '''
value = str(value)
# specifically forbid: ':', ' ', ''', '"'
allowed_chars = string.ascii_letters + string.digits + '_-+,.'
if not all(c in allowed_chars for c in value):
raise qubes.exc.QubesPropertyValueError(self, prop, value,
'Username can contain only those characters: ' + allowed_chars)
return value
class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
'''Base functionality of Qubes VM shared between all VMs. '''Base functionality of Qubes VM shared between all VMs.
@ -418,10 +421,6 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
ls_width=2, ls_width=2,
doc='FIXME') doc='FIXME')
pool_name = qubes.property('pool_name',
default='default',
doc='storage pool for this qube devices')
# CORE2: swallowed uses_default_kernel # CORE2: swallowed uses_default_kernel
kernel = qubes.property('kernel', type=str, kernel = qubes.property('kernel', type=str,
setter=_setter_kernel, setter=_setter_kernel,
@ -448,6 +447,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
default_user = qubes.property('default_user', type=str, default_user = qubes.property('default_user', type=str,
default=(lambda self: self.template.default_user default=(lambda self: self.template.default_user
if hasattr(self, 'template') else 'user'), if hasattr(self, 'template') else 'user'),
setter=_setter_default_user,
ls_width=12, ls_width=12,
doc='FIXME') doc='FIXME')
@ -489,6 +489,13 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
default=(lambda self: self.app.default_dispvm), default=(lambda self: self.app.default_dispvm),
doc='Default VM to be used as Disposable VM for service calls.') doc='Default VM to be used as Disposable VM for service calls.')
updateable = qubes.property('updateable',
default=(lambda self: not hasattr(self, 'template')),
type=bool,
setter=qubes.property.forbidden,
doc='True if this machine may be updated on its own.')
# #
# static, class-wide properties # static, class-wide properties
# #
@ -576,12 +583,6 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
self._qdb_connection = qubesdb.QubesDB(self.name) self._qdb_connection = qubesdb.QubesDB(self.name)
return self._qdb_connection return self._qdb_connection
# XXX shouldn't this go elsewhere?
@property
def updateable(self):
'''True if this machine may be updated on its own.'''
return not hasattr(self, 'template')
@property @property
def dir_path(self): def dir_path(self):
'''Root directory for files related to this domain''' '''Root directory for files related to this domain'''
@ -707,7 +708,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
self.app.pools[vm_pool.name] = vm_pool self.app.pools[vm_pool.name] = vm_pool
@qubes.events.handler('property-set:label') @qubes.events.handler('property-set:label')
def on_property_set_label(self, event, name, new_label, old_label=None): def on_property_set_label(self, event, name, newvalue, oldvalue=None):
# pylint: disable=unused-argument # pylint: disable=unused-argument
if self.icon_path: if self.icon_path:
try: try:
@ -715,10 +716,10 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
except OSError: except OSError:
pass pass
if hasattr(os, "symlink"): if hasattr(os, "symlink"):
os.symlink(new_label.icon_path, self.icon_path) os.symlink(newvalue.icon_path, self.icon_path)
subprocess.call(['sudo', 'xdg-icon-resource', 'forceupdate']) subprocess.call(['sudo', 'xdg-icon-resource', 'forceupdate'])
else: else:
shutil.copy(new_label.icon_path, self.icon_path) shutil.copy(newvalue.icon_path, self.icon_path)
@qubes.events.handler('property-pre-set:name') @qubes.events.handler('property-pre-set:name')
def on_property_pre_set_name(self, event, name, newvalue, oldvalue=None): def on_property_pre_set_name(self, event, name, newvalue, oldvalue=None):
@ -741,11 +742,11 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
'qubes-vm@{}.service'.format(oldvalue)]) 'qubes-vm@{}.service'.format(oldvalue)])
@qubes.events.handler('property-set:name') @qubes.events.handler('property-set:name')
def on_property_set_name(self, event, name, new_name, old_name=None): def on_property_set_name(self, event, name, newvalue, oldvalue=None):
# pylint: disable=unused-argument # pylint: disable=unused-argument
self.init_log() self.init_log()
self.storage.rename(old_name, new_name) self.storage.rename(oldvalue, newvalue)
if self._libvirt_domain is not None: if self._libvirt_domain is not None:
self.libvirt_domain.undefine() self.libvirt_domain.undefine()
@ -760,11 +761,11 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
self.autostart = self.autostart self.autostart = self.autostart
@qubes.events.handler('property-pre-set:autostart') @qubes.events.handler('property-pre-set:autostart')
def on_property_pre_set_autostart(self, event, prop, value, def on_property_pre_set_autostart(self, event, prop, newvalue,
oldvalue=None): oldvalue=None):
# pylint: disable=unused-argument # pylint: disable=unused-argument
# workaround https://bugzilla.redhat.com/show_bug.cgi?id=1181922 # workaround https://bugzilla.redhat.com/show_bug.cgi?id=1181922
if value: if newvalue:
retcode = subprocess.call( retcode = subprocess.call(
["sudo", "ln", "-sf", ["sudo", "ln", "-sf",
"/usr/lib/systemd/system/qubes-vm@.service", "/usr/lib/systemd/system/qubes-vm@.service",

View File

@ -317,6 +317,7 @@ fi
%{python3_sitelib}/qubes/tests/events.py %{python3_sitelib}/qubes/tests/events.py
%{python3_sitelib}/qubes/tests/firewall.py %{python3_sitelib}/qubes/tests/firewall.py
%{python3_sitelib}/qubes/tests/init.py %{python3_sitelib}/qubes/tests/init.py
%{python3_sitelib}/qubes/tests/mgmt.py
%{python3_sitelib}/qubes/tests/storage.py %{python3_sitelib}/qubes/tests/storage.py
%{python3_sitelib}/qubes/tests/storage_file.py %{python3_sitelib}/qubes/tests/storage_file.py
%{python3_sitelib}/qubes/tests/storage_lvm.py %{python3_sitelib}/qubes/tests/storage_lvm.py

View File

@ -1,10 +1,10 @@
#!/bin/sh #!/bin/sh
: "${PYTHON:=python}" : "${PYTHON:=python3}"
: "${TESTPYTHONPATH:=test-packages}" : "${TESTPYTHONPATH:=test-packages}"
PYTHONPATH="${TESTPYTHONPATH}:${PYTHONPATH}" PYTHONPATH="${TESTPYTHONPATH}:${PYTHONPATH}"
export PYTHONPATH export PYTHONPATH
"${PYTHON}" setup.py egg_info --egg-base "${TESTPYTHONPATH}" "${PYTHON}" setup.py egg_info --egg-base "${TESTPYTHONPATH}"
"${PYTHON}" -m qubes.tests.run "$@" "${PYTHON}" -m coverage run --rcfile=ci/coveragerc -m qubes.tests.run "$@"

View File

@ -13,3 +13,11 @@ class libvirtError(Exception):
def openReadOnly(*args, **kwargs): def openReadOnly(*args, **kwargs):
raise libvirtError('mock module, always raises') raise libvirtError('mock module, always raises')
VIR_DOMAIN_BLOCKED = 2
VIR_DOMAIN_RUNNING = 1
VIR_DOMAIN_PAUSED = 3
VIR_DOMAIN_SHUTDOWN = 4
VIR_DOMAIN_SHUTOFF = 5
VIR_DOMAIN_CRASHED = 6
VIR_DOMAIN_PMSUSPENDED = 7