306 lines
8.2 KiB
Python
306 lines
8.2 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):
|
|
proprepr = []
|
|
for prop in self.get_props_list():
|
|
try:
|
|
proprepr.append('{}={!r}'.format(
|
|
prop.__name__, getattr(self, prop.__name__)))
|
|
except AttributeError:
|
|
continue
|
|
|
|
return '<{} object at {:#x} {}>'.format(
|
|
self.__class__.__name__, id(self), ' '.join(proprepr))
|
|
|
|
|
|
|
|
def load(class_, D):
|
|
cls = BaseVM[class_]
|
|
return cls(D)
|
|
|
|
__all__ = qubes.plugins.load(__file__)
|