bc26e74339
Add an API for VMs to announce support for non-service features. This is very similar to supported-service.* features, but applies to non-service features. This may be also used for announcing support for features that do not use qvm-features framework itself - for example some VM kernel features, installed drivers, packages etc. QubesOS/qubes-issues#6030
219 lines
9.0 KiB
ReStructuredText
219 lines
9.0 KiB
ReStructuredText
:py:mod:`qubes.features` - Qubes VM features, services
|
|
======================================================
|
|
|
|
Features are generic mechanism for storing key-value pairs attached to a
|
|
VM. The primary use case for them is data storage for extensions (you can think
|
|
of them as more flexible properties, defined by extensions), but some are also
|
|
used in the qubes core itself. There is no definite list of supported features,
|
|
each extension can set their own and there is no requirement of registration,
|
|
but :program:`qvm-features` man page contains well known ones.
|
|
In addition, there is a mechanism for VM request setting a feature. This is
|
|
useful for extensions to discover if its VM part is present.
|
|
|
|
Features can have three distinct values: no value (not present in mapping,
|
|
which is closest thing to :py:obj:`None`), empty string (which is
|
|
interpreted as :py:obj:`False`) and non-empty string, which is
|
|
:py:obj:`True`. Anything assigned to the mapping is coerced to strings,
|
|
however if you assign instances of :py:class:`bool`, they are converted as
|
|
described above. Be aware that assigning the number `0` (which is considered
|
|
false in Python) will result in string `'0'`, which is considered true.
|
|
|
|
:py:class:`qubes.features.Features` inherits from :py:class:`dict`, so provide
|
|
all the standard functions to get, list and set values. Additionally provide
|
|
helper functions to check if given feature is set on the VM and default to the
|
|
value on the VM's template or netvm. This is useful for features which nature is
|
|
inherited from other VMs, like "is package X is installed" or "is VM behind
|
|
a VPN".
|
|
|
|
Example usage of features in extension:
|
|
|
|
.. code-block:: python
|
|
|
|
import qubes.exc
|
|
import qubes.ext
|
|
|
|
class ExampleExtension(qubes.ext.Extension):
|
|
@qubes.ext.handler('domain-pre-start')
|
|
def on_domain_start(self, vm, event, **kwargs):
|
|
if vm.features.get('do-not-start', False):
|
|
raise qubes.exc.QubesVMError(vm,
|
|
'Start prohibited because of do-not-start feature')
|
|
|
|
if vm.features.check_with_template('something-installed', False):
|
|
# do something
|
|
|
|
The above extension does two things:
|
|
|
|
- prevent starting a qube with ``do-not-start`` feature set
|
|
- do something when ``something-installed`` feature is set on the qube, or its
|
|
template
|
|
|
|
|
|
qvm-features-request, qubes.PostInstall service
|
|
------------------------------------------------
|
|
|
|
When some package in the VM want to request feature to be set (aka advertise
|
|
support for it), it should place a shell script in ``/etc/qubes/post-install.d``.
|
|
This script should call :program:`qvm-features-request` with ``FEATURE=VALUE``
|
|
pair(s) as arguments to request those features. It is recommended to use very
|
|
simple values here (for example ``1``). The script should be named in form
|
|
``XX-package-name.sh`` where ``XX`` is two-digits number below 90 and
|
|
``package-name`` is unique name specific to this package (preferably actual
|
|
package name). The script needs executable bit set.
|
|
|
|
``qubes.PostInstall`` service will call all those scripts after any package
|
|
installation and also after initial template installation.
|
|
This way package have a chance to report to dom0 if any feature is
|
|
added/removed.
|
|
|
|
The features flow to dom0 according to the diagram below. Important part is that
|
|
qubes core :py:class:`qubes.ext.Extension` is responsible for handling such
|
|
request in ``features-request`` event handler. If no extension handles given
|
|
feature request, it will be ignored. The extension should carefuly validate
|
|
requested features (ignoring those not recognized - may be for another
|
|
extension) and only then set appropriate value on VM object
|
|
(:py:attr:`qubes.vm.BaseVM.features`). It is recommended to make the
|
|
verification code as bulletproof as possible (for example allow only specific
|
|
simple values, instead of complex structures), because feature requests come
|
|
from untrusted sources. The features actually set on the VM in some cases may
|
|
not be necessary those requested. Similar for values.
|
|
|
|
.. graphviz::
|
|
|
|
digraph {
|
|
|
|
"qubes.PostInstall";
|
|
"/etc/qubes/post-install.d/ scripts";
|
|
"qvm-features-request";
|
|
"qubes.FeaturesRequest";
|
|
"qubes core extensions";
|
|
"VM features";
|
|
|
|
"qubes.PostInstall" -> "/etc/qubes/post-install.d/ scripts";
|
|
"/etc/qubes/post-install.d/ scripts" -> "qvm-features-request"
|
|
[xlabel="each script calls"];
|
|
"qvm-features-request" -> "qubes.FeaturesRequest"
|
|
[xlabel="last script call the service to dom0"];
|
|
"qubes.FeaturesRequest" -> "qubes core extensions"
|
|
[xlabel="features-request event"];
|
|
"qubes core extensions" -> "VM features"
|
|
[xlabel="verification"];
|
|
|
|
}
|
|
|
|
Example ``/etc/qubes/post-install.d/20-example.sh`` file:
|
|
|
|
.. code-block:: shell
|
|
|
|
#!/bin/sh
|
|
|
|
qvm-features-request example-feature=1
|
|
|
|
Example extension handling the above:
|
|
|
|
.. code-block:: python
|
|
|
|
import qubes.ext
|
|
|
|
class ExampleExtension(qubes.ext.Extension):
|
|
# the last argument must be named untrusted_features
|
|
@qubes.ext.handler('features-request')
|
|
def on_features_request(self, vm, event, untrusted_features):
|
|
# don't allow TemplateBasedVMs to request the feature - should be
|
|
# requested by the template instead
|
|
if hasattr(vm, 'template'):
|
|
return
|
|
|
|
untrusted_value = untrusted_features.get('example-feature', None)
|
|
# check if feature is advertised and verify its value
|
|
if untrusted_value != '1':
|
|
return
|
|
value = untrusted_value
|
|
|
|
# and finally set the value
|
|
vm.features['example-feature'] = value
|
|
|
|
Services
|
|
---------
|
|
|
|
`Qubes services <https://www.qubes-os.org/doc/qubes-service/>`_ are implemented
|
|
as features with ``service.`` prefix. The
|
|
:py:class:`qubes.ext.services.ServicesExtension` enumerate all the features
|
|
in form of ``service.<service-name>`` prefix and write them to QubesDB as
|
|
``/qubes-service/<service-name>`` and value either ``0`` or ``1``.
|
|
VM startup scripts list those entries for for each with value of ``1``, create
|
|
``/var/run/qubes-service/<service-name>`` file. Then, it can be conveniently
|
|
used by other scripts to check whether dom0 wishes service to be enabled or
|
|
disabled.
|
|
|
|
VM package can advertise what services are supported. For that, it needs to
|
|
request ``supported-service.<service-name>`` feature with value ``1`` according
|
|
to description above. The :py:class:`qubes.ext.services.ServicesExtension` will
|
|
handle such request and set this feature on VM object. ``supported-service.``
|
|
features that stop being advertised with ``qvm-features-request`` call are
|
|
removed. This way, it's enough to remove the file from
|
|
``/etc/qubes/post-install.d`` (for example by uninstalling package providing
|
|
the service) to tell dom0 the service is no longer supported. Services
|
|
advertised by TemplateBasedVMs are currently ignored (related
|
|
``supported-service.`` features are not set), but retrieving them may be added
|
|
in the future. Applications checking for specific service support should use
|
|
``vm.features.check_with_template('supported-service.<service-name>', False)``
|
|
call on desired VM object. When enumerating all supported services, application
|
|
should consider both the vm and its template (if any).
|
|
|
|
Various tools will use this information to discover if given service is
|
|
supported. The API does not enforce service being first advertised before being
|
|
enabled (means: there can be service which is enabled, but without matching
|
|
``supported-service.`` feature). The list of well known services is in
|
|
:program:`qvm-service` man page.
|
|
|
|
Example ``/etc/qubes/post-install.d/20-my-service.sh``:
|
|
|
|
.. code-block:: shell
|
|
|
|
#!/bin/sh
|
|
|
|
qvm-features-request supported-service.my-service=1
|
|
|
|
Services and features can be then inspected from dom0 using
|
|
:program:`qvm-features` tool, for example:
|
|
|
|
.. code-block:: shell
|
|
|
|
$ qvm-features my-qube
|
|
supported-service.my-service 1
|
|
|
|
|
|
Announcing supported features
|
|
------------------------------
|
|
|
|
For non-service features, there is similar announce mechanis to the above, but
|
|
uses ``supported-feature.`` prefix. It works like this:
|
|
|
|
1. The TemplateVM (or StandaloneVM) announces
|
|
``supported-feature.FEATURE_NAME=1`` (with ``FEATURE_NAME`` replaced with an
|
|
actual feature name) using ``qvm-features-request`` tool (see above how to use it).
|
|
2. core-admin extension
|
|
:py:class:`qubes.ext.supported_features.SupportedFeaturesExtension` records
|
|
such requests as features on the same VM.
|
|
3. Any tool that wants to check if the feature support is advertised by the VM,
|
|
can look into its features. For template-based VMs it is advised to check
|
|
also its template - there is a `vm.features.check_with_template()`
|
|
function specifically for this.
|
|
|
|
Note that (similar to services) it is not necessary for the feature to be
|
|
advertised to enable it. In fact, many features does not need any support from
|
|
the VM side, so they will work without matching ``supported-feature.`` entry.
|
|
Whether a feature requires VM-side support, is documented on case-by-case basis
|
|
in `qvm-features` tool manual page.
|
|
|
|
Module contents
|
|
---------------
|
|
|
|
.. automodule:: qubes.features
|
|
:members:
|
|
:show-inheritance:
|
|
|
|
.. vim: ts=3 sw=3 et
|
|
|