core-admin/qubes/vm/__init__.py
Wojtek Porczyk ef4f00dac0 qubes/vm: DeviceManager class for herding devices
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.
2015-06-29 17:39:24 +02:00

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__)