ef4f00dac0
collections.defaultdict was not enough, because it cannot pass any arguments to factory. We need to pass domain object and device class to fire events on attach and detach.
327 lines
8.8 KiB
Python
327 lines
8.8 KiB
Python
#!/usr/bin/python2 -O
|
|
|
|
'''Qubes Virtual Machines
|
|
|
|
Main public classes
|
|
-------------------
|
|
|
|
.. autoclass:: BaseVM
|
|
:members:
|
|
:show-inheritance:
|
|
|
|
Helper classes and functions
|
|
----------------------------
|
|
|
|
.. autoclass:: BaseVMMeta
|
|
:members:
|
|
:show-inheritance:
|
|
|
|
Particular VM classes
|
|
---------------------
|
|
|
|
Main types:
|
|
|
|
.. toctree::
|
|
:maxdepth: 1
|
|
|
|
qubesvm
|
|
appvm
|
|
templatevm
|
|
|
|
Special VM types:
|
|
|
|
.. toctree::
|
|
:maxdepth: 1
|
|
|
|
netvm
|
|
proxyvm
|
|
dispvm
|
|
adminvm
|
|
|
|
HVMs:
|
|
|
|
.. toctree::
|
|
:maxdepth: 1
|
|
|
|
hvm
|
|
templatehvm
|
|
|
|
'''
|
|
|
|
import ast
|
|
import collections
|
|
import functools
|
|
import sys
|
|
|
|
import dateutil.parser
|
|
import lxml.etree
|
|
|
|
import qubes
|
|
import qubes.events
|
|
import qubes.plugins
|
|
|
|
|
|
class BaseVMMeta(qubes.plugins.Plugin, qubes.events.EmitterMeta):
|
|
'''Metaclass for :py:class:`.BaseVM`'''
|
|
def __init__(cls, name, bases, dict_):
|
|
super(BaseVMMeta, cls).__init__(name, bases, dict_)
|
|
cls.__hooks__ = collections.defaultdict(list)
|
|
|
|
|
|
class DeviceCollection(object):
|
|
'''Bag for devices.
|
|
|
|
Used as default value for :py:meth:`DeviceManager.__missing__` factory.
|
|
|
|
:param vm: VM for which we manage devices
|
|
:param class_: device class
|
|
'''
|
|
|
|
def __init__(self, vm, class_):
|
|
self._vm = vm
|
|
self._class = class_
|
|
self._set = set()
|
|
|
|
|
|
def attach(self, device):
|
|
'''Attach (add) device to domain.
|
|
|
|
:param str device: device identifier (format is class-dependent)
|
|
'''
|
|
|
|
if device in self:
|
|
raise KeyError(
|
|
'device {!r} of class {} already attached to {!r}'.format(
|
|
device, self._class, self._vm))
|
|
self._vm.fire_event('device-pre-attached:{}'.format(self._class), device)
|
|
self._set.add(device)
|
|
self._vm.fire_event('device-attached:{}'.format(self._class), device)
|
|
|
|
|
|
def detach(self, device):
|
|
'''Detach (remove) device from domain.
|
|
|
|
:param str device: device identifier (format is class-dependent)
|
|
'''
|
|
|
|
if device not in self:
|
|
raise KeyError(
|
|
'device {!r} of class {} not attached to {!r}'.format(
|
|
device, self._class, self._vm))
|
|
self._vm.fire_event('device-pre-detached:{}'.format(self._class), device)
|
|
self._set.remove(device)
|
|
self._vm.fire_event('device-detached:{}'.format(self._class), device)
|
|
|
|
|
|
def __iter__(self):
|
|
return iter(self._set)
|
|
|
|
|
|
def __contains__(self, item):
|
|
return item in self._set
|
|
|
|
|
|
class DeviceManager(dict):
|
|
'''Device manager that hold all devices by their classess.
|
|
|
|
:param vm: VM for which we manage devices
|
|
'''
|
|
|
|
def __init__(self, vm):
|
|
super(DeviceManager, self).__init__()
|
|
self._vm = vm
|
|
|
|
def __missing__(self, key):
|
|
return DeviceCollection(self._vm, key)
|
|
|
|
|
|
class BaseVM(qubes.PropertyHolder):
|
|
'''Base class for all VMs
|
|
|
|
:param app: Qubes application context
|
|
:type app: :py:class:`qubes.Qubes`
|
|
:param xml: xml node from which to deserialise
|
|
:type xml: :py:class:`lxml.etree._Element` or :py:obj:`None`
|
|
|
|
This class is responsible for serialising and deserialising machines and
|
|
provides basic framework. It contains no management logic. For that, see
|
|
:py:class:`qubes.vm.qubesvm.QubesVM`.
|
|
'''
|
|
|
|
__metaclass__ = BaseVMMeta
|
|
|
|
def __init__(self, app, xml, load_stage=2, services={}, devices=None,
|
|
tags={}, *args, **kwargs):
|
|
self.app = app
|
|
self.services = services
|
|
self.devices = DeviceManager(self) if devices is None else devices
|
|
self.tags = tags
|
|
|
|
self.events_enabled = False
|
|
all_names = set(prop.__name__ for prop in self.get_props_list(load_stage=2))
|
|
for key in list(kwargs.keys()):
|
|
if not key in all_names:
|
|
raise AttributeError(
|
|
'No property {!r} found in {!r}'.format(
|
|
key, self.__class__))
|
|
setattr(self, key, kwargs[key])
|
|
del kwargs[key]
|
|
|
|
super(BaseVM, self).__init__(xml, *args, **kwargs)
|
|
|
|
self.events_enabled = True
|
|
self.fire_event('property-load')
|
|
|
|
|
|
def add_new_vm(self, vm):
|
|
'''Add new Virtual Machine to colletion
|
|
|
|
'''
|
|
|
|
vm_cls = QubesVmClasses[vm_type]
|
|
if 'template' in kwargs:
|
|
if not vm_cls.is_template_compatible(kwargs['template']):
|
|
raise QubesException("Template not compatible with selected "
|
|
"VM type")
|
|
|
|
vm = vm_cls(qid=qid, collection=self, **kwargs)
|
|
if not self.verify_new_vm(vm):
|
|
raise QubesException("Wrong VM description!")
|
|
self[vm.qid] = vm
|
|
|
|
# make first created NetVM the default one
|
|
if self.default_fw_netvm_qid is None and vm.is_netvm():
|
|
self.set_default_fw_netvm(vm)
|
|
|
|
if self.default_netvm_qid is None and vm.is_proxyvm():
|
|
self.set_default_netvm(vm)
|
|
|
|
# make first created TemplateVM the default one
|
|
if self.default_template_qid is None and vm.is_template():
|
|
self.set_default_template(vm)
|
|
|
|
# make first created ProxyVM the UpdateVM
|
|
if self.updatevm_qid is None and vm.is_proxyvm():
|
|
self.set_updatevm_vm(vm)
|
|
|
|
# by default ClockVM is the first NetVM
|
|
if self.clockvm_qid is None and vm.is_netvm():
|
|
self.set_clockvm_vm(vm)
|
|
|
|
return vm
|
|
|
|
@classmethod
|
|
def fromxml(cls, app, xml, load_stage=2):
|
|
'''Create VM from XML node
|
|
|
|
:param qubes.Qubes app: :py:class:`qubes.Qubes` application instance
|
|
:param lxml.etree._Element xml: XML node reference
|
|
:param int load_stage: do not change the default (2) unless you know, what you are doing
|
|
'''
|
|
|
|
# sys.stderr.write('{}.fromxml(app={!r}, xml={!r}, load_stage={})\n'.format(
|
|
# cls.__name__, app, xml, load_stage))
|
|
if xml is None:
|
|
return cls(app)
|
|
|
|
services = {}
|
|
devices = collections.defaultdict(list)
|
|
tags = {}
|
|
|
|
# services
|
|
for node in xml.xpath('./services/service'):
|
|
services[node.text] = bool(ast.literal_eval(node.get('enabled', 'True')))
|
|
|
|
# devices (pci, usb, ...)
|
|
for parent in xml.xpath('./devices'):
|
|
devclass = parent.get('class')
|
|
for node in parent.xpath('./device'):
|
|
devices[devclass].append(node.text)
|
|
|
|
# tags
|
|
for node in xml.xpath('./tags/tag'):
|
|
tags[node.get('name')] = node.text
|
|
|
|
# properties
|
|
self = cls(app, xml=xml, services=services, devices=devices, tags=tags)
|
|
self.load_properties(load_stage=load_stage)
|
|
|
|
# TODO: firewall, policy
|
|
|
|
# sys.stderr.write('{}.fromxml return\n'.format(cls.__name__))
|
|
return self
|
|
|
|
|
|
def __xml__(self):
|
|
element = lxml.etree.Element('domain', id='domain-' + str(self.qid))
|
|
|
|
element.append(self.save_properties())
|
|
|
|
services = lxml.etree.Element('services')
|
|
for service in self.services:
|
|
node = lxml.etree.Element('service')
|
|
node.text = service
|
|
if not self.services[service]:
|
|
node.set('enabled', 'False')
|
|
services.append(node)
|
|
element.append(services)
|
|
|
|
for devclass in self.devices:
|
|
devices = lxml.etree.Element('devices')
|
|
devices.set('class', devclass)
|
|
for device in self.devices[devclass]:
|
|
node = lxml.etree.Element('device')
|
|
node.text = device
|
|
devices.append(node)
|
|
element.append(devices)
|
|
|
|
tags = lxml.etree.Element('tags')
|
|
for tag in self.tags:
|
|
node = lxml.etree.Element('tag', name=tag)
|
|
node.text = self.tags[tag]
|
|
tags.append(node)
|
|
element.append(tags)
|
|
|
|
return element
|
|
|
|
def __repr__(self):
|
|
return '<{} object at {:#x} {}>'.format(
|
|
self.__class__.__name__, id(self),
|
|
' '.join('{}={}'.format(prop.__name__, getattr(self, prop.__name__))
|
|
for prop in self.get_props_list()))
|
|
|
|
|
|
@classmethod
|
|
def add_hook(cls, event, f):
|
|
'''Add hook to entire VM class and all subclasses
|
|
|
|
:param str event: event type
|
|
:param callable f: function to fire on event
|
|
|
|
Prototype of the function depends on the exact type of event. Classes
|
|
which inherit from this class will also inherit the hook.
|
|
'''
|
|
|
|
cls.__hooks__[event].append(f)
|
|
|
|
|
|
def fire_hooks(self, event, *args, **kwargs):
|
|
'''Fire hooks associated with an event
|
|
|
|
:param str event: event type
|
|
|
|
*args* and *kwargs* are passed to each function
|
|
'''
|
|
|
|
for cls in self.__class__.__mro__:
|
|
if not hasattr(cls, '__hooks__'): continue
|
|
for hook in cls.__hooks__[event]:
|
|
hook(self, *args, **kwargs)
|
|
|
|
|
|
def load(class_, D):
|
|
cls = BaseVM[class_]
|
|
return cls(D)
|
|
|
|
__all__ = qubes.plugins.load(__file__)
|