qubes.vm.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 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 None), empty string (which is interpreted as False) and non-empty string, which is True. Anything assigned to the mapping is coerced to strings, however if you assign instances of 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.

qubes.vm.Features inherits from 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:

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 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 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 (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.

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:

#!/bin/sh

qvm-features-request example-feature=1

Example extension handling the above:

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 are implemented as features with service. prefix. The 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 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 qvm-service man page.

Example /etc/qubes/post-install.d/20-my-service.sh:

#!/bin/sh

qvm-features-request supported-service.my-service=1

Services and features can be then inspected from dom0 using qvm-features tool, for example:

$ qvm-features my-qube
supported-service.my-service  1

Module contents

class qubes.vm.Features(vm, other=None, **kwargs)[source]

Bases: dict

Manager of the features.

Features can have three distinct values: no value (not present in mapping, which is closest thing to None), empty string (which is interpreted as False) and non-empty string, which is True. Anything assigned to the mapping is coerced to strings, however if you assign instances of 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.

This class inherits from dict, but has most of the methods that manipulate the item disarmed (they raise NotImplementedError). The ones that are left fire appropriate events on the qube that owns an instance of this class.

check_with_netvm(feature, default=<object object>)[source]

Check if the vm’s netvm has the specified feature.

check_with_template(feature, default=<object object>)[source]

Check if the vm’s template has the specified feature.

clear() → None. Remove all items from D.[source]
pop(_key, _default=None)[source]

Not implemented :raises: NotImplementedError

popitem()[source]

Not implemented :raises: NotImplementedError

setdefault(_key, _default=None)[source]

Not implemented :raises: NotImplementedError

update([E, ]**F) → None. Update D from dict/iterable E and F.[source]

If E is present and has a .keys() method, then does: for k in E: D[k] = E[k] If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v In either case, this is followed by: for k in F: D[k] = F[k]