diff --git a/doc/qubes-vm/index.rst b/doc/qubes-vm/index.rst index 297cf15d..b136c1dd 100644 --- a/doc/qubes-vm/index.rst +++ b/doc/qubes-vm/index.rst @@ -1,15 +1,59 @@ :py:mod:`qubes.vm` -- Different Virtual Machine types ===================================================== +Qubes is composed of several virtual machines that are interconnected in +several ways. From now on they will be called „domains”, as they may not +actually be true virtual machines -- we plan to support LXC containers for +example. Because of Xen-only legacy of Qubes code, it is custom to refer to them +in long/plural as ``domains`` and in short/singular as ``vm``. + + +Domain object +------------- + +There are couple of programming objects that refer to domain. The main is the +instance of :py:class:`qubes.vm.QubesVM`. This is the main „porcelain” object, +which carries other objects and supplies convenience methods like +:py:meth:`qubes.vm.qubesvm.QubesVM.start`. This class is actually divided in +two, the :py:class:`qubes.vm.qubesvm.QubesVM` cares about Qubes-specific +actions, that are more or less directly related to security model. It is +intended to be easily auditable by non-expert programmers (ie. we don't use +Python's magic there). The second class is its parent, +:py:class:`qubes.vm.BaseVM`, which is concerned about technicalities like XML +serialising/deserialising. It is of less concern to threat model auditors, but +still relevant to overall security of the Qubes OS. It is written for +programmers by programmers. + +The second object is the XML node that refers to the domain. It can be accessed +as :py:attr:`Qubes.vm.BaseVM.xml` attribute of the domain object. The third one +is :py:attr:`Qubes.vm.qubesvm.QubesVM.libvirt_domain` object for directly +interacting with libvirt. Those objects are intended to be used from core and/or +plugins, but not directly by user or from qvm-tools. They are however public, so +there are no restrictions. + + +Domain classes +-------------- + +There are several different types of VM, because not every Qubes domain is equal +-- some of them perform specific functions, like NetVM; others have different +life cycle, like DisposableVM. For that, different domains have different Python +classes. They are all defined in this package, generally one class per module, +but some modules contain private globals that serve this particular class. + + +Package contents +---------------- + Main public classes -------------------- +^^^^^^^^^^^^^^^^^^^ .. autoclass:: qubes.vm.BaseVM :members: :show-inheritance: Helper classes and functions ----------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: qubes.vm.BaseVMMeta :members: @@ -24,7 +68,7 @@ Helper classes and functions :show-inheritance: Particular VM classes ---------------------- +^^^^^^^^^^^^^^^^^^^^^ Main types: diff --git a/doc/qubes.rst b/doc/qubes.rst index b4616ddd..955870a8 100644 --- a/doc/qubes.rst +++ b/doc/qubes.rst @@ -1,6 +1,218 @@ :py:mod:`qubes` -- Common concepts ================================== +Global Qubes object +------------------- + +Because all objects in Qubes' world are interconnected, there is no possibility +to instantiate them separately. They are all loaded together and contained in +the one ``app`` object, an instance of :py:class:`qubes.Qubes` class. + +The loading from XML is done in stages, because Qubes domains are dependent on +each other in what can be even a circular dependency. Therefore some properties +(especcialy those that refer to another domains) are loaded later. Refer to +:py:class:`qubes.Qubes` class documentation to get description of every stage. + + +Properties +---------- + +Many parameters of Qubes can be changed -- from names of particular domains to +default NetVM for all AppVMs. All of those *configurable* parameters are called +*properties* and can be accessed like Python attributes on their owners:: + + >>> import qubes + >>> app = qubes.Qubes() + >>> app.domain[0] # docutils: +ELLIPSIS + + >>> app.domain[0].name + 'dom0' + +Definition +^^^^^^^^^^ +Properties are defined on global :py:class:`qubes.Qubes` application object and +on every domain. Those classess inherit from :py:class:`PropertyHolder` class, +which is responsible for operation of properties. + +Each Qubes property is actually a *data descriptor* (a Python term), which means +they are attributes of their classess, but when trying to access it from +*instance*, they return it's underlying value instead. They can be thought of as +Python's builtin :py:class:`property`, but greatly enhanced. They are defined in +definition of their class:: + + >>> import qubes + >>> class MyTestHolder(qubes.PropertyHolder): + >>> testprop = qubes.property('testprop') + >>> instance = MyTestHolder() + >>> instance.testprop = 'aqq' + >>> instance.testprop + 'aqq' + +If you like to access some attributes of the property *itself*, you should refer +to instance's class:: + + >>> import qubes + >>> class MyTestHolder(qubes.PropertyHolder): + >>> testprop = qubes.property('testprop') + >>> instance = MyTestHolder() + >>> instance.testprop = 'aqq' + + >>> type(instance.testprop) + + >>> type(instance.__class__.testprop) + + + >>> instance.__class__.testprop.__name__ + 'testprop' + +As a rule, properties are intended to be serialised and deserialised to/from XML +file. There are very few exceptions, but if you don't intend to save the +property to XML, you should generally go for builtin :py:class:`property`. + +One important difference from builtin properties is that there is no getter +function, only setter. In other words, they are not dynamic, you cannot return +different value every time something wants to access it. This is to ensure that +while saving the property is not a moving target. + + +Property's properties +^^^^^^^^^^^^^^^^^^^^^ +You can specify some parameters while defining the property. The most important +is the `type`: on setting, the value is coerced to this type. It is well suited +to builtin types like :py:class:`int`:: + + >>> import qubes + >>> class MyTestHolder(qubes.PropertyHolder): + >>> testprop = qubes.property('testprop') + >>> intprop = qubes.property('intprop', type=int) + + >>> instance = MyTestHolder() + >>> instance.testprop = '123' + >>> instance.intprop = '123' + + >>> instance.testprop + '123' + >>> instance.intprop + 123 + + +Every property should be documented. You should add a short description to your +property, which will appear, among others, in :program:`qvm-prefs` and +:program:`qvm-ls` programs. It should not use any Sphinx-specific markup:: + + >>> import qubes + >>> class MyTestHolder(qubes.PropertyHolder): + >>> testprop = qubes.property('testprop', + >>> doc='My new and shiny property.') + >>> MyTestHolder.testprop.__doc__ + 'My new and shiny property.' + + +In addition to `type`, properties also support `setter` parameter. It acts +similar to `type`, but is always executed (not only when types don't agree) and +accepts more parameters: `self`, `prop` and `value` being respectively: owners' +instance, property's instance and the value being set. There is also `saver`, +which does reverse: given value of the property it should return a string that +can be parsed by `saver`. + + +Unset properties and default values +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Properties may be unset, even if they are defined (that is, on access they raise +:py:exc:`AttributeError` -- that is the normal Python way to tell that the +attribute is absent). You can manually unset a property using Python's ``del`` +statement:: + + >>> import qubes + >>> class MyTestHolder(qubes.PropertyHolder): + >>> testprop = qubes.property('testprop') + >>> instance = MyTestHolder() + >>> instance.testprop + AttributeError: ... + >>> instance.testprop = 123 + >>> instance.testprop + 123 + >>> del instance.testprop + >>> instance.testprop + AttributeError: ... + +Alternatively, some properties may return some other value instead, if that's +the reasonable thing to do. For example, when +:py:attr:`qubes.vm.qubesvm.QubesVM.netvm` is unset, we check global setting +:py:attr:`qubes.Qubes.default_netvm` instead. Returning :py:obj:`None` as +default would be wrong, as it is marker that means „no NetVM, machine +disconnected”. + +You can define a default value either as constant or as a callable. In the +second case, the callable should accept one argument, the instance that owns the +property:: + + >>> import qubes + >>> class MyTestHolder(qubes.PropertyHolder): + >>> testprop = qubes.property('testprop') + >>> def1prop = qubes.property('testprop', default=123) + >>> netvm = qubes.property('testprop', + >>> default=(lambda self: self.app.default_netvm)) + + >>> instance = MyTestHolder() + >>> instance.testprop + AttributeError: ... + >>> instance.def1prop + 123 + >>> instance.netvm # doctest: +SKIP + + + +Setting netvm on particular domain of course does not affect global default, but +only this instance. But there are two problems: + +- You don't know if the value of the property you just accessed was it's + true or default value. +- After ``del``'ing a property, you still will get a value on access. You + cannot count on `AttributeError` raised from them. + +Therefore Qubes support alternative semantics. You can (and probably should, +wherever applicable) use no ``del``, but assignment of special magic object +:py:obj:`qubes.property.DEFAULT`. There is also method +:py:meth:`qubes.PropertyHolder.property_is_default`, which can be used to +distinguish unset from set properties:: + + >>> import qubes + >>> class MyTestHolder(qubes.PropertyHolder): + >>> testprop = qubes.property('testprop', default=123) + >>> instance.testprop + 123 + >>> instance.property_is_default('testprop') + True + >>> instance.testprop = 123 + >>> instance.testprop + >>> instance.property_is_default('testprop') + False + >>> instance.testprop = qubes.property.DEFAULT + >>> instance.property_is_default('testprop') + True + + +Inheritance +^^^^^^^^^^^ +Properties in subclassess overload properties from their parents, like +expected:: + + >>> import qubes + >>> class MyTestHolder(qubes.PropertyHolder): + >>> testprop = qubes.property('testprop') + + >>> class MyOtherHolder(MyTestHolder): + >>> testprop = qubes.property('testprop', setter=qubes.property.forbidden) + + >>> instance = MyOtherHolder() + >>> instane.testprop = 123 + TypeError: ... + + +Module contents +--------------- + .. automodule:: qubes :members: :show-inheritance: diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index 1afb4b7e..2b0064d8 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -153,7 +153,7 @@ class BaseVM(qubes.PropertyHolder): #: dictionary of services that are run on this domain self.services = services or {} - #: :py:class`DeviceManager` object keeping devices that are attached to + #: :py:class:`DeviceManager` object keeping devices that are attached to #: this domain self.devices = DeviceManager(self) if devices is None else devices @@ -294,7 +294,7 @@ class BaseVM(qubes.PropertyHolder): :param str ip: IP address of the frontend :param str mac: MAC (Ethernet) address of the frontend - :param qubes.vm.QubesVM backend: Backend domain + :param qubes.vm.qubesvm.QubesVM backend: Backend domain :rtype: lxml.etree._Element ''' @@ -314,7 +314,7 @@ class BaseVM(qubes.PropertyHolder): :param str ip: IP address of the frontend :param str mac: MAC (Ethernet) address of the frontend - :param qubes.vm.QubesVM backend: Backend domain + :param qubes.vm.qubesvm.QubesVM backend: Backend domain :rtype: lxml.etree._Element ''' diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index 3793d839..6a283d97 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -935,7 +935,7 @@ class QubesVM(qubes.vm.BaseVM): **passio_popen** and **input** are mutually exclusive. :param str service: service name - :param qubes.vm.QubesVM: source domain as presented to this VM + :param qubes.vm.qubesvm.QubesVM: source domain as presented to this VM :param str user: username to run service as :param bool passio_popen: passed verbatim to :py:meth:`run` :param str input: string passed as input to service @@ -1117,7 +1117,7 @@ class QubesVM(qubes.vm.BaseVM): def clone_disk_files(self, src): '''Clone files from other vm. - :param qubes.vm.QubesVM src: source VM + :param qubes.vm.qubesvm.QubesVM src: source VM ''' if src.is_running(): @@ -1278,11 +1278,12 @@ class QubesVM(qubes.vm.BaseVM): } .. seealso:: + http://wiki.libvirt.org/page/VM_lifecycle Description of VM life cycle from the point of view of libvirt. https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainState - Libvirt API for changing state of a domain. + Libvirt's enum describing precise state of a domain. ''' # pylint: disable=too-many-return-statements libvirt_domain = self.libvirt_domain