diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 00000000..895d2c06
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,12 @@
+[report]
+exclude_lines =
+ pragma: no cover
+ ^\s*def __repr__
+ ^\s*(el)?if os\.name ==
+ ^\s*raise (RuntimeError|NotImplementedError)
+ ^\s*except ImportError
+[paths]
+source =
+ qubes
+ /usr/lib/python2.7/site-packages/qubes
+ /usr/lib64/python2.7/site-packages/qubes
diff --git a/.gitignore b/.gitignore
index 06d30793..2cb9c2fa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,9 @@
rpm/
pkgs/
+qubes.egg-info/
+.coverage
+.coverage.*
+htmlcov/
*.pyc
*.pyo
*~
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 00000000..ddd37c38
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,267 @@
+[MASTER]
+
+# Specify a configuration file.
+#rcfile=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Profiled execution.
+profile=no
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Pickle collected data for later comparisons.
+persistent=no
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+#load-plugins=
+
+
+[MESSAGES CONTROL]
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time.
+#enable=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once).
+
+# abstract-class-little-used: see http://www.logilab.org/ticket/111138
+disable=
+ locally-disabled,
+ locally-enabled,
+ file-ignored,
+ duplicate-code,
+ star-args,
+ cyclic-import,
+ abstract-class-little-used,
+ bad-continuation
+
+#
+# OTHER NICE SETS
+#
+
+# IMPORTS
+#disable=all
+#enable=
+# cyclic-import,
+# import-error,
+# no-member,
+# super-on-old-class,
+# undefined-variable,
+# unused-import
+
+
+[REPORTS]
+
+# Set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html
+output-format=text
+
+# Put messages in a separate file for each module / package specified on the
+# command line instead of printing them on stdout. Reports (if any) will be
+# written in a file name "pylint_global.[txt|html]".
+files-output=no
+
+# Tells whether to display a full report or only the messages
+reports=yes
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Add a comment according to your evaluation note. This is used by the global
+# evaluation report (RP0004).
+comment=yes
+
+
+[TYPECHECK]
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of classes names for which member attributes should not be checked
+# (useful for classes with attributes dynamically set).
+#ignored-classes=
+
+# When zope mode is activated, add a predefined set of Zope acquired attributes
+# to generated-members.
+zope=no
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E0201 when accessed. Python regular
+# expressions are accepted.
+#generated-members=
+
+
+[BASIC]
+
+# List of builtins function names that should not be used, separated by a comma
+bad-functions=map,filter,apply,input
+
+# Regular expression which should only match correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression which should only match correct module level names
+const-rgx=(([A-Za-z_][A-Za-z0-9_]*)|(__.*__))$
+
+# Regular expression which should only match correct class names
+class-rgx=([A-Z_][a-zA-Z0-9]+|TC_\d\d_[a-zA-Z0-9_]+)$
+
+# Regular expression which should only match correct function names
+function-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct method names
+method-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct instance attribute names
+attr-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct argument names
+argument-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct variable names
+variable-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct list comprehension /
+# generator expression variable names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=e,i,j,k,m,p,ex,Run,_,log,vm,xc,xs,ip,fd,fh,rw,st,tb
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# Regular expression which should only match functions or classes name which do
+# not require a docstring
+no-docstring-rgx=__.*__
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,FIX,XXX,TODO
+
+
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=80
+
+# Maximum number of lines in a module
+max-module-lines=3000
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+
+[VARIABLES]
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching the beginning of the name of dummy variables
+# (i.e. not used).
+dummy-variables-rgx=_|dummy
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+#additional-builtins=
+
+
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=35
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*
+
+# Maximum number of locals for function / method body
+# Let's have max-args + 5
+max-locals=40
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of branch for function / method body
+# 4x the default value
+max-branches=48
+
+# Maximum number of statements in function / method body
+# Double default
+max-statements=100
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=15
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=100
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=Exception,EnvironmentError
+
diff --git a/.travis.yml b/.travis.yml
index 66bde298..716104bb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,29 @@
sudo: required
dist: trusty
-language: generic
-install: git clone https://github.com/QubesOS/qubes-builder ~/qubes-builder
-script: ~/qubes-builder/scripts/travis-build
+language: python
+python:
+ - '3.5'
+install:
+ - pip install --quiet -r ci/requirements.txt
+ - git clone https://github.com/"${TRAVIS_REPO_SLUG%%/*}"/qubes-builder ~/qubes-builder
+script:
+ - PYTHONPATH=test-packages pylint --rcfile=ci/pylintrc qubes qubespolicy
+ - ./run-tests --no-syslog
+ - ~/qubes-builder/scripts/travis-build
env:
- - DIST_DOM0=fc23 USE_QUBES_REPO_VERSION=3.2 USE_QUBES_REPO_TESTING=1
+ - DIST_DOM0=fc25 USE_QUBES_REPO_VERSION=4.0 USE_QUBES_REPO_TESTING=1
+
+after_success:
+ - ~/qubes-builder/scripts/travis-deploy
+
+# don't build tags which are meant for code signing only
+branches:
+ except:
+ - /.*_.*/
+
+addons:
+ apt:
+ packages:
+ - debootstrap
+
+# vim: ts=2 sts=2 sw=2 et
diff --git a/Makefile b/Makefile
index 545e8089..350d25fb 100644
--- a/Makefile
+++ b/Makefile
@@ -5,6 +5,7 @@ VERSION := $(shell cat version)
DIST_DOM0 ?= fc18
OS ?= Linux
+PYTHON ?= python3
ifeq ($(OS),Linux)
DATADIR ?= /var/lib/qubes
@@ -39,19 +40,10 @@ rpms-dom0:
$(RPMS_DIR)/x86_64/qubes-core-dom0-$(VERSION)*.rpm \
$(RPMS_DIR)/noarch/qubes-core-dom0-doc-$(VERSION)*rpm
-clean:
- make -C dispvm clean
- make -C qmemman clean
-
all:
- make all -C core
- make all -C core-modules
- make all -C tests
+ $(PYTHON) setup.py build
+# make all -C tests
# Currently supported only on xen
-ifeq ($(BACKEND_VMM),xen)
- make all -C qmemman
- make all -C dispvm
-endif
install:
ifeq ($(OS),Linux)
@@ -59,17 +51,19 @@ ifeq ($(OS),Linux)
$(MAKE) install -C linux/aux-tools
$(MAKE) install -C linux/system-config
endif
- $(MAKE) install -C qvm-tools
- $(MAKE) install -C core
- $(MAKE) install -C core-modules
- $(MAKE) install -C tests
+ $(PYTHON) setup.py install -O1 --skip-build --root $(DESTDIR)
+ ln -s qvm-device $(DESTDIR)/usr/bin/qvm-pci
+ ln -s qvm-device $(DESTDIR)/usr/bin/qvm-usb
+# $(MAKE) install -C tests
+ $(MAKE) install -C relaxng
+ mkdir -p $(DESTDIR)/etc/qubes
ifeq ($(BACKEND_VMM),xen)
# Currently supported only on xen
- $(MAKE) install -C qmemman
+ cp etc/qmemman.conf $(DESTDIR)/etc/qubes/
endif
- $(MAKE) install -C dispvm
mkdir -p $(DESTDIR)/etc/qubes-rpc/policy
mkdir -p $(DESTDIR)/usr/libexec/qubes
+ cp qubes-rpc-policy/qubes.FeaturesRequest.policy $(DESTDIR)/etc/qubes-rpc/policy/qubes.FeaturesRequest
cp qubes-rpc-policy/qubes.Filecopy.policy $(DESTDIR)/etc/qubes-rpc/policy/qubes.Filecopy
cp qubes-rpc-policy/qubes.OpenInVM.policy $(DESTDIR)/etc/qubes-rpc/policy/qubes.OpenInVM
cp qubes-rpc-policy/qubes.OpenURL.policy $(DESTDIR)/etc/qubes-rpc/policy/qubes.OpenURL
@@ -78,14 +72,21 @@ endif
cp qubes-rpc-policy/qubes.NotifyTools.policy $(DESTDIR)/etc/qubes-rpc/policy/qubes.NotifyTools
cp qubes-rpc-policy/qubes.GetImageRGBA.policy $(DESTDIR)/etc/qubes-rpc/policy/qubes.GetImageRGBA
cp qubes-rpc-policy/qubes.GetRandomizedTime.policy $(DESTDIR)/etc/qubes-rpc/policy/qubes.GetRandomizedTime
- cp qubes-rpc/qubes.NotifyUpdates $(DESTDIR)/etc/qubes-rpc/
- cp qubes-rpc/qubes.NotifyTools $(DESTDIR)/etc/qubes-rpc/
+ cp qubes-rpc-policy/qubes.NotifyTools.policy $(DESTDIR)/etc/qubes-rpc/policy/qubes.NotifyTools
+ cp qubes-rpc-policy/qubes.NotifyUpdates.policy $(DESTDIR)/etc/qubes-rpc/policy/qubes.NotifyUpdates
+ cp qubes-rpc-policy/qubes.OpenInVM.policy $(DESTDIR)/etc/qubes-rpc/policy/qubes.OpenInVM
+ cp qubes-rpc-policy/qubes.VMShell.policy $(DESTDIR)/etc/qubes-rpc/policy/qubes.VMShell
+ cp qubes-rpc/qubes.FeaturesRequest $(DESTDIR)/etc/qubes-rpc/
cp qubes-rpc/qubes.GetRandomizedTime $(DESTDIR)/etc/qubes-rpc/
+ cp qubes-rpc/qubes.NotifyTools $(DESTDIR)/etc/qubes-rpc/
+ cp qubes-rpc/qubes.NotifyUpdates $(DESTDIR)/etc/qubes-rpc/
cp qubes-rpc/qubes-notify-updates $(DESTDIR)/usr/libexec/qubes/
cp qubes-rpc/qubes-notify-tools $(DESTDIR)/usr/libexec/qubes/
+
mkdir -p "$(DESTDIR)$(FILESDIR)"
- cp vm-config/$(BACKEND_VMM)-vm-template.xml "$(DESTDIR)$(FILESDIR)/vm-template.xml"
- cp vm-config/$(BACKEND_VMM)-vm-template-hvm.xml "$(DESTDIR)$(FILESDIR)/vm-template-hvm.xml"
+ cp -r templates "$(DESTDIR)$(FILESDIR)/templates"
+ rm -f "$(DESTDIR)$(FILESDIR)/templates/README"
+
mkdir -p $(DESTDIR)$(DATADIR)
mkdir -p $(DESTDIR)$(DATADIR)/vm-templates
mkdir -p $(DESTDIR)$(DATADIR)/appvms
diff --git a/Makefile.builder b/Makefile.builder
index 8068a772..bde349a9 100644
--- a/Makefile.builder
+++ b/Makefile.builder
@@ -1,5 +1,5 @@
ifeq ($(PACKAGE_SET),dom0)
-RPM_SPEC_FILES := $(addprefix rpm_spec/,core-dom0.spec core-dom0-doc.spec)
+RPM_SPEC_FILES := rpm_spec/core-dom0.spec
WIN_SOURCE_SUBDIRS := .
WIN_COMPILER := mingw
WIN_PACKAGE_CMD := make msi
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..c767608d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,10 @@
+Qubes core, version 3
+---------------------
+
+[](https://travis-ci.org/woju/qubes-core-admin)
+
+
+This is development branch of the Qubes OS core. This branch is subject to
+rebase without warning until further notice.
+
+API documentation is available: https://qubes-core-admin.readthedocs.org/en/latest/.
diff --git a/ci/coveragerc b/ci/coveragerc
new file mode 100644
index 00000000..2d47c77e
--- /dev/null
+++ b/ci/coveragerc
@@ -0,0 +1,3 @@
+[run]
+source = qubes
+omit = qubes/tests/*
diff --git a/ci/lvm-manage b/ci/lvm-manage
new file mode 100755
index 00000000..4334f06d
--- /dev/null
+++ b/ci/lvm-manage
@@ -0,0 +1,27 @@
+# This is to include LVM-requiring tests in Travis-CI
+if [ "$1" = "setup-lvm" -a -n "$2" ]; then
+ POOL_PATH=$2
+ VG_NAME=`echo $POOL_PATH | cut -f 1 -d /`
+ POOL_NAME=`echo $POOL_PATH | cut -f 2 -d /`
+ if lvs $VG_NAME >/dev/null 2>&1 || lvs $POOL_PATH >/dev/null 2>&1; then
+ echo "WARNING: either VG '$VG_NAME' or thin pool '$POOL_PATH' already exists, not reusing" >&2
+ exit 1
+ fi
+ set -e
+ loop_file=`mktemp`
+ truncate -s 1G $loop_file
+ loop_dev=`losetup -f --show $loop_file`
+ # auto cleanup
+ rm -f $loop_file
+ vgcreate "$VG_NAME" $loop_dev
+ lvcreate --thinpool "$POOL_NAME" --type thin-pool -L 960M "$VG_NAME"
+ exit 0
+elif [ "$1" = "cleanup-lvm" -a -n "$2" ]; then
+ VG_NAME=`echo $2 | cut -f 1 -d /`
+ set -e
+ pvs=`vgs --noheadings -o pv_name $VG_NAME | tr -d ' '`
+ lvremove -f "$2"
+ vgremove "$VG_NAME"
+ losetup -d $pvs
+ exit 0
+fi
diff --git a/ci/pylintrc b/ci/pylintrc
new file mode 100644
index 00000000..428cc139
--- /dev/null
+++ b/ci/pylintrc
@@ -0,0 +1,199 @@
+[MASTER]
+persistent=no
+ignore=tests,backup.py
+
+[MESSAGES CONTROL]
+# abstract-class-little-used: see http://www.logilab.org/ticket/111138
+# deprecated-method:
+# enable again after disabling py-3.4.3 asyncio.ensure_future compat hack
+disable=
+ abstract-class-little-used,
+ bad-continuation,
+ cyclic-import,
+ deprecated-method,
+ duplicate-code,
+ file-ignored,
+ fixme,
+ locally-disabled,
+ locally-enabled,
+ logging-format-interpolation,
+ missing-docstring,
+ star-args
+
+[REPORTS]
+
+# Set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html
+output-format=colorized
+
+#files-output=no
+reports=yes
+
+[TYPECHECK]
+ignored-classes=
+ VMProperty,
+ libvirt,libvirtError,
+ dbus,SystemBus,
+ PCIDevice
+
+ignore-mixin-members=yes
+generated-members=
+ iter_entry_points,
+ Element,ElementTree,QName,SubElement,fromstring,parse,tostring,
+
+[BASIC]
+
+# List of builtins function names that should not be used, separated by a comma
+bad-functions=apply,input
+
+# Regular expression which should only match correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression which should only match correct module level names
+const-rgx=(([A-Za-z_][A-Za-z0-9_]*)|(__.*__))$
+
+# Regular expression which should only match correct class names
+class-rgx=([A-Z_][a-zA-Z0-9]+|TC_\d\d_[a-zA-Z0-9_]+)$
+
+# Regular expression which should only match correct function names
+function-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct method names
+method-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct instance attribute names
+attr-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct argument names
+argument-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct variable names
+variable-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct list comprehension /
+# generator expression variable names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=e,i,j,k,m,p,v,ex,Run,_,log,vm,xc,xs,ip,fd,fh,rw,st,tb,cb,ff
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# Regular expression which should only match functions or classes name which do
+# not require a docstring
+no-docstring-rgx=__.*__
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,FIX,XXX,TODO
+
+
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=80
+
+# Maximum number of lines in a module
+max-module-lines=3000
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+
+[VARIABLES]
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching the beginning of the name of dummy variables
+# (i.e. not used).
+dummy-variables-rgx=_|dummy
+
+
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,TERMIOS,Bastion,rexec
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=35
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*
+
+# Maximum number of locals for function / method body
+# Let's have max-args + 5
+max-locals=40
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of branch for function / method body
+# 4x the default value
+max-branches=48
+
+# Maximum number of statements in function / method body
+# Double default
+max-statements=100
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=15
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=100
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=Exception,EnvironmentError
+
+# vim: ft=conf
diff --git a/ci/requirements.txt b/ci/requirements.txt
new file mode 100644
index 00000000..a95201dc
--- /dev/null
+++ b/ci/requirements.txt
@@ -0,0 +1,9 @@
+# WARNING: those requirements are used only for travis-ci.org
+# they SHOULD NOT be used under normal conditions; use system package manager
+coverage
+docutils
+jinja2
+lxml
+pylint
+sphinx
+pydbus
diff --git a/contrib/check-events b/contrib/check-events
new file mode 100755
index 00000000..80a546f8
--- /dev/null
+++ b/contrib/check-events
@@ -0,0 +1,147 @@
+#!/usr/bin/env python2
+
+from __future__ import print_function
+from pprint import pprint
+
+import argparse
+import ast
+import os
+import sys
+
+SOMETHING = ''
+
+parser = argparse.ArgumentParser()
+
+parser.add_argument('--never-handled',
+ action='store_true', dest='never_handled',
+ help='mark never handled events')
+
+parser.add_argument('--no-never-handled',
+ action='store_false', dest='never_handled',
+ help='do not mark never handled events')
+
+parser.add_argument('directory', metavar='DIRECTORY',
+ help='directory to search for .py files')
+
+class Event(object):
+ def __init__(self, events, name):
+ self.events = events
+ self.name = name
+ self.fired = []
+ self.handled = []
+
+ def fire(self, filename, lineno):
+ self.fired.append((filename, lineno))
+
+ def handle(self, filename, lineno):
+ self.handled.append((filename, lineno))
+
+ def print_summary_one(self, stream, attr, colour, never=True):
+ lines = getattr(self, attr)
+ if lines:
+ for filename, lineno in lines:
+ stream.write(' \033[{}m{}\033[0m {} +{}\n'.format(
+ colour, attr[0], filename, lineno))
+
+ elif never:
+ stream.write(' \033[1;33mnever {}\033[0m\n'.format(attr))
+
+ def print_summary(self, stream, never_handled):
+ stream.write('\033[1m{}\033[0m\n'.format(self.name))
+
+ self.print_summary_one(stream, 'fired', '1;31')
+ self.print_summary_one(stream, 'handled', '1;32', never=never_handled)
+
+
+class Events(dict):
+ def __missing__(self, key):
+ self[key] = Event(self, key)
+ return self[key]
+
+
+class EventVisitor(ast.NodeVisitor):
+ def __init__(self, events, filename, *args, **kwargs):
+ super(EventVisitor, self).__init__(*args, **kwargs)
+ self.events = events
+ self.filename = filename
+
+ def resolve_attr(self, node):
+ if isinstance(node, ast.Name):
+ return node.id
+ if isinstance(node, ast.Attribute):
+ return '{}.{}'.format(self.resolve_attr(node.value), node.attr)
+ raise TypeError('resolve_attr() does not support {!r}'.format(node))
+
+ def visit_Call(self, node):
+ try:
+ name = self.resolve_attr(node.func)
+ except TypeError:
+ # name got something else than identifier in the attribute path;
+ # this may have been 'str'.format() for example; we can't call
+ # events this way
+ return
+
+ if name.endswith('.fire_event') or name.endswith('.fire_event_pre'):
+ # here we throw events; event name is the first argument; sometimes
+ # it is expressed as 'event-stem:' + some_variable
+ eventnode = node.args[0]
+ if isinstance(eventnode, ast.Str):
+ event = eventnode.s
+ elif isinstance(eventnode, ast.BinOp) \
+ and isinstance(eventnode.left, ast.Str):
+ event = eventnode.left.s
+ else:
+ raise AssertionError('fishy event {!r} in {} +{}'.format(
+ eventnode, self.filename, node.lineno))
+
+ if ':' in event:
+ event = ':'.join((event.split(':', 1)[0], SOMETHING))
+
+ self.events[event].fire(self.filename, node.lineno)
+ return
+
+ if name in ('qubes.events.handler', 'qubes.ext.handler'):
+ # here we handle; event names (there may be more than one) are all
+ # positional arguments
+ if node.starargs is not None:
+ raise AssertionError(
+ 'event handler with *args in {} +{}'.format(
+ self.filename, node.lineno))
+
+ for arg in node.args:
+ if not isinstance(arg, ast.Str):
+ raise AssertionError(
+ 'event handler with non-string arg in {} +{}'.format(
+ self.filename, node.lineno))
+
+ event = arg.s
+ if ':' in event:
+ event = ':'.join((event.split(':', 1)[0], SOMETHING))
+
+ self.events[event].handle(self.filename, node.lineno)
+
+ return
+
+ self.generic_visit(node)
+ return
+
+
+def main():
+ args = parser.parse_args()
+
+ events = Events()
+
+ for dirpath, dirnames, filenames in os.walk(args.directory):
+ for filename in filenames:
+ if not filename.endswith('.py'):
+ continue
+ filepath = os.path.join(dirpath, filename)
+ EventVisitor(events, filepath).visit(
+ ast.parse(open(filepath).read(), filepath))
+
+ for event in sorted(events):
+ events[event].print_summary(
+ sys.stdout, never_handled=args.never_handled)
+
+if __name__ == '__main__':
+ main()
diff --git a/contrib/import-graph b/contrib/import-graph
new file mode 100755
index 00000000..7a166a69
--- /dev/null
+++ b/contrib/import-graph
@@ -0,0 +1,180 @@
+#!/usr/bin/env python3
+
+import itertools
+import os
+import re
+import sys
+
+re_import = re.compile(r'^import (.*?)$', re.M)
+re_import_from = re.compile(r'^from (.*?) import .*?$', re.M)
+
+class Import(object):
+ defstyle = {'arrowhead': 'open', 'arrowtail':'none'}
+
+ def __init__(self, importing, imported, **kwargs):
+ self.importing = importing
+ self.imported = imported
+ self.style = self.defstyle.copy()
+ self.style.update(kwargs)
+
+ def __str__(self):
+ return '{}"{}" -> "{}" [{}];'.format(
+ ('//' if self.commented else ''),
+ self.importing,
+ self.imported,
+ ', '.join('{}="{}"'.format(*i) for i in self.style.items()))
+
+ def __eq__(self, other):
+ return (self.importing.name, self.imported.name) \
+ == (other.importing.name, other.imported.name)
+
+ def __hash__(self):
+ return hash((self.importing.name, self.imported.name))
+
+ @property
+ def commented(self):
+ if self.style.get('color', '') != 'red':
+ return True
+# for i in (self.importing, self.imported):
+# if i.name.startswith('qubes.tests'): return True
+# if i.name.startswith('qubes.tools'): return True
+
+
+class Module(set):
+ def __init__(self, package, path):
+ self.package = package
+ self.path = path
+
+ def process(self):
+ with open(os.path.join(self.package.root, self.path)) as fh:
+ data = fh.read()
+ data.replace('\\\n', ' ')
+
+ for imported in re_import.findall(data):
+ try:
+ imported = self.package[imported]
+ except KeyError:
+ continue
+ self.add(Import(self, imported))
+
+ for imported in re_import_from.findall(data):
+ try:
+ imported = self.package[imported]
+ except KeyError:
+ continue
+ self.add(Import(self, imported, style='dotted'))
+
+ def __getitem__(self, key):
+ for i in self:
+ if i.imported == key:
+ return i
+ raise KeyError(key)
+
+ @property
+ def name(self):
+ names = os.path.splitext(self.path)[0].split('/')
+ names.insert(0, self.package.name)
+ if names[-1] == '__init__':
+ del names[-1]
+ return '.'.join(names)
+
+ def __hash__(self):
+ return hash(self.name)
+
+ def __str__(self):
+ return self.name
+
+ def __repr__(self):
+ return '<{} {!r}>'.format(self.__class__.__name__, self.name)
+
+ def __lt__(self, other):
+ return self.name < other.name
+
+ def __eq__(self, other):
+ return self.name == other.name
+
+
+class Cycle(tuple):
+ def __new__(cls, modules):
+ i = modules.index(sorted(modules)[0])
+# sys.stderr.write('modules={!r} i={!r}\n'.format(modules, i))
+ return super(Cycle, cls).__new__(cls, modules[i:] + modules[:i+1])
+
+# def __lt__(self, other):
+# if len(self) < len(other):
+# return True
+# elif len(self) > len(other):
+# return False
+#
+# return super(Cycle, self).__lt__(other)
+
+
+class Package(dict):
+ def __init__(self, root):
+ super(Package, self).__init__()
+ self.root = root
+
+ for dirpath, dirnames, filenames in os.walk(self.root):
+ for filename in filenames:
+ if not os.path.splitext(filename)[1] == '.py':
+ continue
+ module = Module(self,
+ os.path.relpath(os.path.join(dirpath, filename), self.root))
+ self[module.name] = module
+
+ for name, module in self.items():
+ module.process()
+
+ @property
+ def name(self):
+ return os.path.basename(self.root.rstrip(os.path.sep))
+
+ def _find_cycles(self):
+ # stolen from codereview.stackexchange.com/questions/86021 and hacked
+ path = []
+ visited = set()
+
+ def visit(module):
+# if module in visited:
+# return
+# visited.add(module)
+ path.append(module)
+ for i in module:
+ if i.imported in path:
+ yield Cycle(path[path.index(i.imported):])
+ else:
+ yield from visit(i.imported)
+ path.pop()
+
+ for v in self.values():
+ yield from visit(v)
+
+ def find_cycles(self):
+ return list(sorted(set(self._find_cycles())))
+
+ def get_all_imports(self):
+ for module in self.values():
+ yield from module
+
+ def __str__(self):
+ return '''\n
+digraph "import" {{
+charset="utf-8"
+rankdir=BT
+{}
+}}
+'''.format('\n'.join(str(i) for i in self.get_all_imports()))
+
+def main():
+ package = Package(sys.argv[1])
+
+ for cycle in package.find_cycles():
+ for i in range(len(cycle) - 1):
+ edge = cycle[i][cycle[i+1]]
+ edge.style['color'] = 'red'
+ sys.stderr.write(' -> '.join(str(module) for module in cycle) + '\n')
+
+ sys.stdout.write(str(package))
+
+if __name__ == '__main__':
+ main()
diff --git a/core-modules/000QubesVm.py b/core-modules/000QubesVm.py
deleted file mode 100644
index a12537e6..00000000
--- a/core-modules/000QubesVm.py
+++ /dev/null
@@ -1,2161 +0,0 @@
-#!/usr/bin/python2
-# -*- coding: utf-8 -*-
-#
-# The Qubes OS Project, http://www.qubes-os.org
-#
-# Copyright (C) 2010 Joanna Rutkowska
-# Copyright (C) 2013 Marek Marczykowski
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-#
-
-import datetime
-import base64
-import hashlib
-import logging
-import grp
-import lxml.etree
-import os
-import os.path
-import re
-import shutil
-import subprocess
-import sys
-import time
-import uuid
-import xml.parsers.expat
-import signal
-import pwd
-from qubes import qmemman
-from qubes import qmemman_algo
-import libvirt
-
-from qubes.qubes import dry_run,vmm
-from qubes.qubes import register_qubes_vm_class
-from qubes.qubes import QubesVmCollection,QubesException,QubesHost,QubesVmLabels
-from qubes.qubes import defaults,system_path,vm_files,qubes_max_qid
-from qubes.storage import get_pool
-
-qmemman_present = False
-try:
- from qubes.qmemman_client import QMemmanClient
- qmemman_present = True
-except ImportError:
- pass
-
-import qubes.qubesutils
-
-xid_to_name_cache = {}
-
-class QubesVm(object):
- """
- A representation of one Qubes VM
- Only persistent information are stored here, while all the runtime
- information, e.g. Xen dom id, etc, are to be retrieved via Xen API
- Note that qid is not the same as Xen's domid!
- """
-
- # In which order load this VM type from qubes.xml
- load_order = 100
-
- # hooks for plugins (modules) which want to influence existing classes,
- # without introducing new ones
- hooks_clone_disk_files = []
- hooks_create_on_disk = []
- hooks_create_qubesdb_entries = []
- hooks_get_attrs_config = []
- hooks_get_clone_attrs = []
- hooks_get_config_params = []
- hooks_init = []
- hooks_label_setter = []
- hooks_netvm_setter = []
- hooks_post_rename = []
- hooks_pre_rename = []
- hooks_remove_from_disk = []
- hooks_start = []
- hooks_verify_files = []
- hooks_set_attr = []
-
- def get_attrs_config(self):
- """ Object attributes for serialization/deserialization
- inner dict keys:
- - order: initialization order (to keep dependency intact)
- attrs without order will be evaluated at the end
- - default: default value used when attr not given to object constructor
- - attr: set value to this attribute instead of parameter name
- - eval: (DEPRECATED) assign result of this expression instead of
- value directly; local variable 'value' contains
- attribute value (or default if it was not given)
- - func: callable used to parse the value retrieved from XML
- - save: use evaluation result as value for XML serialization; only attrs with 'save' key will be saved in XML
- - save_skip: if present and evaluates to true, attr will be omitted in XML
- - save_attr: save to this XML attribute instead of parameter name
- """
-
- attrs = {
- # __qid cannot be accessed by setattr, so must be set manually in __init__
- "qid": { "attr": "_qid", "order": 0 },
- "name": { "order": 1 },
- "uuid": { "order": 0, "eval": 'uuid.UUID(value) if value else None' },
- "dir_path": { "default": None, "order": 2 },
- "pool_name": { "default":"default" },
- "conf_file": {
- "func": lambda value: self.absolute_path(value, self.name +
- ".conf"),
- "order": 3 },
- ### order >= 10: have base attrs set
- "firewall_conf": {
- "func": self._absolute_path_gen(vm_files["firewall_conf"]),
- "order": 10 },
- "installed_by_rpm": { "default": False, 'order': 10 },
- "template": { "default": None, "attr": '_template', 'order': 10 },
- ### order >= 20: have template set
- "uses_default_netvm": { "default": True, 'order': 20 },
- "netvm": { "default": None, "attr": "_netvm", 'order': 20 },
- "label": { "attr": "_label", "default": defaults["appvm_label"], 'order': 20,
- 'xml_deserialize': lambda _x: QubesVmLabels[_x] },
- "memory": { "default": defaults["memory"], 'order': 20 },
- "maxmem": { "default": None, 'order': 25 },
- "pcidevs": {
- "default": '[]',
- "order": 25,
- "func": lambda value: [] if value in ["none", None] else
- eval(value) if value.find("[") >= 0 else
- eval("[" + value + "]") },
- "pci_strictreset": {"default": True},
- "pci_e820_host": {"default": True},
- # Internal VM (not shown in qubes-manager, doesn't create appmenus entries
- "internal": { "default": False, 'attr': '_internal' },
- "vcpus": { "default": 2 },
- "uses_default_kernel": { "default": True, 'order': 30 },
- "uses_default_kernelopts": { "default": True, 'order': 30 },
- "kernel": {
- "attr": "_kernel",
- "default": None,
- "order": 31,
- "func": lambda value: self._collection.get_default_kernel() if
- self.uses_default_kernel else value },
- "kernelopts": {
- "default": "",
- "order": 31,
- "func": lambda value: value if not self.uses_default_kernelopts\
- else defaults["kernelopts_pcidevs"] if len(self.pcidevs)>0 \
- else self.template.kernelopts if self.template
- else defaults["kernelopts"] },
- "mac": { "attr": "_mac", "default": None },
- "include_in_backups": {
- "func": lambda x: x if x is not None
- else not self.installed_by_rpm },
- "services": {
- "default": {},
- "func": lambda value: eval(str(value)) },
- "debug": { "default": False },
- "default_user": { "default": "user", "attr": "_default_user" },
- "qrexec_timeout": { "default": 60 },
- "autostart": { "default": False, "attr": "_autostart" },
- "uses_default_dispvm_netvm": {"default": True, "order": 30},
- "dispvm_netvm": {"attr": "_dispvm_netvm", "default": None},
- "backup_content" : { 'default': False },
- "backup_size" : {
- "default": 0,
- "func": int },
- "backup_path" : { 'default': "" },
- "backup_timestamp": {
- "func": lambda value:
- datetime.datetime.fromtimestamp(int(value)) if value
- else None },
- ##### Internal attributes - will be overriden in __init__ regardless of args
- "config_file_template": {
- "func": lambda x: system_path["config_template_pv"] },
- "icon_path": {
- "func": lambda x: os.path.join(self.dir_path, "icon.png") if
- self.dir_path is not None else None },
- # used to suppress side effects of clone_attrs
- "_do_not_reset_firewall": { "func": lambda x: False },
- "kernels_dir": {
- # for backward compatibility (or another rare case): kernel=None -> kernel in VM dir
- "func": lambda x: \
- os.path.join(system_path["qubes_kernels_base_dir"],
- self.kernel) if self.kernel is not None \
- else os.path.join(self.dir_path,
- vm_files["kernels_subdir"]) },
- }
-
- ### Mark attrs for XML inclusion
- # Simple string attrs
- for prop in ['qid', 'uuid', 'name', 'dir_path', 'memory', 'maxmem',
- 'pcidevs', 'pci_strictreset', 'vcpus', 'internal',\
- 'uses_default_kernel', 'kernel', 'uses_default_kernelopts',\
- 'kernelopts', 'services', 'installed_by_rpm',\
- 'uses_default_netvm', 'include_in_backups', 'debug',\
- 'qrexec_timeout', 'autostart', 'uses_default_dispvm_netvm',
- 'backup_content', 'backup_size', 'backup_path', 'pool_name',\
- 'pci_e820_host']:
- attrs[prop]['save'] = lambda prop=prop: str(getattr(self, prop))
- # Simple paths
- for prop in ['conf_file', 'firewall_conf']:
- attrs[prop]['save'] = \
- lambda prop=prop: self.relative_path(getattr(self, prop))
- attrs[prop]['save_skip'] = \
- lambda prop=prop: getattr(self, prop) is None
-
- # Can happen only if VM created in offline mode
- attrs['maxmem']['save_skip'] = lambda: self.maxmem is None
- attrs['vcpus']['save_skip'] = lambda: self.vcpus is None
-
- attrs['uuid']['save_skip'] = lambda: self.uuid is None
- attrs['mac']['save'] = lambda: str(self._mac)
- attrs['mac']['save_skip'] = lambda: self._mac is None
-
- attrs['default_user']['save'] = lambda: str(self._default_user)
-
- attrs['backup_timestamp']['save'] = \
- lambda: self.backup_timestamp.strftime("%s")
- attrs['backup_timestamp']['save_skip'] = \
- lambda: self.backup_timestamp is None
-
- attrs['netvm']['save'] = \
- lambda: str(self.netvm.qid) if self.netvm is not None else "none"
- attrs['netvm']['save_attr'] = "netvm_qid"
- attrs['dispvm_netvm']['save'] = \
- lambda: str(self.dispvm_netvm.qid) \
- if self.dispvm_netvm is not None \
- else "none"
- attrs['template']['save'] = \
- lambda: str(self.template.qid) if self.template else "none"
- attrs['template']['save_attr'] = "template_qid"
- attrs['label']['save'] = lambda: self.label.name
-
- # fire hooks
- for hook in self.hooks_get_attrs_config:
- attrs = hook(self, attrs)
- return attrs
-
- def post_set_attr(self, attr, newvalue, oldvalue):
- for hook in self.hooks_set_attr:
- hook(self, attr, newvalue, oldvalue)
-
- def __basic_parse_xml_attr(self, value):
- if value is None:
- return None
- if value.lower() == "none":
- return None
- if value.lower() == "true":
- return True
- if value.lower() == "false":
- return False
- if value.isdigit():
- return int(value)
- return value
-
- def __init__(self, **kwargs):
- self._collection = None
- if 'collection' in kwargs:
- self._collection = kwargs['collection']
- else:
- raise ValueError("No collection given to QubesVM constructor")
-
- # Special case for template b/c it is given in "template_qid" property
- if "xml_element" in kwargs and kwargs["xml_element"].get("template_qid"):
- template_qid = kwargs["xml_element"].get("template_qid")
- if template_qid.lower() != "none":
- if int(template_qid) in self._collection:
- kwargs["template"] = self._collection[int(template_qid)]
- else:
- raise ValueError("Unknown template with QID %s" % template_qid)
- attrs = self.get_attrs_config()
- for attr_name in sorted(attrs, key=lambda _x: attrs[_x]['order'] if 'order' in attrs[_x] else 1000):
- attr_config = attrs[attr_name]
- attr = attr_name
- if 'attr' in attr_config:
- attr = attr_config['attr']
- value = None
- if attr_name in kwargs:
- value = kwargs[attr_name]
- elif 'xml_element' in kwargs and kwargs['xml_element'].get(attr_name) is not None:
- if 'xml_deserialize' in attr_config and callable(attr_config['xml_deserialize']):
- value = attr_config['xml_deserialize'](kwargs['xml_element'].get(attr_name))
- else:
- value = self.__basic_parse_xml_attr(kwargs['xml_element'].get(attr_name))
- else:
- if 'default' in attr_config:
- value = attr_config['default']
- if 'func' in attr_config:
- setattr(self, attr, attr_config['func'](value))
- elif 'eval' in attr_config:
- setattr(self, attr, eval(attr_config['eval']))
- else:
- #print "setting %s to %s" % (attr, value)
- setattr(self, attr, value)
-
- #Init private attrs
- self.__qid = self._qid
-
- self._libvirt_domain = None
- self._qdb_connection = None
-
- assert self.__qid < qubes_max_qid, "VM id out of bounds!"
- assert self.name is not None
-
- if not self.verify_name(self.name):
- msg = ("'%s' is invalid VM name (invalid characters, over 31 chars long, "
- "ends with '-dm', or one of 'none', 'true', 'false')") % self.name
- if 'xml_element' in kwargs:
- print >>sys.stderr, "WARNING: %s" % msg
- else:
- raise QubesException(msg)
-
- if self.netvm is not None:
- self.netvm.connected_vms[self.qid] = self
-
- # Not in generic way to not create QubesHost() to frequently
- if self.maxmem is None and not vmm.offline_mode:
- qubes_host = QubesHost()
- total_mem_mb = qubes_host.memory_total/1024
- self.maxmem = total_mem_mb/2
-
- # Linux specific cap: max memory can't scale beyond 10.79*init_mem
- if self.maxmem > self.memory * 10:
- self.maxmem = self.memory * 10
-
- # Always set if meminfo-writer should be active or not
- if 'meminfo-writer' not in self.services:
- self.services['meminfo-writer'] = not (len(self.pcidevs) > 0)
-
- # Additionally force meminfo-writer disabled when VM have PCI devices
- if len(self.pcidevs) > 0:
- self.services['meminfo-writer'] = False
-
- if 'xml_element' not in kwargs:
- # New VM, disable updates check if requested for new VMs
- if os.path.exists(qubes.qubesutils.UPDATES_DEFAULT_VM_DISABLE_FLAG):
- self.services['qubes-update-check'] = False
-
- # Initialize VM image storage class
- self.storage = get_pool(self.pool_name, self).getStorage()
- self.dir_path = self.storage.vmdir
- self.icon_path = os.path.join(self.storage.vmdir, 'icon.png')
- self.conf_file = os.path.join(self.storage.vmdir, self.name + '.conf')
-
- if hasattr(self, 'kernels_dir'):
- modules_path = os.path.join(self.kernels_dir,
- "modules.img")
- if os.path.exists(modules_path):
- self.storage.modules_img = modules_path
- self.storage.modules_img_rw = self.kernel is None
-
- # Some additional checks for template based VM
- if self.template is not None:
- if not self.template.is_template():
- print >> sys.stderr, "ERROR: template_qid={0} doesn't point to a valid TemplateVM".\
- format(self.template.qid)
- return
- self.template.appvms[self.qid] = self
- else:
- assert self.root_img is not None, "Missing root_img for standalone VM!"
-
- self.log = logging.getLogger('qubes.vm.{}'.format(self.qid))
- self.log.debug('instantiated name={!r} class={}'.format(
- self.name, self.__class__.__name__))
-
- # fire hooks
- for hook in self.hooks_init:
- hook(self)
-
- def __repr__(self):
- return '<{} at {:#0x} qid={!r} name={!r}>'.format(
- self.__class__.__name__,
- id(self),
- self.qid,
- self.name)
-
- def absolute_path(self, arg, default):
- if arg is not None and os.path.isabs(arg):
- return arg
- elif self.dir_path is not None:
- return os.path.join(self.dir_path, (arg if arg is not None else default))
- else:
- # cannot provide any meaningful value without dir_path; this is
- # only to import some older format of `qubes.xml` (for example
- # during migration from older release)
- return None
-
- def _absolute_path_gen(self, default):
- return lambda value: self.absolute_path(value, default)
-
- def relative_path(self, arg):
- return arg.replace(self.dir_path + '/', '')
-
- @property
- def qid(self):
- return self.__qid
-
- @property
- def label(self):
- return self._label
-
- @label.setter
- def label(self, new_label):
- self._label = new_label
- if self.icon_path:
- try:
- os.remove(self.icon_path)
- except:
- pass
- if hasattr(os, "symlink"):
- os.symlink (new_label.icon_path, self.icon_path)
- # FIXME: some os-independent wrapper?
- subprocess.call(['sudo', 'xdg-icon-resource', 'forceupdate'])
- else:
- shutil.copy(new_label.icon_path, self.icon_path)
-
- # fire hooks
- for hook in self.hooks_label_setter:
- hook(self, new_label)
-
- @property
- def netvm(self):
- return self._netvm
-
- # Don't know how properly call setter from base class, so workaround it...
- @netvm.setter
- def netvm(self, new_netvm):
- self._set_netvm(new_netvm)
- # fire hooks
- for hook in self.hooks_netvm_setter:
- hook(self, new_netvm)
-
- def _set_netvm(self, new_netvm):
- self.log.debug('netvm = {!r}'.format(new_netvm))
- if new_netvm and not new_netvm.is_netvm():
- raise ValueError("Vm {!r} does not provide network".format(
- new_netvm))
- if self.is_running() and new_netvm is not None and not new_netvm.is_running():
- raise QubesException("Cannot dynamically attach to stopped NetVM")
- if self.netvm is not None:
- self.netvm.connected_vms.pop(self.qid)
- if self.is_running():
- self.detach_network()
-
- if hasattr(self.netvm, 'post_vm_net_detach'):
- self.netvm.post_vm_net_detach(self)
-
- if new_netvm is not None:
- new_netvm.connected_vms[self.qid]=self
-
- self._netvm = new_netvm
-
- if new_netvm is None:
- return
-
- if self.is_running():
- # refresh IP, DNS etc
- self.create_qubesdb_entries()
- self.attach_network()
- if hasattr(self.netvm, 'post_vm_net_attach'):
- self.netvm.post_vm_net_attach(self)
-
- @property
- def ip(self):
- if self.netvm is not None:
- return self.netvm.get_ip_for_vm(self.qid)
- else:
- return None
-
- @property
- def netmask(self):
- if self.netvm is not None:
- return self.netvm.netmask
- else:
- return None
-
- @property
- def gateway(self):
- # This is gateway IP for _other_ VMs, so make sense only in NetVMs
- return None
-
- @property
- def secondary_dns(self):
- if self.netvm is not None:
- return self.netvm.secondary_dns
- else:
- return None
-
- @property
- def vif(self):
- if self.xid < 0:
- return None
- if self.netvm is None:
- return None
- return "vif{0}.+".format(self.xid)
-
- @property
- def mac(self):
- if self._mac is not None:
- return self._mac
- else:
- return "00:16:3E:5E:6C:{qid:02X}".format(qid=self.qid)
-
- @mac.setter
- def mac(self, new_mac):
- self._mac = new_mac
-
- @property
- def kernel(self):
- return self._kernel
-
- @kernel.setter
- def kernel(self, new_value):
- if new_value is not None:
- if not os.path.exists(os.path.join(system_path[
- 'qubes_kernels_base_dir'], new_value)):
- raise QubesException("Kernel '%s' not installed" % new_value)
- for f in ('vmlinuz', 'initramfs'):
- if not os.path.exists(os.path.join(
- system_path['qubes_kernels_base_dir'], new_value, f)):
- raise QubesException(
- "Kernel '%s' not properly installed: missing %s "
- "file" % (new_value, f))
- self._kernel = new_value
- self.uses_default_kernel = False
-
- @property
- def updateable(self):
- return self.template is None
-
- # Leaved for compatibility
- def is_updateable(self):
- return self.updateable
-
- @property
- def default_user(self):
- if self.template is not None:
- return self.template.default_user
- else:
- return self._default_user
-
- @default_user.setter
- def default_user(self, value):
- self._default_user = value
-
- def is_networked(self):
- if self.is_netvm():
- return True
-
- if self.netvm is not None:
- return True
- else:
- return False
-
- def verify_name(self, name):
- if not isinstance(self.__basic_parse_xml_attr(name), str):
- return False
- if len(name) > 31:
- return False
- if name == 'lost+found':
- # avoid conflict when /var/lib/qubes/appvms is mounted on
- # separate partition
- return False
- if name.endswith('-dm'):
- # avoid conflict with device model stubdomain names for HVMs
- return False
- return re.match(r"^[a-zA-Z][a-zA-Z0-9_.-]*$", name) is not None
-
- def pre_rename(self, new_name):
- if self.autostart:
- subprocess.check_call(['sudo', 'systemctl', '-q', 'disable',
- 'qubes-vm@{}.service'.format(self.name)])
- # fire hooks
- for hook in self.hooks_pre_rename:
- hook(self, new_name)
-
- def set_name(self, name):
- self.log.debug('name = {!r}'.format(name))
- if self.is_running():
- raise QubesException("Cannot change name of running VM!")
-
- if not self.verify_name(name):
- raise QubesException("Invalid VM name")
-
- if self.installed_by_rpm:
- raise QubesException("Cannot rename VM installed by RPM -- first clone VM and then use yum to remove package.")
-
- assert self._collection is not None
- if self._collection.get_vm_by_name(name):
- raise QubesException("VM with this name already exists")
-
- self.pre_rename(name)
- try:
- self.libvirt_domain.undefine()
- except libvirt.libvirtError as e:
- if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
- pass
- else:
- raise
- if self._qdb_connection:
- self._qdb_connection.close()
- self._qdb_connection = None
-
- new_conf = os.path.join(self.dir_path, name + '.conf')
- if os.path.exists(self.conf_file):
- os.rename(self.conf_file, new_conf)
- old_dirpath = self.dir_path
- self.storage.rename(self.name, name)
- new_dirpath = self.storage.vmdir
- self.dir_path = new_dirpath
- old_name = self.name
- self.name = name
- if self.conf_file is not None:
- self.conf_file = new_conf.replace(old_dirpath, new_dirpath)
- if self.icon_path is not None:
- self.icon_path = self.icon_path.replace(old_dirpath, new_dirpath)
- if hasattr(self, 'kernels_dir') and self.kernels_dir is not None:
- self.kernels_dir = self.kernels_dir.replace(old_dirpath, new_dirpath)
- if self.firewall_conf is not None:
- self.firewall_conf = self.firewall_conf.replace(old_dirpath,
- new_dirpath)
-
- self._update_libvirt_domain()
- self.post_rename(old_name)
-
- def post_rename(self, old_name):
- if self.autostart:
- # force setter to be called again
- self.autostart = self.autostart
- # fire hooks
- for hook in self.hooks_post_rename:
- hook(self, old_name)
-
- @property
- def internal(self):
- return self._internal
-
- @internal.setter
- def internal(self, value):
- oldvalue = self._internal
- self._internal = value
- self.post_set_attr('internal', value, oldvalue)
-
- @property
- def dispvm_netvm(self):
- if self.uses_default_dispvm_netvm:
- return self.netvm
- else:
- if isinstance(self._dispvm_netvm, int):
- if self._dispvm_netvm in self._collection:
- return self._collection[self._dispvm_netvm]
- else:
- return None
- else:
- return self._dispvm_netvm
-
- @dispvm_netvm.setter
- def dispvm_netvm(self, value):
- if value and not value.is_netvm():
- raise ValueError("Vm {!r} does not provide network".format(
- value))
- self._dispvm_netvm = value
-
- @property
- def autostart(self):
- return self._autostart
-
- @autostart.setter
- def autostart(self, value):
- if value:
- retcode = subprocess.call(["sudo", "ln", "-sf",
- "/usr/lib/systemd/system/qubes-vm@.service",
- "/etc/systemd/system/multi-user.target.wants/qubes-vm@%s.service" % self.name])
- else:
- retcode = subprocess.call(["sudo", "systemctl", "disable", "qubes-vm@%s.service" % self.name])
- if retcode != 0:
- raise QubesException("Failed to set autostart for VM via systemctl")
- self._autostart = bool(value)
-
- @classmethod
- def is_template_compatible(cls, template):
- """Check if given VM can be a template for this VM"""
- # FIXME: check if the value is instance of QubesTemplateVM, not the VM
- # type. The problem is while this file is loaded, QubesTemplateVM is
- # not defined yet.
- if template and (not template.is_template() or template.type != "TemplateVM"):
- return False
- return True
-
- @property
- def template(self):
- return self._template
-
- @template.setter
- def template(self, value):
- if self._template is None and value is not None:
- raise QubesException("Cannot set template for standalone VM")
- if value and not self.is_template_compatible(value):
- raise QubesException("Incompatible template type %s with VM of type %s" % (value.type, self.type))
- self._template = value
-
- def is_template(self):
- return False
-
- def is_appvm(self):
- return False
-
- def is_netvm(self):
- return False
-
- def is_proxyvm(self):
- return False
-
- def is_disposablevm(self):
- return False
-
- @property
- def qdb(self):
- if self._qdb_connection is None:
- from qubes.qdb import QubesDB
- self._qdb_connection = QubesDB(self.name)
- return self._qdb_connection
-
- @property
- def xid(self):
- try:
- return self.libvirt_domain.ID()
- except libvirt.libvirtError as e:
- if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
- return -1
- else:
- print >>sys.stderr, "libvirt error code: {!r}".format(
- e.get_error_code())
- raise
-
-
- def get_xid(self):
- # obsoleted
- return self.xid
-
- def _update_libvirt_domain(self):
- domain_config = self.create_config_file()
- try:
- self._libvirt_domain = vmm.libvirt_conn.defineXML(domain_config)
- except libvirt.libvirtError as e:
- # shouldn't this be in QubesHVm implementation?
- if e.get_error_code() == libvirt.VIR_ERR_OS_TYPE and \
- e.get_str2() == 'hvm':
- raise QubesException("HVM domains not supported on this "
- "machine. Check BIOS settings for "
- "VT-x/AMD-V extensions.")
- else:
- raise e
- self.uuid = uuid.UUID(bytes=self._libvirt_domain.UUID())
-
- @property
- def libvirt_domain(self):
- if self._libvirt_domain is None:
- if self.uuid is not None:
- self._libvirt_domain = vmm.libvirt_conn.lookupByUUID(self.uuid.bytes)
- else:
- self._libvirt_domain = vmm.libvirt_conn.lookupByName(self.name)
- self.uuid = uuid.UUID(bytes=self._libvirt_domain.UUID())
- return self._libvirt_domain
-
- def get_uuid(self):
- # obsoleted
- return self.uuid
-
- def refresh(self):
- self._libvirt_domain = None
- self._qdb_connection = None
-
- def get_mem(self):
- if dry_run:
- return 666
-
- try:
- if not self.libvirt_domain.isActive():
- return 0
- return self.libvirt_domain.info()[1]
- except libvirt.libvirtError as e:
- if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
- return 0
- # libxl_domain_info failed - domain no longer exists
- elif e.get_error_code() == libvirt.VIR_ERR_INTERNAL_ERROR:
- return 0
- elif e.get_error_code() is None: # unknown...
- return 0
- else:
- print >>sys.stderr, "libvirt error code: {!r}".format(
- e.get_error_code())
- raise
-
- def get_cputime(self):
- if dry_run:
- return 666
-
- try:
- if not self.libvirt_domain.isActive():
- return 0
- return self.libvirt_domain.info()[4]
- except libvirt.libvirtError as e:
- if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
- return 0
- # libxl_domain_info failed - domain no longer exists
- elif e.get_error_code() == libvirt.VIR_ERR_INTERNAL_ERROR:
- return 0
- elif e.get_error_code() is None: # unknown...
- return 0
- else:
- print >>sys.stderr, "libvirt error code: {!r}".format(
- e.get_error_code())
- raise
-
- def get_mem_static_max(self):
- if dry_run:
- return 666
-
- try:
- return self.libvirt_domain.maxMemory()
- except libvirt.libvirtError as e:
- if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
- return 0
- else:
- raise
-
- def get_prefmem(self):
- # TODO: qmemman is still xen specific
- untrusted_meminfo_key = vmm.xs.read('',
- '/local/domain/%s/memory/meminfo'
- % self.xid)
- if untrusted_meminfo_key is None or untrusted_meminfo_key == '':
- return 0
- domain = qmemman.DomainState(self.xid)
- qmemman_algo.refresh_meminfo_for_domain(domain, untrusted_meminfo_key)
- domain.memory_maximum = self.get_mem_static_max()*1024
- return qmemman_algo.prefmem(domain)/1024
-
- def get_per_cpu_time(self):
- if dry_run:
- import random
- return random.random() * 100
-
- try:
- if self.libvirt_domain.isActive():
- return self.libvirt_domain.getCPUStats(
- libvirt.VIR_NODE_CPU_STATS_ALL_CPUS, 0)[0]['cpu_time']/10**9
- else:
- return 0
- except libvirt.libvirtError as e:
- if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
- return 0
- else:
- print >>sys.stderr, "libvirt error code: {!r}".format(
- e.get_error_code())
- raise
-
- def get_disk_utilization_root_img(self):
- return qubes.qubesutils.get_disk_usage(self.root_img)
-
- def get_root_img_sz(self):
- if not os.path.exists(self.root_img):
- return 0
-
- return os.path.getsize(self.root_img)
-
- def get_power_state(self):
- if dry_run:
- return "NA"
-
- try:
- libvirt_domain = self.libvirt_domain
- if libvirt_domain.isActive():
- if libvirt_domain.state()[0] == libvirt.VIR_DOMAIN_PAUSED:
- return "Paused"
- elif libvirt_domain.state()[0] == libvirt.VIR_DOMAIN_CRASHED:
- return "Crashed"
- elif libvirt_domain.state()[0] == libvirt.VIR_DOMAIN_SHUTDOWN:
- return "Halting"
- elif libvirt_domain.state()[0] == libvirt.VIR_DOMAIN_SHUTOFF:
- return "Dying"
- elif libvirt_domain.state()[0] == libvirt.VIR_DOMAIN_PMSUSPENDED:
- return "Suspended"
- else:
- if not self.is_fully_usable():
- return "Transient"
- else:
- return "Running"
- else:
- return 'Halted'
- except libvirt.libvirtError as e:
- if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
- return "Halted"
- else:
- raise
-
-
- def is_guid_running(self):
- xid = self.xid
- if xid < 0:
- return False
- if not os.path.exists('/var/run/qubes/guid-running.%d' % xid):
- return False
- return True
-
- def is_qrexec_running(self):
- if self.xid < 0:
- return False
- return os.path.exists('/var/run/qubes/qrexec.%s' % self.name)
-
- def is_fully_usable(self):
- # Running gui-daemon implies also VM running
- if not self.is_guid_running():
- return False
- if not self.is_qrexec_running():
- return False
- return True
-
- def is_running(self):
- if vmm.offline_mode:
- return False
- try:
- if self.libvirt_domain.isActive():
- return True
- else:
- return False
- except libvirt.libvirtError as e:
- if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
- return False
- # libxl_domain_info failed - domain no longer exists
- elif e.get_error_code() == libvirt.VIR_ERR_INTERNAL_ERROR:
- return False
- elif e.get_error_code() is None: # unknown...
- return False
- else:
- print >>sys.stderr, "libvirt error code: {!r}".format(
- e.get_error_code())
- raise
-
- def is_paused(self):
- try:
- if self.libvirt_domain.state()[0] == libvirt.VIR_DOMAIN_PAUSED:
- return True
- else:
- return False
- except libvirt.libvirtError as e:
- if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
- return False
- # libxl_domain_info failed - domain no longer exists
- elif e.get_error_code() == libvirt.VIR_ERR_INTERNAL_ERROR:
- return False
- elif e.get_error_code() is None: # unknown...
- return False
- else:
- print >>sys.stderr, "libvirt error code: {!r}".format(
- e.get_error_code())
- raise
-
- def get_start_time(self):
- if not self.is_running():
- return None
-
- # TODO
- uuid = self.uuid
-
- start_time = vmm.xs.read('', "/vm/%s/start_time" % str(uuid))
- if start_time:
- return datetime.datetime.fromtimestamp(float(start_time))
- else:
- return None
-
- def is_outdated(self):
- # Makes sense only on VM based on template
- if self.template is None:
- return False
-
- if not self.is_running():
- return False
-
- if not hasattr(self.template, 'rootcow_img'):
- return False
-
- rootimg_inode = os.stat(self.template.root_img)
- try:
- rootcow_inode = os.stat(self.template.rootcow_img)
- except OSError:
- # The only case when rootcow_img doesn't exists is in the middle of
- # commit_changes, so VM is outdated right now
- return True
-
- current_dmdev = "/dev/mapper/snapshot-{0:x}:{1}-{2:x}:{3}".format(
- rootimg_inode[2], rootimg_inode[1],
- rootcow_inode[2], rootcow_inode[1])
-
- # FIXME
- # 51712 (0xCA00) is xvda
- # backend node name not available through xenapi :(
- used_dmdev = vmm.xs.read('', "/local/domain/0/backend/vbd/{0}/51712/node".format(self.xid))
-
- return used_dmdev != current_dmdev
-
- @property
- def private_img(self):
- return self.storage.private_img
-
- @property
- def root_img(self):
- return self.storage.root_img
-
- @property
- def volatile_img(self):
- return self.storage.volatile_img
-
- def get_disk_utilization(self):
- return qubes.qubesutils.get_disk_usage(self.dir_path)
-
- def get_disk_utilization_private_img(self):
- return qubes.qubesutils.get_disk_usage(self.private_img)
-
- def get_private_img_sz(self):
- return self.storage.get_private_img_sz()
-
- def resize_private_img(self, size):
- assert size >= self.get_private_img_sz(), "Cannot shrink private.img"
-
- # resize the image
- self.storage.resize_private_img(size)
-
- # and then the filesystem
- retcode = 0
- if self.is_running():
- retcode = self.run("while [ \"`blockdev --getsize64 /dev/xvdb`\" -lt {0} ]; do ".format(size) +
- "head /dev/xvdb > /dev/null; sleep 0.2; done; resize2fs /dev/xvdb", user="root", wait=True)
- if retcode != 0:
- raise QubesException("resize2fs failed")
-
-
-
- # FIXME: should be outside of QubesVM?
- def get_timezone(self):
- # fc18
- if os.path.islink('/etc/localtime'):
- return '/'.join(os.readlink('/etc/localtime').split('/')[-2:])
- # <=fc17
- elif os.path.exists('/etc/sysconfig/clock'):
- clock_config = open('/etc/sysconfig/clock', "r")
- clock_config_lines = clock_config.readlines()
- clock_config.close()
- zone_re = re.compile(r'^ZONE="(.*)"')
- for line in clock_config_lines:
- line_match = zone_re.match(line)
- if line_match:
- return line_match.group(1)
- else:
- # last resort way, some applications makes /etc/localtime
- # hardlink instead of symlink...
- tz_info = os.stat('/etc/localtime')
- if not tz_info:
- return None
- if tz_info.st_nlink > 1:
- p = subprocess.Popen(['find', '/usr/share/zoneinfo',
- '-inum', str(tz_info.st_ino),
- '-print', '-quit'],
- stdout=subprocess.PIPE)
- tz_path = p.communicate()[0].strip()
- return tz_path.replace('/usr/share/zoneinfo/', '')
- return None
-
- def cleanup_vifs(self):
- """
- Xend does not remove vif when backend domain is down, so we must do it
- manually
- """
-
- # FIXME: remove this?
- if not self.is_running():
- return
-
- dev_basepath = '/local/domain/%d/device/vif' % self.xid
- for dev in (vmm.xs.ls('', dev_basepath) or []):
- # check if backend domain is alive
- backend_xid = int(vmm.xs.read('', '%s/%s/backend-id' % (dev_basepath, dev)))
- if backend_xid in vmm.libvirt_conn.listDomainsID():
- # check if device is still active
- if vmm.xs.read('', '%s/%s/state' % (dev_basepath, dev)) == '4':
- continue
- # remove dead device
- vmm.xs.rm('', '%s/%s' % (dev_basepath, dev))
-
- def create_qubesdb_entries(self):
- if dry_run:
- return
-
- self.qdb.write("/name", self.name)
- self.qdb.write("/qubes-vm-type", self.type)
- self.qdb.write("/qubes-vm-updateable", str(self.updateable))
- self.qdb.write("/qubes-vm-persistence",
- "full" if self.updateable else "rw-only")
- self.qdb.write("/qubes-base-template",
- self.template.name if self.template else '')
-
- if self.is_netvm():
- self.qdb.write("/qubes-netvm-gateway", self.gateway)
- self.qdb.write("/qubes-netvm-primary-dns", self.gateway)
- self.qdb.write("/qubes-netvm-secondary-dns", self.secondary_dns)
- self.qdb.write("/qubes-netvm-netmask", self.netmask)
- self.qdb.write("/qubes-netvm-network", self.network)
-
- if self.netvm is not None:
- self.qdb.write("/qubes-ip", self.ip)
- self.qdb.write("/qubes-netmask", self.netvm.netmask)
- self.qdb.write("/qubes-gateway", self.netvm.gateway)
- self.qdb.write("/qubes-primary-dns", self.netvm.gateway)
- self.qdb.write("/qubes-secondary-dns", self.netvm.secondary_dns)
-
- tzname = self.get_timezone()
- if tzname:
- self.qdb.write("/qubes-timezone", tzname)
-
- for srv in self.services.keys():
- # convert True/False to "1"/"0"
- self.qdb.write("/qubes-service/{0}".format(srv),
- str(int(self.services[srv])))
-
- self.qdb.write("/qubes-block-devices", '')
-
- self.qdb.write("/qubes-usb-devices", '')
-
- self.qdb.write("/qubes-debug-mode", str(int(self.debug)))
-
- self.provide_random_seed_to_vm()
-
- # TODO: Currently the whole qmemman is quite Xen-specific, so stay with
- # xenstore for it until decided otherwise
- if qmemman_present:
- vmm.xs.set_permissions('', '/local/domain/{0}/memory'.format(self.xid),
- [{ 'dom': self.xid }])
-
- # fire hooks
- for hook in self.hooks_create_qubesdb_entries:
- hook(self)
-
- def provide_random_seed_to_vm(self):
- f = open('/dev/urandom', 'r')
- s = f.read(64)
- if len(s) != 64:
- raise IOError("failed to read seed from /dev/urandom")
- f.close()
- self.qdb.write("/qubes-random-seed", base64.b64encode(hashlib.sha512(s).digest()))
-
- def _format_net_dev(self, ip, mac, backend):
- template = " \n" \
- " \n" \
- " \n" \
- " \n" \
- " \n" \
- " \n"
- return template.format(ip=ip, mac=mac, backend=backend)
-
- def _format_pci_dev(self, address):
- template = " \n" \
- " \n" \
- " \n" \
- " \n" \
- " \n"
- dev_match = re.match('([0-9a-f]+):([0-9a-f]+)\.([0-9a-f]+)', address)
- if not dev_match:
- raise QubesException("Invalid PCI device address: %s" % address)
- return template.format(
- bus=dev_match.group(1),
- slot=dev_match.group(2),
- fun=dev_match.group(3),
- strictreset=("" if self.pci_strictreset else
- " nostrictreset='yes'"),
- )
-
- def get_config_params(self):
- args = {}
- args['name'] = self.name
- if hasattr(self, 'kernels_dir'):
- args['kerneldir'] = self.kernels_dir
- args['uuidnode'] = "%s" % str(self.uuid) if self.uuid else ""
- args['vmdir'] = self.dir_path
- args['pcidevs'] = ''.join(map(self._format_pci_dev, self.pcidevs))
- args['mem'] = str(self.memory)
- if self.maxmem < self.memory:
- args['mem'] = str(self.maxmem)
- args['maxmem'] = str(self.maxmem)
- if 'meminfo-writer' in self.services and not self.services['meminfo-writer']:
- # If dynamic memory management disabled, set maxmem=mem
- args['maxmem'] = args['mem']
- args['vcpus'] = str(self.vcpus)
- args['features'] = ''
- if self.netvm is not None:
- args['ip'] = self.ip
- args['mac'] = self.mac
- args['gateway'] = self.netvm.gateway
- args['dns1'] = self.netvm.gateway
- args['dns2'] = self.secondary_dns
- args['netmask'] = self.netmask
- args['netdev'] = self._format_net_dev(self.ip, self.mac, self.netvm.name)
- args['network_begin'] = ''
- args['network_end'] = ''
- args['no_network_begin'] = ''
- else:
- args['ip'] = ''
- args['mac'] = ''
- args['gateway'] = ''
- args['dns1'] = ''
- args['dns2'] = ''
- args['netmask'] = ''
- args['netdev'] = ''
- args['network_begin'] = ''
- args['no_network_begin'] = ''
- args['no_network_end'] = ''
- if len(self.pcidevs) and self.pci_e820_host:
- args['features'] = ''
- args.update(self.storage.get_config_params())
- if hasattr(self, 'kernelopts'):
- args['kernelopts'] = self.kernelopts
- if self.debug:
- print >> sys.stderr, "--> Debug mode: adding 'earlyprintk=xen' to kernel opts"
- args['kernelopts'] += ' earlyprintk=xen'
-
- # fire hooks
- for hook in self.hooks_get_config_params:
- args = hook(self, args)
-
- return args
-
- @property
- def uses_custom_config(self):
- return self.conf_file != self.absolute_path(self.name + ".conf", None)
-
- def create_config_file(self, file_path = None, prepare_dvm = False):
- if file_path is None:
- file_path = self.conf_file
- if self.uses_custom_config:
- conf_appvm = open(file_path, "r")
- domain_config = conf_appvm.read()
- conf_appvm.close()
- return domain_config
-
- f_conf_template = open(self.config_file_template, 'r')
- conf_template = f_conf_template.read()
- f_conf_template.close()
-
- template_params = self.get_config_params()
- if prepare_dvm:
- template_params['name'] = '%NAME%'
- template_params['privatedev'] = ''
- template_params['netdev'] = re.sub(r"address='[0-9.]*'", "address='%IP%'", template_params['netdev'])
- domain_config = conf_template.format(**template_params)
-
- # FIXME: This is only for debugging purposes
- old_umask = os.umask(002)
- try:
- if os.path.exists(file_path):
- os.unlink(file_path)
- conf_appvm = open(file_path, "w")
- conf_appvm.write(domain_config)
- conf_appvm.close()
- except:
- # Ignore errors
- pass
- finally:
- os.umask(old_umask)
-
- return domain_config
-
- def create_on_disk(self, verbose=False, source_template = None):
- self.log.debug('create_on_disk(source_template={!r})'.format(
- source_template))
- if source_template is None:
- source_template = self.template
- assert source_template is not None
-
- if dry_run:
- return
-
- self.storage.create_on_disk(verbose, source_template)
-
- if self.updateable:
- kernels_dir = source_template.kernels_dir
- if verbose:
- print >> sys.stderr, "--> Copying the kernel (set kernel \"none\" to use it): {0}".\
- format(kernels_dir)
-
- os.mkdir (self.dir_path + '/kernels')
- for f in ("vmlinuz", "initramfs", "modules.img"):
- shutil.copy(os.path.join(kernels_dir, f),
- os.path.join(self.dir_path, vm_files["kernels_subdir"], f))
-
- if verbose:
- print >> sys.stderr, "--> Creating icon symlink: {0} -> {1}".format(self.icon_path, self.label.icon_path)
- if hasattr(os, "symlink"):
- os.symlink (self.label.icon_path, self.icon_path)
- else:
- shutil.copy(self.label.icon_path, self.icon_path)
-
- # Make sure that we have UUID allocated
- if not vmm.offline_mode:
- self._update_libvirt_domain()
- else:
- self.uuid = uuid.uuid4()
-
- # fire hooks
- for hook in self.hooks_create_on_disk:
- hook(self, verbose, source_template=source_template)
-
- def get_clone_attrs(self):
- attrs = ['kernel', 'uses_default_kernel', 'netvm', 'uses_default_netvm',
- 'memory', 'maxmem', 'kernelopts', 'uses_default_kernelopts',
- 'services', 'vcpus', '_mac', 'pcidevs', 'include_in_backups',
- '_label', 'default_user', 'qrexec_timeout']
-
- # fire hooks
- for hook in self.hooks_get_clone_attrs:
- attrs = hook(self, attrs)
-
- return attrs
-
- def clone_attrs(self, src_vm, fail_on_error=True):
- self._do_not_reset_firewall = True
- for prop in self.get_clone_attrs():
- try:
- val = getattr(src_vm, prop)
- if isinstance(val, dict):
- val = val.copy()
- setattr(self, prop, val)
- except Exception as e:
- if fail_on_error:
- self._do_not_reset_firewall = False
- raise
- else:
- print >>sys.stderr, "WARNING: %s" % str(e)
- self._do_not_reset_firewall = False
-
- def clone_disk_files(self, src_vm, verbose):
- if dry_run:
- return
-
- if src_vm.is_running():
- raise QubesException("Attempt to clone a running VM!")
-
- self.storage.clone_disk_files(src_vm, verbose)
-
- if src_vm.icon_path is not None and self.icon_path is not None:
- if os.path.exists (src_vm.dir_path):
- if os.path.islink(src_vm.icon_path):
- icon_path = os.readlink(src_vm.icon_path)
- if verbose:
- print >> sys.stderr, "--> Creating icon symlink: {0} -> {1}".format(self.icon_path, icon_path)
- os.symlink (icon_path, self.icon_path)
- else:
- if verbose:
- print >> sys.stderr, "--> Copying icon: {0} -> {1}".format(src_vm.icon_path, self.icon_path)
- shutil.copy(src_vm.icon_path, self.icon_path)
-
- if src_vm.has_firewall():
- self.write_firewall_conf(src_vm.get_firewall_conf())
-
- # Make sure that we have UUID allocated
- self._update_libvirt_domain()
-
- # fire hooks
- for hook in self.hooks_clone_disk_files:
- hook(self, src_vm, verbose)
-
- def verify_files(self):
- if dry_run:
- return
-
- self.storage.verify_files()
-
- if not os.path.exists (os.path.join(self.kernels_dir, 'vmlinuz')):
- raise QubesException (
- "VM kernel does not exist: {0}".\
- format(os.path.join(self.kernels_dir, 'vmlinuz')))
-
- if not os.path.exists (os.path.join(self.kernels_dir, 'initramfs')):
- raise QubesException (
- "VM initramfs does not exist: {0}".\
- format(os.path.join(self.kernels_dir, 'initramfs')))
-
- # fire hooks
- for hook in self.hooks_verify_files:
- hook(self)
-
- return True
-
- def remove_from_disk(self):
- self.log.debug('remove_from_disk()')
- if dry_run:
- return
-
- # fire hooks
- for hook in self.hooks_remove_from_disk:
- hook(self)
-
- try:
- self.libvirt_domain.undefine()
- except libvirt.libvirtError as e:
- if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
- # already undefined
- pass
- else:
- print >>sys.stderr, "libvirt error code: {!r}".format(
- e.get_error_code())
- raise
-
- if os.path.exists("/etc/systemd/system/multi-user.target.wants/qubes-vm@" + self.name + ".service"):
- retcode = subprocess.call(["sudo", "systemctl", "-q", "disable",
- "qubes-vm@" + self.name + ".service"])
- if retcode != 0:
- raise QubesException("Failed to delete autostart entry for VM")
-
- self.storage.remove_from_disk()
-
- def write_firewall_conf(self, conf):
- defaults = self.get_firewall_conf()
- expiring_rules_present = False
- for item in defaults.keys():
- if item not in conf:
- conf[item] = defaults[item]
-
- root = lxml.etree.Element(
- "QubesFirewallRules",
- policy = "allow" if conf["allow"] else "deny",
- dns = "allow" if conf["allowDns"] else "deny",
- icmp = "allow" if conf["allowIcmp"] else "deny",
- yumProxy = "allow" if conf["allowYumProxy"] else "deny"
- )
-
- for rule in conf["rules"]:
- # For backward compatibility
- if "proto" not in rule:
- if rule["portBegin"] is not None and rule["portBegin"] > 0:
- rule["proto"] = "tcp"
- else:
- rule["proto"] = "any"
- element = lxml.etree.Element(
- "rule",
- address=rule["address"],
- proto=str(rule["proto"]),
- )
- if rule["netmask"] is not None and rule["netmask"] != 32:
- element.set("netmask", str(rule["netmask"]))
- if rule.get("portBegin", None) is not None and \
- rule["portBegin"] > 0:
- element.set("port", str(rule["portBegin"]))
- if rule.get("portEnd", None) is not None and rule["portEnd"] > 0:
- element.set("toport", str(rule["portEnd"]))
- if "expire" in rule:
- element.set("expire", str(rule["expire"]))
- expiring_rules_present = True
-
- root.append(element)
-
- tree = lxml.etree.ElementTree(root)
-
- try:
- old_umask = os.umask(002)
- with open(self.firewall_conf, 'w') as f:
- tree.write(f, encoding="UTF-8", pretty_print=True)
- f.close()
- os.umask(old_umask)
- except EnvironmentError as err:
- print >> sys.stderr, "{0}: save error: {1}".format(
- os.path.basename(sys.argv[0]), err)
- return False
-
- # Automatically enable/disable 'yum-proxy-setup' service based on allowYumProxy
- if conf['allowYumProxy']:
- self.services['yum-proxy-setup'] = True
- else:
- if self.services.has_key('yum-proxy-setup'):
- self.services.pop('yum-proxy-setup')
-
- if expiring_rules_present:
- subprocess.call(["sudo", "systemctl", "start",
- "qubes-reload-firewall@%s.timer" % self.name])
-
- return True
-
- def has_firewall(self):
- return os.path.exists (self.firewall_conf)
-
- def get_firewall_defaults(self):
- return { "rules": list(), "allow": True, "allowDns": True, "allowIcmp": True, "allowYumProxy": False }
-
- def get_firewall_conf(self):
- conf = self.get_firewall_defaults()
-
- try:
- tree = lxml.etree.parse(self.firewall_conf)
- root = tree.getroot()
-
- conf["allow"] = (root.get("policy") == "allow")
- conf["allowDns"] = (root.get("dns") == "allow")
- conf["allowIcmp"] = (root.get("icmp") == "allow")
- conf["allowYumProxy"] = (root.get("yumProxy") == "allow")
-
- for element in root:
- rule = {}
- attr_list = ("address", "netmask", "proto", "port", "toport",
- "expire")
-
- for attribute in attr_list:
- rule[attribute] = element.get(attribute)
-
- if rule["netmask"] is not None:
- rule["netmask"] = int(rule["netmask"])
- else:
- rule["netmask"] = 32
-
- if rule["port"] is not None:
- rule["portBegin"] = int(rule["port"])
- else:
- # backward compatibility
- rule["portBegin"] = 0
-
- # For backward compatibility
- if rule["proto"] is None:
- if rule["portBegin"] > 0:
- rule["proto"] = "tcp"
- else:
- rule["proto"] = "any"
-
- if rule["toport"] is not None:
- rule["portEnd"] = int(rule["toport"])
- else:
- rule["portEnd"] = None
-
- if rule["expire"] is not None:
- rule["expire"] = int(rule["expire"])
- if rule["expire"] <= int(datetime.datetime.now().strftime(
- "%s")):
- continue
- else:
- del(rule["expire"])
-
- del(rule["port"])
- del(rule["toport"])
-
- conf["rules"].append(rule)
-
- except EnvironmentError as err:
- return conf
- except (xml.parsers.expat.ExpatError,
- ValueError, LookupError) as err:
- print("{0}: load error: {1}".format(
- os.path.basename(sys.argv[0]), err))
- return None
-
- return conf
-
- def pci_add(self, pci):
- self.log.debug('pci_add(pci={!r})'.format(pci))
- if not os.path.exists('/sys/bus/pci/devices/0000:%s' % pci):
- raise QubesException("Invalid PCI device: %s" % pci)
- if self.pcidevs.count(pci):
- # already added
- return
- self.pcidevs.append(pci)
- if self.is_running():
- try:
- subprocess.check_call(['sudo', system_path["qubes_pciback_cmd"], pci])
- subprocess.check_call(['sudo', 'xl', 'pci-attach', str(self.xid), pci])
- except Exception as e:
- print >>sys.stderr, "Failed to attach PCI device on the fly " \
- "(%s), changes will be seen after VM restart" % str(e)
-
- def pci_remove(self, pci):
- self.log.debug('pci_remove(pci={!r})'.format(pci))
- if not self.pcidevs.count(pci):
- # not attached
- return
- self.pcidevs.remove(pci)
- if self.is_running():
- p = subprocess.Popen(['xl', 'pci-list', str(self.xid)],
- stdout=subprocess.PIPE)
- result = p.communicate()
- m = re.search(r"^(\d+.\d+)\s+0000:%s$" % pci, result[0], flags=re.MULTILINE)
- if not m:
- print >>sys.stderr, "Device %s already detached" % pci
- return
- vmdev = m.group(1)
- try:
- self.run_service("qubes.DetachPciDevice",
- user="root", input="00:%s" % vmdev)
- subprocess.check_call(['sudo', 'xl', 'pci-detach', str(self.xid), pci])
- except Exception as e:
- print >>sys.stderr, "Failed to detach PCI device on the fly " \
- "(%s), changes will be seen after VM restart" % str(e)
-
- def run(self, command, user = None, verbose = True, autostart = False,
- notify_function = None,
- passio = False, passio_popen = False, passio_stderr=False,
- ignore_stderr=False, localcmd = None, wait = False, gui = True,
- filter_esc = False):
- """command should be in form 'cmdline'
- When passio_popen=True, popen object with stdout connected to pipe.
- When additionally passio_stderr=True, stderr also is connected to pipe.
- When ignore_stderr=True, stderr is connected to /dev/null.
- """
-
- self.log.debug(
- 'run(command={!r}, user={!r}, passio={!r}, wait={!r})'.format(
- command, user, passio, wait))
-
- if user is None:
- user = self.default_user
- null = None
- if not self.is_running() and not self.is_paused():
- if not autostart:
- raise QubesException("VM not running")
-
- try:
- if notify_function is not None:
- notify_function ("info", "Starting the '{0}' VM...".format(self.name))
- elif verbose:
- print >> sys.stderr, "Starting the VM '{0}'...".format(self.name)
- self.start(verbose=verbose, start_guid = gui, notify_function=notify_function)
-
- except (IOError, OSError, QubesException) as err:
- raise QubesException("Error while starting the '{0}' VM: {1}".format(self.name, err))
- except (MemoryError) as err:
- raise QubesException("Not enough memory to start '{0}' VM! "
- "Close one or more running VMs and try "
- "again.".format(self.name))
-
- if self.is_paused():
- raise QubesException("VM is paused")
- if not self.is_qrexec_running():
- raise QubesException(
- "Domain '{}': qrexec not connected.".format(self.name))
-
- if gui and os.getenv("DISPLAY") is not None and not self.is_guid_running():
- self.start_guid(verbose = verbose, notify_function = notify_function)
-
- args = [system_path["qrexec_client_path"], "-d", str(self.name), "%s:%s" % (user, command)]
- if localcmd is not None:
- args += [ "-l", localcmd]
- if filter_esc:
- args += ["-t"]
- if os.isatty(sys.stderr.fileno()):
- args += ["-T"]
-
- call_kwargs = {}
- if ignore_stderr or not passio:
- null = open("/dev/null", "w+")
- call_kwargs['stderr'] = null
- if not passio:
- call_kwargs['stdin'] = null
- call_kwargs['stdout'] = null
-
- if passio_popen:
- popen_kwargs={'stdout': subprocess.PIPE}
- popen_kwargs['stdin'] = subprocess.PIPE
- if passio_stderr:
- popen_kwargs['stderr'] = subprocess.PIPE
- else:
- popen_kwargs['stderr'] = call_kwargs.get('stderr', None)
- p = subprocess.Popen (args, **popen_kwargs)
- if null:
- null.close()
- return p
- if not wait and not passio:
- args += ["-e"]
- retcode = subprocess.call(args, **call_kwargs)
- if null:
- null.close()
- return retcode
-
- def run_service(self, service, source="dom0", user=None,
- passio_popen=False, input=None, localcmd=None, gui=False,
- wait=True):
- if bool(input) + bool(passio_popen) + bool(localcmd) > 1:
- raise ValueError("'input', 'passio_popen', 'localcmd' cannot be "
- "used together")
- if not wait and (localcmd or input):
- raise ValueError("Cannot use wait=False with input or "
- "localcmd specified")
- if localcmd:
- return self.run("QUBESRPC %s %s" % (service, source),
- localcmd=localcmd, user=user, wait=wait, gui=gui)
- elif input:
- p = self.run("QUBESRPC %s %s" % (service, source),
- user=user, wait=wait, gui=gui, passio_popen=True,
- passio_stderr=True)
- p.communicate(input)
- return p.returncode
- else:
- return self.run("QUBESRPC %s %s" % (service, source),
- passio_popen=passio_popen, user=user, wait=wait,
- gui=gui, passio_stderr=passio_popen)
-
- def attach_network(self, verbose = False, wait = True, netvm = None):
- self.log.debug('attach_network(netvm={!r})'.format(netvm))
- if dry_run:
- return
-
- if not self.is_running():
- raise QubesException ("VM not running!")
-
- if netvm is None:
- netvm = self.netvm
-
- if netvm is None:
- raise QubesException ("NetVM not set!")
-
- if netvm.qid != 0:
- if not netvm.is_running():
- if verbose:
- print >> sys.stderr, "--> Starting NetVM {0}...".format(netvm.name)
- netvm.start()
-
- self.libvirt_domain.attachDevice(
- self._format_net_dev(self.ip, self.mac, self.netvm.name))
-
- def detach_network(self, verbose = False, netvm = None):
- self.log.debug('detach_network(netvm={!r})'.format(netvm))
- if dry_run:
- return
-
- if not self.is_running():
- raise QubesException ("VM not running!")
-
- if netvm is None:
- netvm = self.netvm
-
- if netvm is None:
- raise QubesException ("NetVM not set!")
-
- self.libvirt_domain.detachDevice( self._format_net_dev(self.ip,
- self.mac, self.netvm.name))
-
- def wait_for_session(self, notify_function = None):
- self.log.debug('wait_for_session()')
- #self.run('echo $$ >> /tmp/qubes-session-waiter; [ ! -f /tmp/qubes-session-env ] && exec sleep 365d', ignore_stderr=True, gui=False, wait=True)
-
- # Note : User root is redefined to SYSTEM in the Windows agent code
- p = self.run('QUBESRPC qubes.WaitForSession none', user="root", passio_popen=True, gui=False, wait=True)
- p.communicate(input=self.default_user)
-
- def start_guid(self, verbose = True, notify_function = None,
- extra_guid_args=None, before_qrexec=False):
- self.log.debug(
- 'start_guid(extra_guid_args={!r}, before_qrexec={!r})'.format(
- extra_guid_args, before_qrexec))
- if before_qrexec:
- # On PV start GUId only after qrexec-daemon
- return
-
- if verbose:
- print >> sys.stderr, "--> Starting Qubes GUId..."
-
- guid_cmd = []
- if os.getuid() == 0:
- # try to always have guid running as normal user, otherwise
- # clipboard file may be created as root and other permission
- # problems
- qubes_group = grp.getgrnam('qubes')
- guid_cmd = ['runuser', '-u', qubes_group.gr_mem[0], '--']
-
- guid_cmd += [system_path["qubes_guid_path"],
- "-d", str(self.xid), "-N", self.name,
- "-c", self.label.color,
- "-i", self.label.icon_path,
- "-l", str(self.label.index)]
- if extra_guid_args is not None:
- guid_cmd += extra_guid_args
- if self.debug:
- guid_cmd += ['-v', '-v']
- elif not verbose:
- guid_cmd += ['-q']
- # Avoid using environment variables for checking the current session,
- # because this script may be called with cleared env (like with sudo).
- if subprocess.check_output(
- ['xprop', '-root', '-notype', 'KWIN_RUNNING']) == \
- 'KWIN_RUNNING = 0x1\n':
- # native decoration plugins is used, so adjust window properties
- # accordingly
- guid_cmd += ['-T'] # prefix window titles with VM name
- # get owner of X11 session
- session_owner = None
- for line in subprocess.check_output(['xhost']).splitlines():
- if line == 'SI:localuser:root':
- pass
- elif line.startswith('SI:localuser:'):
- session_owner = line.split(":")[2]
- if session_owner is not None:
- data_dir = os.path.expanduser(
- '~{}/.local/share'.format(session_owner))
- else:
- # fallback to current user
- data_dir = os.path.expanduser('~/.local/share')
-
- guid_cmd += ['-p',
- '_KDE_NET_WM_COLOR_SCHEME=s:{}'.format(
- os.path.join(data_dir,
- 'qubes-kde', self.label.name + '.colors'))]
-
- retcode = subprocess.call (guid_cmd)
- if (retcode != 0) :
- raise QubesException("Cannot start qubes-guid!")
-
- if not self.is_qrexec_running():
- return
-
- try:
- import qubes.monitorlayoutnotify
- if verbose:
- print >> sys.stderr, "--> Sending monitor layout..."
- monitor_layout = qubes.monitorlayoutnotify.get_monitor_layout()
- # Notify VM only if we've got a monitor_layout which is not empty
- # or else we break proper VM resolution set by gui-agent
- if len(monitor_layout) > 0:
- qubes.monitorlayoutnotify.notify_vm(self, monitor_layout)
- except ImportError as e:
- print >>sys.stderr, "ERROR: %s" % e
-
- if verbose:
- print >> sys.stderr, "--> Waiting for qubes-session..."
-
- self.wait_for_session(notify_function)
-
- def start_qrexec_daemon(self, verbose = False, notify_function = None):
- self.log.debug('start_qrexec_daemon()')
- if verbose:
- print >> sys.stderr, "--> Starting the qrexec daemon..."
- qrexec = []
- if os.getuid() == 0:
- # try to always have qrexec running as normal user, otherwise
- # many qrexec services would need to deal with root/user
- # permission problems
- qubes_group = grp.getgrnam('qubes')
- qrexec = ['runuser', '-u', qubes_group.gr_mem[0], '--']
-
- qrexec += ['env', 'QREXEC_STARTUP_TIMEOUT=' + str(self.qrexec_timeout),
- system_path["qrexec_daemon_path"]]
-
- qrexec_args = [str(self.xid), self.name, self.default_user]
- if not verbose:
- qrexec_args.insert(0, "-q")
- retcode = subprocess.call(qrexec + qrexec_args)
- if (retcode != 0) :
- raise OSError ("Cannot execute qrexec-daemon!")
-
- def start_qubesdb(self):
- self.log.debug('start_qubesdb()')
- pidfile = '/var/run/qubes/qubesdb.{}.pid'.format(self.name)
- try:
- if os.path.exists(pidfile):
- old_qubesdb_pid = open(pidfile, 'r').read()
- try:
- os.kill(int(old_qubesdb_pid), signal.SIGTERM)
- except OSError:
- raise QubesException(
- "Failed to kill old QubesDB instance (PID {}). "
- "Terminate it manually and retry. "
- "If that isn't QubesDB process, "
- "remove the pidfile: {}".format(old_qubesdb_pid,
- pidfile))
- timeout = 25
- while os.path.exists(pidfile) and timeout:
- time.sleep(0.2)
- timeout -= 1
- except IOError: # ENOENT (pidfile)
- pass
-
- # force connection to a new daemon
- self._qdb_connection = None
-
- qubesdb_cmd = []
- if os.getuid() == 0:
- # try to always have qubesdb running as normal user, otherwise
- # killing it at VM restart (see above) will always fail
- qubes_group = grp.getgrnam('qubes')
- qubesdb_cmd = ['runuser', '-u', qubes_group.gr_mem[0], '--']
-
- qubesdb_cmd += [
- system_path["qubesdb_daemon_path"],
- str(self.xid),
- self.name]
-
- retcode = subprocess.call (qubesdb_cmd)
- if retcode != 0:
- raise OSError("ERROR: Cannot execute qubesdb-daemon!")
-
- def request_memory(self, mem_required = None):
- # Overhead of per-VM/per-vcpu Xen structures, taken from OpenStack nova/virt/xenapi/driver.py
- # see https://wiki.openstack.org/wiki/XenServer/Overhead
- # add an extra MB because Nova rounds up to MBs
- MEM_OVERHEAD_BASE = (3 + 1) * 1024 * 1024
- MEM_OVERHEAD_PER_VCPU = 3 * 1024 * 1024 / 2
- if mem_required is None:
- mem_required = int(self.memory) * 1024 * 1024
- if qmemman_present:
- qmemman_client = QMemmanClient()
- try:
- mem_required_with_overhead = mem_required + MEM_OVERHEAD_BASE + self.vcpus * MEM_OVERHEAD_PER_VCPU
- got_memory = qmemman_client.request_memory(mem_required_with_overhead)
- except IOError as e:
- raise IOError("ERROR: Failed to connect to qmemman: %s" % str(e))
- if not got_memory:
- qmemman_client.close()
- raise MemoryError ("ERROR: insufficient memory to start VM '%s'" % self.name)
- return qmemman_client
-
- def start(self, verbose = False, preparing_dvm = False, start_guid = True,
- notify_function = None, mem_required = None):
- self.log.debug('start('
- 'preparing_dvm={!r}, start_guid={!r}, mem_required={!r})'.format(
- preparing_dvm, start_guid, mem_required))
- if dry_run:
- return
-
- # Intentionally not used is_running(): eliminate also "Paused", "Crashed", "Halting"
- if self.get_power_state() != "Halted":
- raise QubesException ("VM is already running!")
-
- self.verify_files()
-
- if self.netvm is not None:
- if self.netvm.qid != 0:
- if not self.netvm.is_running():
- if verbose:
- print >> sys.stderr, "--> Starting NetVM {0}...".format(self.netvm.name)
- self.netvm.start(verbose = verbose, start_guid = start_guid, notify_function = notify_function)
-
- self.storage.prepare_for_vm_startup(verbose=verbose)
- if verbose:
- print >> sys.stderr, "--> Loading the VM (type = {0})...".format(self.type)
-
- self._update_libvirt_domain()
-
- qmemman_client = self.request_memory(mem_required)
-
- # Bind pci devices to pciback driver
- for pci in self.pcidevs:
- try:
- nd = vmm.libvirt_conn.nodeDeviceLookupByName('pci_0000_' + pci.replace(':','_').replace('.','_'))
- except libvirt.libvirtError as e:
- if e.get_error_code() == libvirt.VIR_ERR_NO_NODE_DEVICE:
- raise QubesException(
- "PCI device {} does not exist (domain {})".
- format(pci, self.name))
- else:
- raise
- try:
- nd.dettach()
- except libvirt.libvirtError as e:
- if e.get_error_code() == libvirt.VIR_ERR_INTERNAL_ERROR:
- # already detached
- pass
- else:
- raise
-
- self.libvirt_domain.createWithFlags(libvirt.VIR_DOMAIN_START_PAUSED)
-
- try:
- if verbose:
- print >> sys.stderr, "--> Starting Qubes DB..."
- self.start_qubesdb()
-
- xid = self.xid
- self.log.debug('xid={}'.format(xid))
-
- if preparing_dvm:
- self.services['qubes-dvm'] = True
- if verbose:
- print >> sys.stderr, "--> Setting Qubes DB info for the VM..."
- self.create_qubesdb_entries()
-
- if verbose:
- print >> sys.stderr, "--> Updating firewall rules..."
- netvm = self.netvm
- while netvm is not None:
- if netvm.is_proxyvm() and netvm.is_running():
- netvm.write_iptables_qubesdb_entry()
- netvm = netvm.netvm
-
- # fire hooks
- for hook in self.hooks_start:
- hook(self, verbose = verbose, preparing_dvm = preparing_dvm,
- start_guid = start_guid, notify_function = notify_function)
- except:
- self.force_shutdown()
- raise
-
- if verbose:
- print >> sys.stderr, "--> Starting the VM..."
- self.libvirt_domain.resume()
-
-# close() is not really needed, because the descriptor is close-on-exec
-# anyway, the reason to postpone close() is that possibly xl is not done
-# constructing the domain after its main process exits
-# so we close() when we know the domain is up
-# the successful unpause is some indicator of it
- if qmemman_present:
- qmemman_client.close()
-
- extra_guid_args = []
- if preparing_dvm:
- # Run GUI daemon in "invisible" mode, so applications started by
- # prerun script will not disturb the user
- extra_guid_args = ['-I']
- elif not os.path.exists('/var/run/qubes/shm.id') \
- and not os.path.exists('/var/run/shm.id'):
- # Start GUI daemon only when shmoverride is loaded; unless
- # preparing DispVM, where it isn't needed because of "invisible"
- # mode
- start_guid = False
- if start_guid and 'DISPLAY' not in os.environ:
- if verbose:
- print >> sys.stderr, \
- "WARNING: not starting GUI, because DISPLAY not set"
- start_guid = False
-
- if start_guid:
- self.start_guid(verbose=verbose, notify_function=notify_function,
- before_qrexec=True, extra_guid_args=extra_guid_args)
-
- if not preparing_dvm:
- self.start_qrexec_daemon(verbose=verbose,notify_function=notify_function)
-
- if start_guid:
- self.start_guid(verbose=verbose, notify_function=notify_function,
- extra_guid_args=extra_guid_args)
-
- return xid
-
- def _cleanup_zombie_domains(self):
- """
- This function is workaround broken libxl (which leaves not fully
- created domain on failure) and vchan on domain crash behaviour
- @return: None
- """
- xc = self.get_xc_dominfo()
- if xc and xc['dying'] == 1:
- # GUID still running?
- guid_pidfile = '/var/run/qubes/guid-running.%d' % xc['domid']
- if os.path.exists(guid_pidfile):
- guid_pid = open(guid_pidfile).read().strip()
- os.kill(int(guid_pid), 15)
- # qrexec still running?
- if self.is_qrexec_running():
- #TODO: kill qrexec daemon
- pass
-
- def shutdown(self, force=False, xid = None):
- self.log.debug('shutdown()')
- if dry_run:
- return
-
- if not self.is_running():
- raise QubesException ("VM already stopped!")
-
- self.libvirt_domain.shutdown()
-
- def force_shutdown(self, xid = None):
- self.log.debug('force_shutdown()')
- if dry_run:
- return
-
- if not self.is_running() and not self.is_paused():
- raise QubesException ("VM already stopped!")
-
- self.libvirt_domain.destroy()
- self.refresh()
-
- def suspend(self):
- self.log.debug('suspend()')
- if dry_run:
- return
-
- if not self.is_running() and not self.is_paused() or \
- self.get_power_state() == "Suspended":
- raise QubesException ("VM not running!")
-
- if len (self.pcidevs) > 0:
- self.libvirt_domain.pMSuspendForDuration(
- libvirt.VIR_NODE_SUSPEND_TARGET_MEM, 0, 0)
- else:
- self.pause()
-
- def resume(self):
- self.log.debug('resume()')
- if dry_run:
- return
-
- if self.get_power_state() == "Suspended":
- self.libvirt_domain.pMWakeup()
- else:
- self.unpause()
-
- def pause(self):
- self.log.debug('pause()')
- if dry_run:
- return
-
- if not self.is_running():
- raise QubesException ("VM not running!")
-
- self.libvirt_domain.suspend()
-
- def unpause(self):
- self.log.debug('unpause()')
- if dry_run:
- return
-
- if not self.is_paused():
- raise QubesException ("VM not paused!")
-
- self.libvirt_domain.resume()
-
- def get_xml_attrs(self):
- attrs = {}
- attrs_config = self.get_attrs_config()
- for attr in attrs_config:
- attr_config = attrs_config[attr]
- if 'save' in attr_config:
- if 'save_skip' in attr_config:
- if callable(attr_config['save_skip']):
- if attr_config['save_skip']():
- continue
- elif eval(attr_config['save_skip']):
- continue
- if callable(attr_config['save']):
- value = attr_config['save']()
- else:
- value = eval(attr_config['save'])
- if 'save_attr' in attr_config:
- attrs[attr_config['save_attr']] = value
- else:
- attrs[attr] = value
- return attrs
-
- def create_xml_element(self):
-
- attrs = self.get_xml_attrs()
- element = lxml.etree.Element(
- # Compatibility hack (Qubes*VM in type vs Qubes*Vm in XML)...
- "Qubes" + self.type.replace("VM", "Vm"),
- **attrs)
- return element
-
-register_qubes_vm_class(QubesVm)
diff --git a/core-modules/001QubesResizableVm.py b/core-modules/001QubesResizableVm.py
deleted file mode 100644
index 831db14f..00000000
--- a/core-modules/001QubesResizableVm.py
+++ /dev/null
@@ -1,76 +0,0 @@
-#!/usr/bin/python2
-# -*- encoding: utf8 -*-
-#
-# The Qubes OS Project, http://www.qubes-os.org
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-#
-
-from __future__ import (
- absolute_import,
- division,
- print_function,
- unicode_literals,
-)
-
-from qubes.qubes import (
- register_qubes_vm_class,
- QubesException,
- QubesVm,
-)
-from time import sleep
-
-
-class QubesResizableVm(QubesVm):
-
- def resize_root_img(self, size, allow_start=False):
- if self.template:
- raise QubesException("Cannot resize root.img of template-based VM"
- ". Resize the root.img of the template "
- "instead.")
-
- if self.is_running():
- raise QubesException("Cannot resize root.img of running VM")
-
- if size < self.get_root_img_sz():
- raise QubesException(
- "For your own safety shringing of root.img is disabled. If "
- "you really know what you are doing, use 'truncate' manually.")
-
- f_root = open(self.root_img, "a+b")
- f_root.truncate(size)
- f_root.close()
-
-
-class QubesResizableVmWithResize2fs(QubesResizableVm):
-
- def resize_root_img(self, size, allow_start=False):
- super(QubesResizableVmWithResize2fs, self).\
- resize_root_img(size, allow_start=allow_start)
- if not allow_start:
- raise QubesException("To complete the resize operation start the "
- "qube. You may need to run resize2fs manually"
- "in the qube .")
- self.start(start_guid=False)
- self.run("resize2fs /dev/mapper/dmroot", user="root", wait=True,
- gui=False)
- self.shutdown()
- while self.is_running():
- sleep(1)
-
-
-register_qubes_vm_class(QubesResizableVm)
-register_qubes_vm_class(QubesResizableVmWithResize2fs)
diff --git a/core-modules/003QubesTemplateVm.py b/core-modules/003QubesTemplateVm.py
deleted file mode 100644
index 4d9a6c79..00000000
--- a/core-modules/003QubesTemplateVm.py
+++ /dev/null
@@ -1,103 +0,0 @@
-#!/usr/bin/python2
-# -*- coding: utf-8 -*-
-#
-# The Qubes OS Project, http://www.qubes-os.org
-#
-# Copyright (C) 2010 Joanna Rutkowska
-# Copyright (C) 2013 Marek Marczykowski
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-#
-
-import os
-import sys
-
-from qubes.qubes import (
- defaults,
- dry_run,
- register_qubes_vm_class,
- system_path,
- vmm,
- QubesResizableVmWithResize2fs,
- QubesVmCollection,
-)
-
-
-class QubesTemplateVm(QubesResizableVmWithResize2fs):
- """
- A class that represents an TemplateVM. A child of QubesVm.
- """
-
- # In which order load this VM type from qubes.xml
- load_order = 50
-
- def get_attrs_config(self):
- attrs_config = super(QubesTemplateVm, self).get_attrs_config()
- attrs_config['dir_path']['func'] = \
- lambda value: value if value is not None else \
- os.path.join(system_path["qubes_templates_dir"], self.name)
- attrs_config['label']['default'] = defaults["template_label"]
-
- return attrs_config
-
- def __init__(self, **kwargs):
-
- super(QubesTemplateVm, self).__init__(**kwargs)
-
- self.appvms = QubesVmCollection()
-
- @property
- def type(self):
- return "TemplateVM"
-
- @property
- def updateable(self):
- return True
-
- def is_template(self):
- return True
-
- def get_firewall_defaults(self):
- return { "rules": list(), "allow": False, "allowDns": False, "allowIcmp": False, "allowYumProxy": True }
-
- @property
- def rootcow_img(self):
- return self.storage.rootcow_img
-
- def clone_disk_files(self, src_vm, verbose):
- if dry_run:
- return
-
- super(QubesTemplateVm, self).clone_disk_files(src_vm=src_vm, verbose=verbose)
-
- # Create root-cow.img
- self.commit_changes(verbose=verbose)
-
- def commit_changes (self, verbose = False):
- self.log.debug('commit_changes()')
-
- if not vmm.offline_mode:
- assert not self.is_running(), "Attempt to commit changes on running Template VM!"
-
- if verbose:
- print >> sys.stderr, "--> Commiting template updates... COW: {0}...".format (self.rootcow_img)
-
- if dry_run:
- return
-
- self.storage.commit_template_changes()
-
-register_qubes_vm_class(QubesTemplateVm)
diff --git a/core-modules/005QubesNetVm.py b/core-modules/005QubesNetVm.py
deleted file mode 100644
index 904e74f1..00000000
--- a/core-modules/005QubesNetVm.py
+++ /dev/null
@@ -1,181 +0,0 @@
-#!/usr/bin/python2
-# -*- coding: utf-8 -*-
-#
-# The Qubes OS Project, http://www.qubes-os.org
-#
-# Copyright (C) 2010 Joanna Rutkowska
-# Copyright (C) 2013 Marek Marczykowski
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-#
-import sys
-import os.path
-import libvirt
-
-from qubes.qubes import QubesVm,register_qubes_vm_class,vmm,dry_run
-from qubes.qubes import defaults,system_path,vm_files
-from qubes.qubes import QubesVmCollection,QubesException
-
-class QubesNetVm(QubesVm):
- """
- A class that represents a NetVM. A child of QubesCowVM.
- """
-
- # In which order load this VM type from qubes.xml
- load_order = 70
-
- def get_attrs_config(self):
- attrs_config = super(QubesNetVm, self).get_attrs_config()
- attrs_config['dir_path']['func'] = \
- lambda value: value if value is not None else \
- os.path.join(system_path["qubes_servicevms_dir"], self.name)
- attrs_config['uses_default_netvm']['func'] = lambda x: False
- attrs_config['label']['default'] = defaults["servicevm_label"]
- attrs_config['memory']['default'] = 300
-
- # New attributes
- attrs_config['netid'] = {
- 'save': lambda: str(self.netid),
- 'order': 30,
- 'func': lambda value: value if value is not None else
- self._collection.get_new_unused_netid() }
- attrs_config['netprefix'] = {
- 'func': lambda x: "10.137.{0}.".format(self.netid) }
- attrs_config['dispnetprefix'] = {
- 'func': lambda x: "10.138.{0}.".format(self.netid) }
-
- # Dont save netvm prop
- attrs_config['netvm'].pop('save')
- attrs_config['uses_default_netvm'].pop('save')
-
- return attrs_config
-
- def __init__(self, **kwargs):
- super(QubesNetVm, self).__init__(**kwargs)
- self.connected_vms = QubesVmCollection()
-
- self.__network = "10.137.{0}.0".format(self.netid)
- self.__netmask = defaults["vm_default_netmask"]
- self.__gateway = self.netprefix + "1"
- self.__secondary_dns = self.netprefix + "254"
-
- self.__external_ip_allowed_xids = set()
-
- self.log.debug('network={} netmask={} gateway={} secondary_dns={}'.format(
- self.network, self.netmask, self.gateway, self.secondary_dns))
-
- @property
- def type(self):
- return "NetVM"
-
- def is_netvm(self):
- return True
-
- @property
- def gateway(self):
- return self.__gateway
-
- @property
- def secondary_dns(self):
- return self.__secondary_dns
-
- @property
- def netmask(self):
- return self.__netmask
-
- @property
- def network(self):
- return self.__network
-
- def get_ip_for_vm(self, qid):
- lo = qid % 253 + 2
- assert lo >= 2 and lo <= 254, "Wrong IP address for VM"
- return self.netprefix + "{0}".format(lo)
-
- def get_ip_for_dispvm(self, dispid):
- lo = dispid % 254 + 1
- assert lo >= 1 and lo <= 254, "Wrong IP address for VM"
- return self.dispnetprefix + "{0}".format(lo)
-
- def update_external_ip_permissions(self, xid = -1):
- # TODO: VMs in __external_ip_allowed_xids should be notified via RPC
- # service on exteran IP change
- pass
-
- def start(self, **kwargs):
- if dry_run:
- return
-
- xid=super(QubesNetVm, self).start(**kwargs)
-
- # Connect vif's of already running VMs
- for vm in self.connected_vms.values():
- if not vm.is_running():
- continue
-
- if 'verbose' in kwargs and kwargs['verbose']:
- print >> sys.stderr, "--> Attaching network to '{0}'...".format(vm.name)
-
- # Cleanup stale VIFs
- vm.cleanup_vifs()
-
- # force frontend to forget about this device
- # module actually will be loaded back by udev, as soon as network is attached
- try:
- vm.run("modprobe -r xen-netfront xennet", user="root")
- except:
- pass
-
- try:
- vm.attach_network(wait=False)
- except QubesException as ex:
- print >> sys.stderr, ("WARNING: Cannot attach to network to '{0}': {1}".format(vm.name, ex))
-
- return xid
-
- def shutdown(self, force=False):
- if dry_run:
- return
-
- connected_vms = [vm for vm in self.connected_vms.values() if vm.is_running()]
- if connected_vms and not force:
- raise QubesException("There are other VMs connected to this VM: " + str([vm.name for vm in connected_vms]))
-
- # detach network interfaces of connected VMs before shutting down,
- # otherwise libvirt will not notice it and will try to detach them
- # again (which would fail, obviously).
- # This code can be removed when #1426 got implemented
- for vm in self.connected_vms.values():
- if vm.is_running():
- try:
- vm.detach_network()
- except (QubesException, libvirt.libvirtError):
- # ignore errors
- pass
-
- super(QubesNetVm, self).shutdown(force=force)
-
- def add_external_ip_permission(self, xid):
- if int(xid) < 0:
- return
- self.__external_ip_allowed_xids.add(int(xid))
- self.update_external_ip_permissions()
-
- def remove_external_ip_permission(self, xid):
- self.__external_ip_allowed_xids.discard(int(xid))
- self.update_external_ip_permissions()
-
-register_qubes_vm_class(QubesNetVm)
diff --git a/core-modules/006QubesAdminVm.py b/core-modules/006QubesAdminVm.py
deleted file mode 100644
index ce0b5882..00000000
--- a/core-modules/006QubesAdminVm.py
+++ /dev/null
@@ -1,107 +0,0 @@
-#!/usr/bin/python2
-# -*- coding: utf-8 -*-
-#
-# The Qubes OS Project, http://www.qubes-os.org
-#
-# Copyright (C) 2010 Joanna Rutkowska
-# Copyright (C) 2013 Marek Marczykowski
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-#
-
-from qubes.qubes import QubesNetVm,register_qubes_vm_class
-from qubes.qubes import defaults
-from qubes.qubes import QubesException,dry_run,vmm
-import psutil
-
-class QubesAdminVm(QubesNetVm):
-
- # In which order load this VM type from qubes.xml
- load_order = 10
-
- def get_attrs_config(self):
- attrs = super(QubesAdminVm, self).get_attrs_config()
- attrs.pop('kernel')
- attrs.pop('kernels_dir')
- attrs.pop('kernelopts')
- attrs.pop('uses_default_kernel')
- attrs.pop('uses_default_kernelopts')
- return attrs
-
- def __init__(self, **kwargs):
- super(QubesAdminVm, self).__init__(qid=0, name="dom0", netid=0,
- dir_path=None,
- private_img = None,
- template = None,
- maxmem = 0,
- vcpus = 0,
- label = defaults["template_label"],
- **kwargs)
-
- @property
- def xid(self):
- return 0
-
- @property
- def libvirt_domain(self):
- raise ValueError("Dom0 do not have libvirt object")
-
- @property
- def type(self):
- return "AdminVM"
-
- def is_running(self):
- return True
-
- def get_power_state(self):
- return "Running"
-
- def get_mem(self):
- return psutil.virtual_memory().total/1024
-
- def get_mem_static_max(self):
- return vmm.libvirt_conn.getInfo()[1]
-
- def get_cputime(self):
- # TODO: measure it somehow
- return 0
-
- def get_disk_usage(self, file_or_dir):
- return 0
-
- def get_disk_utilization(self):
- return 0
-
- def get_disk_utilization_private_img(self):
- return 0
-
- def get_private_img_sz(self):
- return 0
-
- @property
- def ip(self):
- return "10.137.0.2"
-
- def start(self, **kwargs):
- raise QubesException ("Cannot start Dom0 fake domain!")
-
- def suspend(self):
- return
-
- def verify_files(self):
- return True
-
-register_qubes_vm_class(QubesAdminVm)
diff --git a/core-modules/006QubesProxyVm.py b/core-modules/006QubesProxyVm.py
deleted file mode 100644
index b5ae20f8..00000000
--- a/core-modules/006QubesProxyVm.py
+++ /dev/null
@@ -1,208 +0,0 @@
-#!/usr/bin/python2
-# -*- coding: utf-8 -*-
-#
-# The Qubes OS Project, http://www.qubes-os.org
-#
-# Copyright (C) 2010 Joanna Rutkowska
-# Copyright (C) 2013 Marek Marczykowski
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-#
-
-from datetime import datetime
-
-from qubes.qubes import QubesNetVm,register_qubes_vm_class,vmm,dry_run
-from qubes.qubes import QubesVmCollection,QubesException
-
-yum_proxy_ip = '10.137.255.254'
-yum_proxy_port = '8082'
-
-class QubesProxyVm(QubesNetVm):
- """
- A class that represents a ProxyVM, ex FirewallVM. A child of QubesNetVM.
- """
-
- def get_attrs_config(self):
- attrs_config = super(QubesProxyVm, self).get_attrs_config()
- attrs_config['uses_default_netvm']['func'] = lambda x: False
- # Save netvm prop again
- attrs_config['netvm']['save'] = \
- lambda: str(self.netvm.qid) if self.netvm is not None else "none"
-
- return attrs_config
-
- def __init__(self, **kwargs):
- super(QubesProxyVm, self).__init__(**kwargs)
- self.rules_applied = None
-
- @property
- def type(self):
- return "ProxyVM"
-
- def is_proxyvm(self):
- return True
-
- def _set_netvm(self, new_netvm):
- old_netvm = self.netvm
- super(QubesProxyVm, self)._set_netvm(new_netvm)
- if vmm.offline_mode:
- return
- if self.netvm is not None:
- self.netvm.add_external_ip_permission(self.get_xid())
- self.write_netvm_domid_entry()
- if old_netvm is not None:
- old_netvm.remove_external_ip_permission(self.get_xid())
-
- def post_vm_net_attach(self, vm):
- """ Called after some VM net-attached to this ProxyVm """
-
- self.write_iptables_qubesdb_entry()
-
- def post_vm_net_detach(self, vm):
- """ Called after some VM net-detached from this ProxyVm """
-
- self.write_iptables_qubesdb_entry()
-
- def start(self, **kwargs):
- if dry_run:
- return
- retcode = super(QubesProxyVm, self).start(**kwargs)
- if self.netvm is not None:
- self.netvm.add_external_ip_permission(self.get_xid())
- self.write_netvm_domid_entry()
- return retcode
-
- def force_shutdown(self, **kwargs):
- if dry_run:
- return
- if self.netvm is not None:
- self.netvm.remove_external_ip_permission(kwargs['xid'] if 'xid' in kwargs else self.get_xid())
- super(QubesProxyVm, self).force_shutdown(**kwargs)
-
- def create_qubesdb_entries(self):
- if dry_run:
- return
-
- super(QubesProxyVm, self).create_qubesdb_entries()
- self.qdb.write("/qubes-iptables-error", '')
- self.write_iptables_qubesdb_entry()
-
- def write_netvm_domid_entry(self, xid = -1):
- if not self.is_running():
- return
-
- if xid < 0:
- xid = self.get_xid()
-
- if self.netvm is None:
- self.qdb.write("/qubes-netvm-domid", '')
- else:
- self.qdb.write("/qubes-netvm-domid",
- "{0}".format(self.netvm.get_xid()))
-
- def write_iptables_qubesdb_entry(self):
- self.qdb.rm("/qubes-iptables-domainrules/")
- iptables = "# Generated by Qubes Core on {0}\n".format(datetime.now().ctime())
- iptables += "*filter\n"
- iptables += ":INPUT DROP [0:0]\n"
- iptables += ":FORWARD DROP [0:0]\n"
- iptables += ":OUTPUT ACCEPT [0:0]\n"
-
- # Strict INPUT rules
- iptables += "-A INPUT -i vif+ -p udp -m udp --dport 68 -j DROP\n"
- iptables += "-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED " \
- "-j ACCEPT\n"
- iptables += "-A INPUT -p icmp -j ACCEPT\n"
- iptables += "-A INPUT -i lo -j ACCEPT\n"
- iptables += "-A INPUT -j REJECT --reject-with icmp-host-prohibited\n"
-
- iptables += "-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED " \
- "-j ACCEPT\n"
- # Allow dom0 networking
- iptables += "-A FORWARD -i vif0.0 -j ACCEPT\n"
- # Deny inter-VMs networking
- iptables += "-A FORWARD -i vif+ -o vif+ -j DROP\n"
- iptables += "COMMIT\n"
- self.qdb.write("/qubes-iptables-header", iptables)
-
- vms = [vm for vm in self.connected_vms.values()]
- for vm in vms:
- iptables="*filter\n"
- conf = vm.get_firewall_conf()
-
- xid = vm.get_xid()
- if xid < 0: # VM not active ATM
- continue
-
- ip = vm.ip
- if ip is None:
- continue
-
- # Anti-spoof rules are added by vif-script (vif-route-qubes), here we trust IP address
-
- accept_action = "ACCEPT"
- reject_action = "REJECT --reject-with icmp-host-prohibited"
-
- if conf["allow"]:
- default_action = accept_action
- rules_action = reject_action
- else:
- default_action = reject_action
- rules_action = accept_action
-
- for rule in conf["rules"]:
- iptables += "-A FORWARD -s {0} -d {1}".format(ip, rule["address"])
- if rule["netmask"] != 32:
- iptables += "/{0}".format(rule["netmask"])
-
- if rule["proto"] is not None and rule["proto"] != "any":
- iptables += " -p {0}".format(rule["proto"])
- if rule["portBegin"] is not None and rule["portBegin"] > 0:
- iptables += " --dport {0}".format(rule["portBegin"])
- if rule["portEnd"] is not None and rule["portEnd"] > rule["portBegin"]:
- iptables += ":{0}".format(rule["portEnd"])
-
- iptables += " -j {0}\n".format(rules_action)
-
- if conf["allowDns"] and self.netvm is not None:
- # PREROUTING does DNAT to NetVM DNSes, so we need self.netvm.
- # properties
- iptables += "-A FORWARD -s {0} -p udp -d {1} --dport 53 -j " \
- "ACCEPT\n".format(ip,self.netvm.gateway)
- iptables += "-A FORWARD -s {0} -p udp -d {1} --dport 53 -j " \
- "ACCEPT\n".format(ip,self.netvm.secondary_dns)
- iptables += "-A FORWARD -s {0} -p tcp -d {1} --dport 53 -j " \
- "ACCEPT\n".format(ip,self.netvm.gateway)
- iptables += "-A FORWARD -s {0} -p tcp -d {1} --dport 53 -j " \
- "ACCEPT\n".format(ip,self.netvm.secondary_dns)
- if conf["allowIcmp"]:
- iptables += "-A FORWARD -s {0} -p icmp -j ACCEPT\n".format(ip)
- if conf["allowYumProxy"]:
- iptables += "-A FORWARD -s {0} -p tcp -d {1} --dport {2} -j ACCEPT\n".format(ip, yum_proxy_ip, yum_proxy_port)
- else:
- iptables += "-A FORWARD -s {0} -p tcp -d {1} --dport {2} -j DROP\n".format(ip, yum_proxy_ip, yum_proxy_port)
-
- iptables += "-A FORWARD -s {0} -j {1}\n".format(ip, default_action)
- iptables += "COMMIT\n"
- self.qdb.write("/qubes-iptables-domainrules/"+str(xid), iptables)
- # no need for ending -A FORWARD -j DROP, cause default action is DROP
-
- self.write_netvm_domid_entry()
-
- self.rules_applied = None
- self.qdb.write("/qubes-iptables", 'reload')
-
-register_qubes_vm_class(QubesProxyVm)
diff --git a/core-modules/01QubesAppVm.py b/core-modules/01QubesAppVm.py
deleted file mode 100644
index 9a46fb47..00000000
--- a/core-modules/01QubesAppVm.py
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/usr/bin/python2
-# -*- coding: utf-8 -*-
-#
-# The Qubes OS Project, http://www.qubes-os.org
-#
-# Copyright (C) 2010 Joanna Rutkowska
-# Copyright (C) 2013 Marek Marczykowski
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-#
-
-import os.path
-
-from qubes.qubes import (
- register_qubes_vm_class,
- system_path,
- QubesResizableVmWithResize2fs,
- QubesVmLabel,
-)
-
-
-class QubesAppVm(QubesResizableVmWithResize2fs):
- """
- A class that represents an AppVM. A child of QubesVm.
- """
- def get_attrs_config(self):
- attrs_config = super(QubesAppVm, self).get_attrs_config()
- attrs_config['dir_path']['func'] = \
- lambda value: value if value is not None else \
- os.path.join(system_path["qubes_appvms_dir"], self.name)
-
- return attrs_config
-
- @property
- def type(self):
- return "AppVM"
-
- def is_appvm(self):
- return True
-
-register_qubes_vm_class(QubesAppVm)
diff --git a/core-modules/01QubesDisposableVm.py b/core-modules/01QubesDisposableVm.py
deleted file mode 100644
index a0fe4e54..00000000
--- a/core-modules/01QubesDisposableVm.py
+++ /dev/null
@@ -1,248 +0,0 @@
-#!/usr/bin/python2
-# -*- coding: utf-8 -*-
-#
-# The Qubes OS Project, http://www.qubes-os.org
-#
-# Copyright (C) 2010 Joanna Rutkowska
-# Copyright (C) 2013 Marek Marczykowski
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-#
-
-import os
-import sys
-import libvirt
-import time
-from qubes.qubes import QubesVm,QubesVmLabel,register_qubes_vm_class, \
- QubesException
-from qubes.qubes import QubesDispVmLabels
-from qubes.qubes import dry_run,vmm
-import grp
-
-qmemman_present = False
-try:
- from qubes.qmemman_client import QMemmanClient
- qmemman_present = True
-except ImportError:
- pass
-
-DISPID_STATE_FILE = '/var/run/qubes/dispid'
-GUID_SHMID_FILE = ['/var/run/qubes/shm.id', '/var/run/shm.id']
-
-class QubesDisposableVm(QubesVm):
- """
- A class that represents an DisposableVM. A child of QubesVm.
- """
-
- # In which order load this VM type from qubes.xml
- load_order = 120
-
-
- def _assign_new_dispid(self):
- # This method in called while lock on qubes.xml is held, so no need for
- # additional lock
- if os.path.exists(DISPID_STATE_FILE):
- f = open(DISPID_STATE_FILE, 'r+')
- dispid = int(f.read())
- f.seek(0)
- f.truncate(0)
- f.write(str(dispid+1))
- f.close()
- else:
- dispid = 1
- f = open(DISPID_STATE_FILE, 'w')
- f.write(str(dispid+1))
- f.close()
- os.chown(DISPID_STATE_FILE, -1, grp.getgrnam('qubes').gr_gid)
- os.chmod(DISPID_STATE_FILE, 0664)
- return dispid
-
- def get_attrs_config(self):
- attrs_config = super(QubesDisposableVm, self).get_attrs_config()
-
- attrs_config['name']['func'] = \
- lambda x: "disp%d" % self.dispid if x is None else x
-
- # New attributes
- attrs_config['dispid'] = {
- 'func': lambda x: (self._assign_new_dispid() if x is None
- else int(x)),
- 'save': lambda: str(self.dispid),
- # needs to be set before name
- 'order': 0
- }
- attrs_config['include_in_backups']['func'] = lambda x: False
- attrs_config['disp_savefile'] = {
- 'default': '/var/run/qubes/current-savefile',
- 'save': lambda: str(self.disp_savefile) }
-
- return attrs_config
-
- def __init__(self, **kwargs):
-
- disp_template = None
- if 'disp_template' in kwargs.keys():
- disp_template = kwargs['disp_template']
- kwargs['template'] = disp_template.template
- kwargs['dir_path'] = disp_template.dir_path
- kwargs['kernel'] = disp_template.kernel
- kwargs['uses_default_kernel'] = disp_template.uses_default_kernel
- kwargs['kernelopts'] = disp_template.kernelopts
- kwargs['uses_default_kernelopts'] = \
- disp_template.uses_default_kernelopts
- super(QubesDisposableVm, self).__init__(**kwargs)
-
- assert self.template is not None, "Missing template for DisposableVM!"
-
- if disp_template:
- self.clone_attrs(disp_template)
-
- # Use DispVM icon with the same color
- if self._label:
- self._label = QubesDispVmLabels[self._label.name]
- self.icon_path = self._label.icon_path
-
- @property
- def type(self):
- return "DisposableVM"
-
- def is_disposablevm(self):
- return True
-
- @property
- def ip(self):
- if self.netvm is not None:
- return self.netvm.get_ip_for_dispvm(self.dispid)
- else:
- return None
-
- def get_clone_attrs(self):
- attrs = super(QubesDisposableVm, self).get_clone_attrs()
- attrs.remove('_label')
- return attrs
-
- def do_not_use_get_xml_attrs(self):
- # Minimal set - do not inherit rest of attributes
- attrs = {}
- attrs["qid"] = str(self.qid)
- attrs["name"] = self.name
- attrs["dispid"] = str(self.dispid)
- attrs["template_qid"] = str(self.template.qid)
- attrs["label"] = self.label.name
- attrs["firewall_conf"] = self.relative_path(self.firewall_conf)
- attrs["netvm_qid"] = str(self.netvm.qid) if self.netvm is not None else "none"
- return attrs
-
- def verify_files(self):
- return True
-
- def get_config_params(self):
- attrs = super(QubesDisposableVm, self).get_config_params()
- attrs['privatedev'] = ''
- return attrs
-
- def create_qubesdb_entries(self):
- super(QubesDisposableVm, self).create_qubesdb_entries()
-
- self.qdb.write("/qubes-vm-persistence", "none")
- self.qdb.write('/qubes-restore-complete', '1')
-
- def start(self, verbose = False, **kwargs):
- self.log.debug('start()')
- if dry_run:
- return
-
- # Intentionally not used is_running(): eliminate also "Paused", "Crashed", "Halting"
- if self.get_power_state() != "Halted":
- raise QubesException ("VM is already running!")
-
- if self.netvm is not None:
- if self.netvm.qid != 0:
- if not self.netvm.is_running():
- if verbose:
- print >> sys.stderr, "--> Starting NetVM {0}...".\
- format(self.netvm.name)
- self.netvm.start(verbose=verbose, **kwargs)
-
- if verbose:
- print >> sys.stderr, "--> Loading the VM (type = {0})...".format(self.type)
-
- print >>sys.stderr, "time=%s, creating config file" % (str(time.time()))
- # refresh config file
- domain_config = self.create_config_file()
-
- qmemman_client = self.request_memory()
-
- # dispvm cannot have PCI devices
- assert (len(self.pcidevs) == 0), "DispVM cannot have PCI devices"
-
- print >>sys.stderr, "time=%s, calling restore" % (str(time.time()))
- vmm.libvirt_conn.restoreFlags(self.disp_savefile,
- domain_config, libvirt.VIR_DOMAIN_SAVE_PAUSED)
-
- print >>sys.stderr, "time=%s, done" % (str(time.time()))
- self._libvirt_domain = None
-
- if verbose:
- print >> sys.stderr, "--> Starting Qubes DB..."
- self.start_qubesdb()
-
- self.services['qubes-dvm'] = True
- if verbose:
- print >> sys.stderr, "--> Setting Qubes DB info for the VM..."
- self.create_qubesdb_entries()
- print >>sys.stderr, "time=%s, done qubesdb" % (str(time.time()))
-
- # fire hooks
- for hook in self.hooks_start:
- hook(self, verbose = verbose, **kwargs)
-
- if verbose:
- print >> sys.stderr, "--> Starting the VM..."
- self.libvirt_domain.resume()
- print >>sys.stderr, "time=%s, resumed" % (str(time.time()))
-
-# close() is not really needed, because the descriptor is close-on-exec
-# anyway, the reason to postpone close() is that possibly xl is not done
-# constructing the domain after its main process exits
-# so we close() when we know the domain is up
-# the successful unpause is some indicator of it
- if qmemman_present:
- qmemman_client.close()
-
- if kwargs.get('start_guid', True) and \
- any(os.path.exists(x) for x in GUID_SHMID_FILE):
- self.start_guid(verbose=verbose, before_qrexec=True,
- notify_function=kwargs.get('notify_function', None))
-
- self.start_qrexec_daemon(verbose=verbose,
- notify_function=kwargs.get('notify_function', None))
- print >>sys.stderr, "time=%s, qrexec done" % (str(time.time()))
-
- if kwargs.get('start_guid', True) and \
- any(os.path.exists(x) for x in GUID_SHMID_FILE):
- self.start_guid(verbose=verbose,
- notify_function=kwargs.get('notify_function', None))
- print >>sys.stderr, "time=%s, guid done" % (str(time.time()))
-
- return self.xid
-
- def remove_from_disk(self):
- # nothing to remove
- pass
-
-# register classes
-register_qubes_vm_class(QubesDisposableVm)
diff --git a/core-modules/01QubesHVm.py b/core-modules/01QubesHVm.py
deleted file mode 100644
index c98b67b5..00000000
--- a/core-modules/01QubesHVm.py
+++ /dev/null
@@ -1,457 +0,0 @@
-#!/usr/bin/python2
-# -*- coding: utf-8 -*-
-#
-# The Qubes OS Project, http://www.qubes-os.org
-#
-# Copyright (C) 2010 Joanna Rutkowska
-# Copyright (C) 2013 Marek Marczykowski
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-#
-
-import os
-import os.path
-import signal
-import subprocess
-import sys
-import shutil
-from xml.etree import ElementTree
-
-from qubes.qubes import (
- dry_run,
- defaults,
- register_qubes_vm_class,
- system_path,
- vmm,
- QubesException,
- QubesResizableVm,
-)
-
-
-system_path["config_template_hvm"] = '/usr/share/qubes/vm-template-hvm.xml'
-
-defaults["hvm_disk_size"] = 20*1024*1024*1024
-defaults["hvm_private_img_size"] = 2*1024*1024*1024
-defaults["hvm_memory"] = 512
-
-
-class QubesHVm(QubesResizableVm):
- """
- A class that represents an HVM. A child of QubesVm.
- """
-
- # FIXME: logically should inherit after QubesAppVm, but none of its methods
- # are useful for HVM
-
- def get_attrs_config(self):
- attrs = super(QubesHVm, self).get_attrs_config()
- attrs.pop('kernel')
- attrs.pop('kernels_dir')
- attrs.pop('kernelopts')
- attrs.pop('uses_default_kernel')
- attrs.pop('uses_default_kernelopts')
- attrs['dir_path']['func'] = lambda value: value if value is not None \
- else os.path.join(system_path["qubes_appvms_dir"], self.name)
- attrs['config_file_template']['func'] = \
- lambda x: system_path["config_template_hvm"]
- attrs['drive'] = { 'attr': '_drive',
- 'save': lambda: str(self.drive) }
- # Remove this two lines when HVM will get qmemman support
- attrs['maxmem'].pop('save')
- attrs['maxmem']['func'] = lambda x: self.memory
- attrs['timezone'] = { 'default': 'localtime',
- 'save': lambda: str(self.timezone) }
- attrs['qrexec_installed'] = { 'default': False,
- 'attr': '_qrexec_installed',
- 'save': lambda: str(self._qrexec_installed) }
- attrs['guiagent_installed'] = { 'default' : False,
- 'attr': '_guiagent_installed',
- 'save': lambda: str(self._guiagent_installed) }
- attrs['seamless_gui_mode'] = { 'default': False,
- 'attr': '_seamless_gui_mode',
- 'save': lambda: str(self._seamless_gui_mode) }
- attrs['services']['default'] = "{'meminfo-writer': False}"
-
- attrs['memory']['default'] = defaults["hvm_memory"]
-
- return attrs
-
- def __init__(self, **kwargs):
-
- super(QubesHVm, self).__init__(**kwargs)
-
- # Default for meminfo-writer have changed to (correct) False in the
- # same version as introduction of guiagent_installed, so for older VMs
- # with wrong setting, change is based on 'guiagent_installed' presence
- if "guiagent_installed" not in kwargs and \
- (not 'xml_element' in kwargs or kwargs['xml_element'].get('guiagent_installed') is None):
- self.services['meminfo-writer'] = False
-
- @property
- def type(self):
- return "HVM"
-
- def is_appvm(self):
- return True
-
- @classmethod
- def is_template_compatible(cls, template):
- if template and (not template.is_template() or template.type != "TemplateHVM"):
- return False
- return True
-
- def get_clone_attrs(self):
- attrs = super(QubesHVm, self).get_clone_attrs()
- attrs.remove('kernel')
- attrs.remove('uses_default_kernel')
- attrs.remove('kernelopts')
- attrs.remove('uses_default_kernelopts')
- attrs += [ 'timezone' ]
- attrs += [ 'qrexec_installed' ]
- attrs += [ 'guiagent_installed' ]
- return attrs
-
- @property
- def qrexec_installed(self):
- return self._qrexec_installed or \
- bool(self.template and self.template.qrexec_installed)
-
- @qrexec_installed.setter
- def qrexec_installed(self, value):
- if self.template and self.template.qrexec_installed and not value:
- print >>sys.stderr, "WARNING: When qrexec_installed set in template, it will be propagated to the VM"
- self._qrexec_installed = value
-
- @property
- def guiagent_installed(self):
- return self._guiagent_installed or \
- bool(self.template and self.template.guiagent_installed)
-
- @guiagent_installed.setter
- def guiagent_installed(self, value):
- if self.template and self.template.guiagent_installed and not value:
- print >>sys.stderr, "WARNING: When guiagent_installed set in template, it will be propagated to the VM"
- self._guiagent_installed = value
-
- @property
- def seamless_gui_mode(self):
- if not self.guiagent_installed:
- return False
- return self._seamless_gui_mode
-
- @seamless_gui_mode.setter
- def seamless_gui_mode(self, value):
- if self._seamless_gui_mode == value:
- return
- if not self.guiagent_installed and value:
- raise ValueError("Seamless GUI mode requires GUI agent installed")
-
- self._seamless_gui_mode = value
- if self.is_running():
- self.send_gui_mode()
-
- @property
- def drive(self):
- return self._drive
-
- @drive.setter
- def drive(self, value):
- if value is None:
- self._drive = None
- return
-
- # strip type for a moment
- drv_type = "cdrom"
- if value.startswith("hd:") or value.startswith("cdrom:"):
- (drv_type, unused, value) = value.partition(":")
- drv_type = drv_type.lower()
-
- # sanity check
- if drv_type not in ['hd', 'cdrom']:
- raise QubesException("Unsupported drive type: %s" % type)
-
- if value.count(":") == 0:
- value = "dom0:" + value
- if value.count(":/") == 0:
- # FIXME: when Windows backend will be supported, improve this
- raise QubesException("Drive path must be absolute")
-
- self._drive = drv_type + ":" + value
-
- def create_on_disk(self, verbose, source_template = None):
- self.log.debug('create_on_disk(source_template={!r})'.format(
- source_template))
- if dry_run:
- return
-
- if source_template is None:
- source_template = self.template
-
- # create empty disk
- self.storage.private_img_size = defaults["hvm_private_img_size"]
- self.storage.root_img_size = defaults["hvm_disk_size"]
- self.storage.create_on_disk(verbose, source_template)
-
- if verbose:
- print >> sys.stderr, "--> Creating icon symlink: {0} -> {1}".format(self.icon_path, self.label.icon_path)
-
- try:
- if hasattr(os, "symlink"):
- os.symlink (self.label.icon_path, self.icon_path)
- else:
- shutil.copy(self.label.icon_path, self.icon_path)
- except Exception as e:
- print >> sys.stderr, "WARNING: Failed to set VM icon: %s" % str(e)
-
- # Make sure that we have UUID allocated
- self._update_libvirt_domain()
-
- # fire hooks
- for hook in self.hooks_create_on_disk:
- hook(self, verbose, source_template=source_template)
-
- def get_private_img_sz(self):
- if not os.path.exists(self.private_img):
- return 0
-
- return os.path.getsize(self.private_img)
-
- def resize_private_img(self, size):
- assert size >= self.get_private_img_sz(), "Cannot shrink private.img"
-
- if self.is_running():
- raise NotImplementedError("Online resize of HVM's private.img not implemented, shutdown the VM first")
-
- self.storage.resize_private_img(size)
-
- def get_config_params(self):
-
- params = super(QubesHVm, self).get_config_params()
-
- self.storage.drive = self.drive
- params.update(self.storage.get_config_params())
- params['volatiledev'] = ''
-
- if self.timezone.lower() == 'localtime':
- params['time_basis'] = 'localtime'
- params['timeoffset'] = '0'
- elif self.timezone.isdigit():
- params['time_basis'] = 'UTC'
- params['timeoffset'] = self.timezone
- else:
- print >>sys.stderr, "WARNING: invalid 'timezone' value: %s" % self.timezone
- params['time_basis'] = 'UTC'
- params['timeoffset'] = '0'
- return params
-
- def verify_files(self):
- if dry_run:
- return
-
- self.storage.verify_files()
-
- # fire hooks
- for hook in self.hooks_verify_files:
- hook(self)
-
- return True
-
- @property
- def vif(self):
- if self.xid < 0:
- return None
- if self.netvm is None:
- return None
- return "vif{0}.+".format(self.stubdom_xid)
-
- @property
- def mac(self):
- if self._mac is not None:
- return self._mac
- elif self.template is not None:
- return self.template.mac
- else:
- return "00:16:3E:5E:6C:{qid:02X}".format(qid=self.qid)
-
- @mac.setter
- def mac(self, value):
- self._mac = value
-
- def run(self, command, **kwargs):
- if self.qrexec_installed:
- if 'gui' in kwargs and kwargs['gui']==False:
- command = "nogui:" + command
- return super(QubesHVm, self).run(command, **kwargs)
- else:
- raise QubesException("Needs qrexec agent installed in VM to use this function. See also qvm-prefs.")
-
- @property
- def stubdom_xid(self):
- if self.xid < 0:
- return -1
-
- if vmm.xs is None:
- return -1
-
- stubdom_xid_str = vmm.xs.read('', '/local/domain/%d/image/device-model-domid' % self.xid)
- if stubdom_xid_str is not None:
- return int(stubdom_xid_str)
- else:
- return -1
-
- def validate_drive_path(self, drive):
- drive_type, drive_domain, drive_path = drive.split(':', 2)
- if drive_domain == 'dom0':
- if not os.path.exists(drive_path):
- raise QubesException("Invalid drive path '{}'".format(
- drive_path))
-
- def start(self, *args, **kwargs):
- if self.drive:
- self.validate_drive_path(self.drive)
- # make it available to storage.prepare_for_vm_startup, which is
- # called before actually building VM libvirt configuration
- self.storage.drive = self.drive
-
- if self.template and self.template.is_running():
- raise QubesException("Cannot start the HVM while its template is running")
- try:
- if 'mem_required' not in kwargs:
- # Reserve 44MB for stubdomain
- kwargs['mem_required'] = (self.memory + 44) * 1024 * 1024
- return super(QubesHVm, self).start(*args, **kwargs)
- except QubesException as e:
- capabilities = vmm.libvirt_conn.getCapabilities()
- tree = ElementTree.fromstring(capabilities)
- os_types = tree.findall('./guest/os_type')
- if 'hvm' not in map(lambda x: x.text, os_types):
- raise QubesException("Cannot start HVM without VT-x/AMD-v enabled")
- else:
- raise
-
- def start_stubdom_guid(self, verbose=True):
-
- guid_cmd = [system_path["qubes_guid_path"],
- "-d", str(self.stubdom_xid),
- "-t", str(self.xid),
- "-N", self.name,
- "-c", self.label.color,
- "-i", self.label.icon_path,
- "-l", str(self.label.index)]
- if self.debug:
- guid_cmd += ['-v', '-v']
- elif not verbose:
- guid_cmd += ['-q']
- retcode = subprocess.call (guid_cmd)
- if (retcode != 0) :
- raise QubesException("Cannot start qubes-guid!")
-
- def start_guid(self, verbose=True, notify_function=None,
- before_qrexec=False, **kwargs):
- if not before_qrexec:
- return
-
- if not self.guiagent_installed or self.debug:
- if verbose:
- print >> sys.stderr, "--> Starting Qubes GUId (full screen)..."
- self.start_stubdom_guid(verbose=verbose)
-
- kwargs['extra_guid_args'] = kwargs.get('extra_guid_args', []) + \
- ['-Q', '-n']
-
- stubdom_guid_pidfile = \
- '/var/run/qubes/guid-running.%d' % self.stubdom_xid
- if not self.debug and os.path.exists(stubdom_guid_pidfile):
- # Terminate stubdom guid once "real" gui agent connects
- stubdom_guid_pid = int(open(stubdom_guid_pidfile, 'r').read())
- kwargs['extra_guid_args'] += ['-K', str(stubdom_guid_pid)]
-
- super(QubesHVm, self).start_guid(verbose, notify_function, **kwargs)
-
- def start_qrexec_daemon(self, **kwargs):
- if not self.qrexec_installed:
- if kwargs.get('verbose', False):
- print >> sys.stderr, "--> Starting the qrexec daemon..."
- xid = self.get_xid()
- qrexec_env = os.environ.copy()
- qrexec_env['QREXEC_STARTUP_NOWAIT'] = '1'
- retcode = subprocess.call ([system_path["qrexec_daemon_path"], str(xid), self.name, self.default_user], env=qrexec_env)
- if (retcode != 0) :
- self.force_shutdown(xid=xid)
- raise OSError ("ERROR: Cannot execute qrexec-daemon!")
- else:
- super(QubesHVm, self).start_qrexec_daemon(**kwargs)
-
- if self.guiagent_installed:
- if kwargs.get('verbose'):
- print >> sys.stderr, "--> Waiting for user '%s' login..." % self.default_user
-
- self.wait_for_session(notify_function=kwargs.get('notify_function', None))
- self.send_gui_mode()
-
- def send_gui_mode(self):
- if self.seamless_gui_mode:
- service_input = "SEAMLESS"
- else:
- service_input = "FULLSCREEN"
-
- self.run_service("qubes.SetGuiMode", input=service_input)
-
- def _cleanup_zombie_domains(self):
- super(QubesHVm, self)._cleanup_zombie_domains()
- if not self.is_running():
- xc_stubdom = self.get_xc_dominfo(name=self.name+'-dm')
- if xc_stubdom is not None:
- if xc_stubdom['paused'] == 1:
- subprocess.call(['xl', 'destroy', str(xc_stubdom['domid'])])
- if xc_stubdom['dying'] == 1:
- # GUID still running?
- guid_pidfile = \
- '/var/run/qubes/guid-running.%d' % xc_stubdom['domid']
- if os.path.exists(guid_pidfile):
- guid_pid = open(guid_pidfile).read().strip()
- os.kill(int(guid_pid), 15)
-
- def suspend(self):
- if dry_run:
- return
-
- if not self.is_running() and not self.is_paused():
- raise QubesException ("VM not running!")
-
- self.pause()
-
- def is_guid_running(self):
- # If user force the guiagent, is_guid_running will mimic a standard QubesVM
- if self.guiagent_installed:
- return super(QubesHVm, self).is_guid_running()
- else:
- xid = self.stubdom_xid
- if xid < 0:
- return False
- if not os.path.exists('/var/run/qubes/guid-running.%d' % xid):
- return False
- return True
-
- def is_fully_usable(self):
- # Running gui-daemon implies also VM running
- if not self.is_guid_running():
- return False
- if self.qrexec_installed and not self.is_qrexec_running():
- return False
- return True
-
-register_qubes_vm_class(QubesHVm)
diff --git a/core-modules/02QubesTemplateHVm.py b/core-modules/02QubesTemplateHVm.py
deleted file mode 100644
index 6452a8eb..00000000
--- a/core-modules/02QubesTemplateHVm.py
+++ /dev/null
@@ -1,110 +0,0 @@
-#!/usr/bin/python2
-# -*- coding: utf-8 -*-
-#
-# The Qubes OS Project, http://www.qubes-os.org
-#
-# Copyright (C) 2010 Joanna Rutkowska
-# Copyright (C) 2013 Marek Marczykowski
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-#
-
-import os
-import os.path
-import subprocess
-import stat
-import sys
-import re
-
-from qubes.qubes import QubesHVm,register_qubes_vm_class,dry_run,vmm
-from qubes.qubes import QubesException,QubesVmCollection
-from qubes.qubes import system_path,defaults
-
-class QubesTemplateHVm(QubesHVm):
- """
- A class that represents an HVM template. A child of QubesHVm.
- """
-
- # In which order load this VM type from qubes.xml
- load_order = 50
-
- def get_attrs_config(self):
- attrs_config = super(QubesTemplateHVm, self).get_attrs_config()
- attrs_config['dir_path']['func'] = \
- lambda value: value if value is not None else \
- os.path.join(system_path["qubes_templates_dir"], self.name)
- attrs_config['label']['default'] = defaults["template_label"]
- return attrs_config
-
-
- def __init__(self, **kwargs):
-
- super(QubesTemplateHVm, self).__init__(**kwargs)
-
- self.appvms = QubesVmCollection()
-
- @property
- def type(self):
- return "TemplateHVM"
-
- @property
- def updateable(self):
- return True
-
- def is_template(self):
- return True
-
- def is_appvm(self):
- return False
-
- @property
- def rootcow_img(self):
- return self.storage.rootcow_img
-
- @classmethod
- def is_template_compatible(cls, template):
- if template is None:
- return True
- return False
-
- def resize_root_img(self, size):
- for vm in self.appvms.values():
- if vm.is_running():
- raise QubesException("Cannot resize root.img while any VM "
- "based on this tempate is running")
- return super(QubesTemplateHVm, self).resize_root_img(size)
-
- def start(self, *args, **kwargs):
- for vm in self.appvms.values():
- if vm.is_running():
- raise QubesException("Cannot start HVM template while VMs based on it are running")
- return super(QubesTemplateHVm, self).start(*args, **kwargs)
-
- def commit_changes (self, verbose = False):
- self.log.debug('commit_changes()')
-
- if not vmm.offline_mode:
- assert not self.is_running(), "Attempt to commit changes on running Template VM!"
-
- if verbose:
- print >> sys.stderr, "--> Commiting template updates... COW: {0}...".format (self.rootcow_img)
-
- if dry_run:
- return
-
- self.storage.commit_template_changes()
-
-register_qubes_vm_class(QubesTemplateHVm)
diff --git a/core-modules/Makefile b/core-modules/Makefile
deleted file mode 100644
index b5c4046d..00000000
--- a/core-modules/Makefile
+++ /dev/null
@@ -1,15 +0,0 @@
-PYTHON_QUBESMODPATH = $(PYTHON_SITEPATH)/qubes/modules
-
-all:
- python -m compileall .
- python -O -m compileall .
-
-install:
-ifndef PYTHON_SITEPATH
- $(error PYTHON_SITEPATH not defined)
-endif
- mkdir -p $(DESTDIR)$(PYTHON_QUBESMODPATH)
- cp 0*.py $(DESTDIR)$(PYTHON_QUBESMODPATH)
- cp 0*.py[co] $(DESTDIR)$(PYTHON_QUBESMODPATH)
- cp __init__.py $(DESTDIR)$(PYTHON_QUBESMODPATH)
- cp __init__.py[co] $(DESTDIR)$(PYTHON_QUBESMODPATH)
diff --git a/core-modules/README.txt b/core-modules/README.txt
deleted file mode 100644
index 17f6445a..00000000
--- a/core-modules/README.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-This directory contains Qubes core modules. It will be loaded in
-lexicographical order, use numeric prefix to force load ordering.
-
-0* - Qubes base modules
-00* - Qubes core VM classes
diff --git a/core/.gitignore b/core/.gitignore
deleted file mode 100644
index 2bc03a8d..00000000
--- a/core/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-*.pyo
diff --git a/core/Makefile b/core/Makefile
deleted file mode 100644
index 9204929f..00000000
--- a/core/Makefile
+++ /dev/null
@@ -1,33 +0,0 @@
-OS ?= Linux
-
-PYTHON_QUBESPATH = $(PYTHON_SITEPATH)/qubes
-SETTINGS_SUFFIX = $(BACKEND_VMM)-$(OS)
-
-all:
- python -m compileall .
- python -O -m compileall .
- make -C storage all
-
-install:
-ifndef PYTHON_SITEPATH
- $(error PYTHON_SITEPATH not defined)
-endif
- mkdir -p $(DESTDIR)$(PYTHON_QUBESPATH)
- cp qubes.py $(DESTDIR)$(PYTHON_QUBESPATH)
- cp qubes.py[co] $(DESTDIR)$(PYTHON_QUBESPATH)
- cp qubesutils.py $(DESTDIR)$(PYTHON_QUBESPATH)
- cp qubesutils.py[co] $(DESTDIR)$(PYTHON_QUBESPATH)
- cp guihelpers.py $(DESTDIR)$(PYTHON_QUBESPATH)
- cp guihelpers.py[co] $(DESTDIR)$(PYTHON_QUBESPATH)
- cp notify.py $(DESTDIR)$(PYTHON_QUBESPATH)
- cp notify.py[co] $(DESTDIR)$(PYTHON_QUBESPATH)
- cp backup.py $(DESTDIR)$(PYTHON_QUBESPATH)
- cp backup.py[co] $(DESTDIR)$(PYTHON_QUBESPATH)
-ifneq ($(BACKEND_VMM),)
- if [ -r settings-$(SETTINGS_SUFFIX).py ]; then \
- cp settings-$(SETTINGS_SUFFIX).py $(DESTDIR)$(PYTHON_QUBESPATH)/settings.py && \
- cp settings-$(SETTINGS_SUFFIX).pyc $(DESTDIR)$(PYTHON_QUBESPATH)/settings.pyc && \
- cp settings-$(SETTINGS_SUFFIX).pyo $(DESTDIR)$(PYTHON_QUBESPATH)/settings.pyo; \
- fi
-endif
- make -C storage install
diff --git a/core/backup.py b/core/backup.py
deleted file mode 100644
index 048f2be9..00000000
--- a/core/backup.py
+++ /dev/null
@@ -1,2325 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-#
-# The Qubes OS Project, http://www.qubes-os.org
-#
-# Copyright (C) 2013-2015 Marek Marczykowski-Górecki
-#
-# Copyright (C) 2013 Olivier Médoc
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-#
-from __future__ import unicode_literals
-from qubes import QubesException, QubesVmCollection
-from qubes import QubesVmClasses
-from qubes import system_path, vm_files
-from qubesutils import size_to_human, print_stdout, print_stderr, get_disk_usage
-import sys
-import os
-import fcntl
-import subprocess
-import re
-import shutil
-import tempfile
-import time
-import grp
-import pwd
-import errno
-import datetime
-from multiprocessing import Queue, Process
-
-BACKUP_DEBUG = False
-
-HEADER_FILENAME = 'backup-header'
-DEFAULT_CRYPTO_ALGORITHM = 'aes-256-cbc'
-DEFAULT_HMAC_ALGORITHM = 'SHA512'
-DEFAULT_COMPRESSION_FILTER = 'gzip'
-CURRENT_BACKUP_FORMAT_VERSION = '3'
-# Maximum size of error message get from process stderr (including VM process)
-MAX_STDERR_BYTES = 1024
-# header + qubes.xml max size
-HEADER_QUBES_XML_MAX_SIZE = 1024 * 1024
-
-# global state for backup_cancel()
-running_backup_operation = None
-
-
-class BackupOperationInfo:
- def __init__(self):
- self.canceled = False
- self.processes_to_kill_on_cancel = []
- self.tmpdir_to_remove = None
-
-
-class BackupCanceledError(QubesException):
- def __init__(self, msg, tmpdir=None):
- super(BackupCanceledError, self).__init__(msg)
- self.tmpdir = tmpdir
-
-
-class BackupHeader:
- version = 'version'
- encrypted = 'encrypted'
- compressed = 'compressed'
- compression_filter = 'compression-filter'
- crypto_algorithm = 'crypto-algorithm'
- hmac_algorithm = 'hmac-algorithm'
- bool_options = ['encrypted', 'compressed']
- int_options = ['version']
-
-
-def file_to_backup(file_path, subdir=None):
- sz = get_disk_usage(file_path)
-
- if subdir is None:
- abs_file_path = os.path.abspath(file_path)
- abs_base_dir = os.path.abspath(system_path["qubes_base_dir"]) + '/'
- abs_file_dir = os.path.dirname(abs_file_path) + '/'
- (nothing, directory, subdir) = abs_file_dir.partition(abs_base_dir)
- assert nothing == ""
- assert directory == abs_base_dir
- else:
- if len(subdir) > 0 and not subdir.endswith('/'):
- subdir += '/'
- return [{"path": file_path, "size": sz, "subdir": subdir}]
-
-
-def backup_cancel():
- """
- Cancel currently running backup/restore operation
-
- @return: True if any operation was signaled
- """
- if running_backup_operation is None:
- return False
-
- running_backup_operation.canceled = True
- for proc in running_backup_operation.processes_to_kill_on_cancel:
- try:
- proc.terminate()
- except:
- pass
- return True
-
-
-def backup_prepare(vms_list=None, exclude_list=None,
- print_callback=print_stdout, hide_vm_names=True):
- """
- If vms = None, include all (sensible) VMs;
- exclude_list is always applied
- """
- files_to_backup = file_to_backup(system_path["qubes_store_filename"])
-
- if exclude_list is None:
- exclude_list = []
-
- qvm_collection = QubesVmCollection()
- qvm_collection.lock_db_for_writing()
- qvm_collection.load()
-
- if vms_list is None:
- all_vms = [vm for vm in qvm_collection.values()]
- selected_vms = [vm for vm in all_vms if vm.include_in_backups]
- appvms_to_backup = [vm for vm in selected_vms if
- vm.is_appvm() and not vm.internal]
- netvms_to_backup = [vm for vm in selected_vms if
- vm.is_netvm() and not vm.qid == 0]
- template_vms_worth_backingup = [vm for vm in selected_vms if (
- vm.is_template() and vm.include_in_backups)]
- dom0 = [qvm_collection[0]]
-
- vms_list = appvms_to_backup + netvms_to_backup + \
- template_vms_worth_backingup + dom0
-
- vms_for_backup = vms_list
- # Apply exclude list
- if exclude_list:
- vms_for_backup = [vm for vm in vms_list if vm.name not in exclude_list]
-
- there_are_running_vms = False
-
- fields_to_display = [
- {"name": "VM", "width": 16},
- {"name": "type", "width": 12},
- {"name": "size", "width": 12}
- ]
-
- # Display the header
- s = ""
- for f in fields_to_display:
- fmt = "{{0:-^{0}}}-+".format(f["width"] + 1)
- s += fmt.format('-')
- print_callback(s)
- s = ""
- for f in fields_to_display:
- fmt = "{{0:>{0}}} |".format(f["width"] + 1)
- s += fmt.format(f["name"])
- print_callback(s)
- s = ""
- for f in fields_to_display:
- fmt = "{{0:-^{0}}}-+".format(f["width"] + 1)
- s += fmt.format('-')
- print_callback(s)
-
- files_to_backup_index = 0
- for vm in sorted(vms_for_backup, key=lambda vm: vm.name):
- if vm.is_template():
- # handle templates later
- continue
- if vm.qid == 0:
- # handle dom0 later
- continue
-
- if hide_vm_names:
- subdir = 'vm%d/' % vm.qid
- else:
- subdir = None
-
- if vm.private_img is not None:
- files_to_backup += file_to_backup(vm.private_img, subdir)
-
- if vm.is_appvm():
- files_to_backup += file_to_backup(vm.icon_path, subdir)
- if vm.updateable:
- if os.path.exists(vm.dir_path + "/apps.templates"):
- # template
- files_to_backup += file_to_backup(
- vm.dir_path + "/apps.templates", subdir)
- else:
- # standaloneVM
- files_to_backup += file_to_backup(vm.dir_path + "/apps", subdir)
-
- if os.path.exists(vm.dir_path + "/kernels"):
- files_to_backup += file_to_backup(vm.dir_path + "/kernels",
- subdir)
- if os.path.exists(vm.firewall_conf):
- files_to_backup += file_to_backup(vm.firewall_conf, subdir)
- if 'appmenus_whitelist' in vm_files and \
- os.path.exists(os.path.join(vm.dir_path,
- vm_files['appmenus_whitelist'])):
- files_to_backup += file_to_backup(
- os.path.join(vm.dir_path, vm_files['appmenus_whitelist']),
- subdir)
-
- if vm.updateable:
- files_to_backup += file_to_backup(vm.root_img, subdir)
-
- s = ""
- fmt = "{{0:>{0}}} |".format(fields_to_display[0]["width"] + 1)
- s += fmt.format(vm.name)
-
- fmt = "{{0:>{0}}} |".format(fields_to_display[1]["width"] + 1)
- if vm.is_netvm():
- s += fmt.format("NetVM" + (" + Sys" if vm.updateable else ""))
- else:
- s += fmt.format("AppVM" + (" + Sys" if vm.updateable else ""))
-
- vm_size = reduce(lambda x, y: x + y["size"],
- files_to_backup[files_to_backup_index:],
- 0)
- files_to_backup_index = len(files_to_backup)
-
- fmt = "{{0:>{0}}} |".format(fields_to_display[2]["width"] + 1)
- s += fmt.format(size_to_human(vm_size))
-
- if vm.is_running():
- s += " <-- The VM is running, please shut it down before proceeding " \
- "with the backup!"
- there_are_running_vms = True
-
- print_callback(s)
-
- for vm in vms_for_backup:
- if not vm.is_template():
- # already handled
- continue
- if vm.qid == 0:
- # handle dom0 later
- continue
- vm_sz = vm.get_disk_utilization()
- if hide_vm_names:
- template_subdir = 'vm%d/' % vm.qid
- else:
- template_subdir = os.path.relpath(
- vm.dir_path,
- system_path["qubes_base_dir"]) + '/'
- template_to_backup = [{"path": vm.dir_path + '/.',
- "size": vm_sz,
- "subdir": template_subdir}]
- files_to_backup += template_to_backup
-
- s = ""
- fmt = "{{0:>{0}}} |".format(fields_to_display[0]["width"] + 1)
- s += fmt.format(vm.name)
-
- fmt = "{{0:>{0}}} |".format(fields_to_display[1]["width"] + 1)
- s += fmt.format("Template VM")
-
- fmt = "{{0:>{0}}} |".format(fields_to_display[2]["width"] + 1)
- s += fmt.format(size_to_human(vm_sz))
-
- if vm.is_running():
- s += " <-- The VM is running, please shut it down before proceeding " \
- "with the backup!"
- there_are_running_vms = True
-
- print_callback(s)
-
- # Initialize backup flag on all VMs
- vms_for_backup_qid = [vm.qid for vm in vms_for_backup]
- for vm in qvm_collection.values():
- vm.backup_content = False
- if vm.qid == 0:
- # handle dom0 later
- continue
-
- if vm.qid in vms_for_backup_qid:
- vm.backup_content = True
- vm.backup_size = vm.get_disk_utilization()
- if hide_vm_names:
- vm.backup_path = 'vm%d' % vm.qid
- else:
- vm.backup_path = os.path.relpath(vm.dir_path,
- system_path["qubes_base_dir"])
-
- # Dom0 user home
- if 0 in vms_for_backup_qid:
- local_user = grp.getgrnam('qubes').gr_mem[0]
- home_dir = pwd.getpwnam(local_user).pw_dir
- # Home dir should have only user-owned files, so fix it now to prevent
- # permissions problems - some root-owned files can left after
- # 'sudo bash' and similar commands
- subprocess.check_call(['sudo', 'chown', '-R', local_user, home_dir])
-
- home_sz = get_disk_usage(home_dir)
- home_to_backup = [
- {"path": home_dir, "size": home_sz, "subdir": 'dom0-home/'}]
- files_to_backup += home_to_backup
-
- vm = qvm_collection[0]
- vm.backup_content = True
- vm.backup_size = home_sz
- vm.backup_path = os.path.join('dom0-home', os.path.basename(home_dir))
-
- s = ""
- fmt = "{{0:>{0}}} |".format(fields_to_display[0]["width"] + 1)
- s += fmt.format('Dom0')
-
- fmt = "{{0:>{0}}} |".format(fields_to_display[1]["width"] + 1)
- s += fmt.format("User home")
-
- fmt = "{{0:>{0}}} |".format(fields_to_display[2]["width"] + 1)
- s += fmt.format(size_to_human(home_sz))
-
- print_callback(s)
-
- qvm_collection.save()
- # FIXME: should be after backup completed
- qvm_collection.unlock_db()
-
- total_backup_sz = 0
- for f in files_to_backup:
- total_backup_sz += f["size"]
-
- s = ""
- for f in fields_to_display:
- fmt = "{{0:-^{0}}}-+".format(f["width"] + 1)
- s += fmt.format('-')
- print_callback(s)
-
- s = ""
- fmt = "{{0:>{0}}} |".format(fields_to_display[0]["width"] + 1)
- s += fmt.format("Total size:")
- fmt = "{{0:>{0}}} |".format(
- fields_to_display[1]["width"] + 1 + 2 + fields_to_display[2][
- "width"] + 1)
- s += fmt.format(size_to_human(total_backup_sz))
- print_callback(s)
-
- s = ""
- for f in fields_to_display:
- fmt = "{{0:-^{0}}}-+".format(f["width"] + 1)
- s += fmt.format('-')
- print_callback(s)
-
- vms_not_for_backup = [vm.name for vm in qvm_collection.values()
- if not vm.backup_content]
- print_callback("VMs not selected for backup:\n%s" % "\n".join(sorted(
- vms_not_for_backup)))
-
- if there_are_running_vms:
- raise QubesException("Please shutdown all VMs before proceeding.")
-
- for fileinfo in files_to_backup:
- assert len(fileinfo["subdir"]) == 0 or fileinfo["subdir"][-1] == '/', \
- "'subdir' must ends with a '/': %s" % unicode(fileinfo)
-
- return files_to_backup
-
-
-class SendWorker(Process):
- def __init__(self, queue, base_dir, backup_stdout):
- super(SendWorker, self).__init__()
- self.queue = queue
- self.base_dir = base_dir
- self.backup_stdout = backup_stdout
-
- def run(self):
- if BACKUP_DEBUG:
- print "Started sending thread"
-
- if BACKUP_DEBUG:
- print "Moving to temporary dir", self.base_dir
- os.chdir(self.base_dir)
-
- for filename in iter(self.queue.get, None):
- if filename == "FINISHED" or filename == "ERROR":
- break
-
- if BACKUP_DEBUG:
- print "Sending file", filename
- # This tar used for sending data out need to be as simple, as
- # simple, as featureless as possible. It will not be
- # verified before untaring.
- tar_final_cmd = ["tar", "-cO", "--posix",
- "-C", self.base_dir, filename]
- final_proc = subprocess.Popen(tar_final_cmd,
- stdin=subprocess.PIPE,
- stdout=self.backup_stdout)
- if final_proc.wait() >= 2:
- if self.queue.full():
- # if queue is already full, remove some entry to wake up
- # main thread, so it will be able to notice error
- self.queue.get()
- # handle only exit code 2 (tar fatal error) or
- # greater (call failed?)
- raise QubesException(
- "ERROR: Failed to write the backup, out of disk space? "
- "Check console output or ~/.xsession-errors for details.")
-
- # Delete the file as we don't need it anymore
- if BACKUP_DEBUG:
- print "Removing file", filename
- os.remove(filename)
-
- if BACKUP_DEBUG:
- print "Finished sending thread"
-
-
-def prepare_backup_header(target_directory, passphrase, compressed=False,
- encrypted=False,
- hmac_algorithm=DEFAULT_HMAC_ALGORITHM,
- crypto_algorithm=DEFAULT_CRYPTO_ALGORITHM,
- compression_filter=None):
- header_file_path = os.path.join(target_directory, HEADER_FILENAME)
- with open(header_file_path, "w") as f:
- f.write(str("%s=%s\n" % (BackupHeader.version,
- CURRENT_BACKUP_FORMAT_VERSION)))
- f.write(str("%s=%s\n" % (BackupHeader.hmac_algorithm, hmac_algorithm)))
- f.write(str("%s=%s\n" % (BackupHeader.crypto_algorithm,
- crypto_algorithm)))
- f.write(str("%s=%s\n" % (BackupHeader.encrypted, str(encrypted))))
- f.write(str("%s=%s\n" % (BackupHeader.compressed, str(compressed))))
- if compressed:
- f.write(str("%s=%s\n" % (BackupHeader.compression_filter,
- str(compression_filter))))
-
- hmac = subprocess.Popen(["openssl", "dgst",
- "-" + hmac_algorithm, "-hmac", passphrase],
- stdin=open(header_file_path, "r"),
- stdout=open(header_file_path + ".hmac", "w"))
- if hmac.wait() != 0:
- raise QubesException("Failed to compute hmac of header file")
- return HEADER_FILENAME, HEADER_FILENAME + ".hmac"
-
-
-def backup_do(base_backup_dir, files_to_backup, passphrase,
- progress_callback=None, encrypted=False, appvm=None,
- compressed=False, hmac_algorithm=DEFAULT_HMAC_ALGORITHM,
- crypto_algorithm=DEFAULT_CRYPTO_ALGORITHM,
- tmpdir=None):
- global running_backup_operation
-
- def queue_put_with_check(proc, vmproc, queue, element):
- if queue.full():
- if not proc.is_alive():
- if vmproc:
- message = ("Failed to write the backup, VM output:\n" +
- vmproc.stderr.read())
- else:
- message = "Failed to write the backup. Out of disk space?"
- raise QubesException(message)
- queue.put(element)
-
- total_backup_sz = 0
- passphrase = passphrase.encode('utf-8')
- for f in files_to_backup:
- total_backup_sz += f["size"]
-
- if isinstance(compressed, str):
- compression_filter = compressed
- else:
- compression_filter = DEFAULT_COMPRESSION_FILTER
-
- running_backup_operation = BackupOperationInfo()
- vmproc = None
- tar_sparse = None
- if appvm is not None:
- # Prepare the backup target (Qubes service call)
- backup_target = "QUBESRPC qubes.Backup dom0"
-
- # If APPVM, STDOUT is a PIPE
- vmproc = appvm.run(command=backup_target, passio_popen=True,
- passio_stderr=True)
- vmproc.stdin.write(base_backup_dir.
- replace("\r", "").replace("\n", "") + "\n")
- backup_stdout = vmproc.stdin
- running_backup_operation.processes_to_kill_on_cancel.append(vmproc)
- else:
- # Prepare the backup target (local file)
- if os.path.isdir(base_backup_dir):
- backup_target = base_backup_dir + "/qubes-{0}". \
- format(time.strftime("%Y-%m-%dT%H%M%S"))
- else:
- backup_target = base_backup_dir
-
- # Create the target directory
- if not os.path.exists(os.path.dirname(base_backup_dir)):
- raise QubesException(
- "ERROR: the backup directory for {0} does not exists".
- format(base_backup_dir))
-
- # If not APPVM, STDOUT is a local file
- backup_stdout = open(backup_target, 'wb')
-
- global blocks_backedup
- blocks_backedup = 0
- if callable(progress_callback):
- progress = blocks_backedup * 11 / total_backup_sz
- progress_callback(progress)
-
- backup_tmpdir = tempfile.mkdtemp(prefix="backup_", dir=tmpdir)
- running_backup_operation.tmpdir_to_remove = backup_tmpdir
-
- # Tar with tape length does not deals well with stdout (close stdout between
- # two tapes)
- # For this reason, we will use named pipes instead
- if BACKUP_DEBUG:
- print "Working in", backup_tmpdir
-
- backup_pipe = os.path.join(backup_tmpdir, "backup_pipe")
- if BACKUP_DEBUG:
- print "Creating pipe in:", backup_pipe
- os.mkfifo(backup_pipe)
-
- if BACKUP_DEBUG:
- print "Will backup:", files_to_backup
-
- header_files = prepare_backup_header(backup_tmpdir, passphrase,
- compressed=bool(compressed),
- encrypted=encrypted,
- hmac_algorithm=hmac_algorithm,
- crypto_algorithm=crypto_algorithm,
- compression_filter=compression_filter)
-
- # Setup worker to send encrypted data chunks to the backup_target
- def compute_progress(new_size, total_backup_size):
- global blocks_backedup
- blocks_backedup += new_size
- if callable(progress_callback):
- this_progress = blocks_backedup / float(total_backup_size)
- progress_callback(int(round(this_progress * 100, 2)))
-
- to_send = Queue(10)
- send_proc = SendWorker(to_send, backup_tmpdir, backup_stdout)
- send_proc.start()
-
- for f in header_files:
- to_send.put(f)
-
- for filename in files_to_backup:
- if BACKUP_DEBUG:
- print "Backing up", filename
-
- backup_tempfile = os.path.join(backup_tmpdir,
- filename["subdir"],
- os.path.basename(filename["path"]))
- if BACKUP_DEBUG:
- print "Using temporary location:", backup_tempfile
-
- # Ensure the temporary directory exists
- if not os.path.isdir(os.path.dirname(backup_tempfile)):
- os.makedirs(os.path.dirname(backup_tempfile))
-
- # The first tar cmd can use any complex feature as we want. Files will
- # be verified before untaring this.
- # Prefix the path in archive with filename["subdir"] to have it
- # verified during untar
- tar_cmdline = (["tar", "-Pc", '--sparse',
- "-f", backup_pipe,
- '-C', os.path.dirname(filename["path"])] +
- (['--dereference'] if filename["subdir"] != "dom0-home/"
- else []) +
- ['--xform', 's:^%s:%s\\0:' % (
- os.path.basename(filename["path"]),
- filename["subdir"]),
- os.path.basename(filename["path"])
- ])
- if compressed:
- tar_cmdline.insert(-1,
- "--use-compress-program=%s" % compression_filter)
-
- if BACKUP_DEBUG:
- print " ".join(tar_cmdline)
-
- # Tips: Popen(bufsize=0)
- # Pipe: tar-sparse | encryptor [| hmac] | tar | backup_target
- # Pipe: tar-sparse [| hmac] | tar | backup_target
- tar_sparse = subprocess.Popen(tar_cmdline, stdin=subprocess.PIPE,
- stderr=(open(os.devnull, 'w')
- if not BACKUP_DEBUG
- else None))
- running_backup_operation.processes_to_kill_on_cancel.append(tar_sparse)
-
- # Wait for compressor (tar) process to finish or for any error of other
- # subprocesses
- i = 0
- run_error = "paused"
- encryptor = None
- if encrypted:
- # Start encrypt
- # If no cipher is provided, the data is forwarded unencrypted !!!
- encryptor = subprocess.Popen(["openssl", "enc",
- "-e", "-" + crypto_algorithm,
- "-pass", "pass:" + passphrase],
- stdin=open(backup_pipe, 'rb'),
- stdout=subprocess.PIPE)
- pipe = encryptor.stdout
- else:
- pipe = open(backup_pipe, 'rb')
- while run_error == "paused":
-
- # Start HMAC
- hmac = subprocess.Popen(["openssl", "dgst",
- "-" + hmac_algorithm, "-hmac", passphrase],
- stdin=subprocess.PIPE,
- stdout=subprocess.PIPE)
-
- # Prepare a first chunk
- chunkfile = backup_tempfile + "." + "%03d" % i
- i += 1
- chunkfile_p = open(chunkfile, 'wb')
-
- common_args = {
- 'backup_target': chunkfile_p,
- 'total_backup_sz': total_backup_sz,
- 'hmac': hmac,
- 'vmproc': vmproc,
- 'addproc': tar_sparse,
- 'progress_callback': compute_progress,
- 'size_limit': 100 * 1024 * 1024,
- }
- run_error = wait_backup_feedback(
- in_stream=pipe, streamproc=encryptor,
- **common_args)
- chunkfile_p.close()
-
- if BACKUP_DEBUG:
- print "Wait_backup_feedback returned:", run_error
-
- if running_backup_operation.canceled:
- try:
- tar_sparse.terminate()
- except:
- pass
- try:
- hmac.terminate()
- except:
- pass
- tar_sparse.wait()
- hmac.wait()
- to_send.put("ERROR")
- send_proc.join()
- shutil.rmtree(backup_tmpdir)
- running_backup_operation = None
- raise BackupCanceledError("Backup canceled")
- if run_error and run_error != "size_limit":
- send_proc.terminate()
- if run_error == "VM" and vmproc:
- raise QubesException(
- "Failed to write the backup, VM output:\n" +
- vmproc.stderr.read(MAX_STDERR_BYTES))
- else:
- raise QubesException("Failed to perform backup: error in " +
- run_error)
-
- # Send the chunk to the backup target
- queue_put_with_check(
- send_proc, vmproc, to_send,
- os.path.relpath(chunkfile, backup_tmpdir))
-
- # Close HMAC
- hmac.stdin.close()
- hmac.wait()
- if BACKUP_DEBUG:
- print "HMAC proc return code:", hmac.poll()
-
- # Write HMAC data next to the chunk file
- hmac_data = hmac.stdout.read()
- if BACKUP_DEBUG:
- print "Writing hmac to", chunkfile + ".hmac"
- hmac_file = open(chunkfile + ".hmac", 'w')
- hmac_file.write(hmac_data)
- hmac_file.flush()
- hmac_file.close()
-
- # Send the HMAC to the backup target
- queue_put_with_check(
- send_proc, vmproc, to_send,
- os.path.relpath(chunkfile, backup_tmpdir) + ".hmac")
-
- if tar_sparse.poll() is None or run_error == "size_limit":
- run_error = "paused"
- else:
- running_backup_operation.processes_to_kill_on_cancel.remove(
- tar_sparse)
- if BACKUP_DEBUG:
- print "Finished tar sparse with exit code", tar_sparse \
- .poll()
- pipe.close()
-
- queue_put_with_check(send_proc, vmproc, to_send, "FINISHED")
- send_proc.join()
- shutil.rmtree(backup_tmpdir)
-
- if running_backup_operation.canceled:
- running_backup_operation = None
- raise BackupCanceledError("Backup canceled")
-
- running_backup_operation = None
-
- if send_proc.exitcode != 0:
- raise QubesException(
- "Failed to send backup: error in the sending process")
-
- if vmproc:
- if BACKUP_DEBUG:
- print "VMProc1 proc return code:", vmproc.poll()
- if tar_sparse is not None:
- print "Sparse1 proc return code:", tar_sparse.poll()
- vmproc.stdin.close()
-
- # Save date of last backup
- qvm_collection = QubesVmCollection()
- qvm_collection.lock_db_for_writing()
- qvm_collection.load()
-
- for vm in qvm_collection.values():
- if vm.backup_content:
- vm.backup_timestamp = datetime.datetime.now()
-
- qvm_collection.save()
- qvm_collection.unlock_db()
-
-
-'''
-' Wait for backup chunk to finish
-' - Monitor all the processes (streamproc, hmac, vmproc, addproc) for errors
-' - Copy stdout of streamproc to backup_target and hmac stdin if available
-' - Compute progress based on total_backup_sz and send progress to
-' progress_callback function
-' - Returns if
-' - one of the monitored processes error out (streamproc, hmac, vmproc,
-' addproc), along with the processe that failed
-' - all of the monitored processes except vmproc finished successfully
-' (vmproc termination is controlled by the python script)
-' - streamproc does not delivers any data anymore (return with the error
-' "")
-' - size_limit is provided and is about to be exceeded
-'''
-
-
-def wait_backup_feedback(progress_callback, in_stream, streamproc,
- backup_target, total_backup_sz, hmac=None, vmproc=None,
- addproc=None,
- size_limit=None):
- buffer_size = 409600
-
- run_error = None
- run_count = 1
- bytes_copied = 0
- while run_count > 0 and run_error is None:
-
- if size_limit and bytes_copied + buffer_size > size_limit:
- return "size_limit"
- buf = in_stream.read(buffer_size)
- progress_callback(len(buf), total_backup_sz)
- bytes_copied += len(buf)
-
- run_count = 0
- if hmac:
- retcode = hmac.poll()
- if retcode is not None:
- if retcode != 0:
- run_error = "hmac"
- else:
- run_count += 1
-
- if addproc:
- retcode = addproc.poll()
- if retcode is not None:
- if retcode != 0:
- run_error = "addproc"
- else:
- run_count += 1
-
- if vmproc:
- retcode = vmproc.poll()
- if retcode is not None:
- if retcode != 0:
- run_error = "VM"
- if BACKUP_DEBUG:
- print vmproc.stdout.read()
- else:
- # VM should run until the end
- pass
-
- if streamproc:
- retcode = streamproc.poll()
- if retcode is not None:
- if retcode != 0:
- run_error = "streamproc"
- break
- elif retcode == 0 and len(buf) <= 0:
- return ""
- run_count += 1
-
- else:
- if len(buf) <= 0:
- return ""
-
- try:
- backup_target.write(buf)
- except IOError as e:
- if e.errno == errno.EPIPE:
- run_error = "target"
- else:
- raise
-
- if hmac:
- hmac.stdin.write(buf)
-
- return run_error
-
-
-def verify_hmac(filename, hmacfile, passphrase, algorithm):
- if BACKUP_DEBUG:
- print "Verifying file " + filename
-
- if hmacfile != filename + ".hmac":
- raise QubesException(
- "ERROR: expected hmac for {}, but got {}".
- format(filename, hmacfile))
-
- hmac_proc = subprocess.Popen(["openssl", "dgst", "-" + algorithm,
- "-hmac", passphrase],
- stdin=open(filename, 'rb'),
- stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- hmac_stdout, hmac_stderr = hmac_proc.communicate()
-
- if len(hmac_stderr) > 0:
- raise QubesException(
- "ERROR: verify file {0}: {1}".format(filename, hmac_stderr))
- else:
- if BACKUP_DEBUG:
- print "Loading hmac for file " + filename
- hmac = load_hmac(open(hmacfile, 'r').read())
-
- if len(hmac) > 0 and load_hmac(hmac_stdout) == hmac:
- os.unlink(hmacfile)
- if BACKUP_DEBUG:
- print "File verification OK -> Sending file " + filename
- return True
- else:
- raise QubesException(
- "ERROR: invalid hmac for file {0}: {1}. "
- "Is the passphrase correct?".
- format(filename, load_hmac(hmac_stdout)))
- # Not reachable
- return False
-
-
-class ExtractWorker2(Process):
- def __init__(self, queue, base_dir, passphrase, encrypted, total_size,
- print_callback, error_callback, progress_callback, vmproc=None,
- compressed=False, crypto_algorithm=DEFAULT_CRYPTO_ALGORITHM,
- verify_only=False):
- super(ExtractWorker2, self).__init__()
- self.queue = queue
- self.base_dir = base_dir
- self.passphrase = passphrase
- self.encrypted = encrypted
- self.compressed = compressed
- self.crypto_algorithm = crypto_algorithm
- self.verify_only = verify_only
- self.total_size = total_size
- self.blocks_backedup = 0
- self.tar2_process = None
- self.tar2_current_file = None
- self.decompressor_process = None
- self.decryptor_process = None
-
- self.print_callback = print_callback
- self.error_callback = error_callback
- self.progress_callback = progress_callback
-
- self.vmproc = vmproc
-
- self.restore_pipe = os.path.join(self.base_dir, "restore_pipe")
- if BACKUP_DEBUG:
- print "Creating pipe in:", self.restore_pipe
- os.mkfifo(self.restore_pipe)
-
- self.stderr_encoding = sys.stderr.encoding or 'utf-8'
-
- def compute_progress(self, new_size, _):
- if self.progress_callback:
- self.blocks_backedup += new_size
- progress = self.blocks_backedup / float(self.total_size)
- progress = int(round(progress * 100, 2))
- self.progress_callback(progress)
-
- def collect_tar_output(self):
- if not self.tar2_process.stderr:
- return
-
- if self.tar2_process.poll() is None:
- try:
- new_lines = self.tar2_process.stderr \
- .read(MAX_STDERR_BYTES).splitlines()
- except IOError as e:
- if e.errno == errno.EAGAIN:
- return
- else:
- raise
- else:
- new_lines = self.tar2_process.stderr.readlines()
-
- new_lines = map(lambda x: x.decode(self.stderr_encoding), new_lines)
-
- if not BACKUP_DEBUG:
- msg_re = re.compile(r".*#[0-9].*restore_pipe")
- new_lines = filter(lambda x: not msg_re.match(x), new_lines)
-
- self.tar2_stderr += new_lines
-
- def run(self):
- try:
- self.__run__()
- except Exception as e:
- exc_type, exc_value, exc_traceback = sys.exc_info()
- # Cleanup children
- for process in [self.decompressor_process,
- self.decryptor_process,
- self.tar2_process]:
- if process:
- # FIXME: kill()?
- try:
- process.terminate()
- except OSError:
- pass
- process.wait()
- self.error_callback("ERROR: " + unicode(e))
- raise e, None, exc_traceback
-
- def __run__(self):
- if BACKUP_DEBUG and callable(self.print_callback):
- self.print_callback("Started sending thread")
- self.print_callback("Moving to dir " + self.base_dir)
- os.chdir(self.base_dir)
-
- filename = None
-
- for filename in iter(self.queue.get, None):
- if filename == "FINISHED" or filename == "ERROR":
- break
-
- if BACKUP_DEBUG and callable(self.print_callback):
- self.print_callback("Extracting file " + filename)
-
- if filename.endswith('.000'):
- # next file
- if self.tar2_process is not None:
- if self.tar2_process.wait() != 0:
- self.collect_tar_output()
- self.error_callback(
- "ERROR: unable to extract files for {0}, tar "
- "output:\n {1}".
- format(self.tar2_current_file,
- "\n ".join(self.tar2_stderr)))
- else:
- # Finished extracting the tar file
- self.tar2_process = None
- self.tar2_current_file = None
-
- tar2_cmdline = ['tar',
- '-%sMk%sf' % ("t" if self.verify_only else "x",
- "v" if BACKUP_DEBUG else ""),
- self.restore_pipe,
- os.path.relpath(filename.rstrip('.000'))]
- if BACKUP_DEBUG and callable(self.print_callback):
- self.print_callback("Running command " +
- unicode(tar2_cmdline))
- self.tar2_process = subprocess.Popen(tar2_cmdline,
- stdin=subprocess.PIPE,
- stderr=subprocess.PIPE)
- fcntl.fcntl(self.tar2_process.stderr.fileno(), fcntl.F_SETFL,
- fcntl.fcntl(self.tar2_process.stderr.fileno(),
- fcntl.F_GETFL) | os.O_NONBLOCK)
- self.tar2_stderr = []
- elif not self.tar2_process:
- # Extracting of the current archive failed, skip to the next
- # archive
- if not BACKUP_DEBUG:
- os.remove(filename)
- continue
- else:
- self.collect_tar_output()
- if BACKUP_DEBUG and callable(self.print_callback):
- self.print_callback("Releasing next chunck")
- self.tar2_process.stdin.write("\n")
- self.tar2_process.stdin.flush()
- self.tar2_current_file = filename
-
- pipe = open(self.restore_pipe, 'wb')
- common_args = {
- 'backup_target': pipe,
- 'total_backup_sz': self.total_size,
- 'hmac': None,
- 'vmproc': self.vmproc,
- 'addproc': self.tar2_process
- }
- if self.encrypted:
- # Start decrypt
- self.decryptor_process = subprocess.Popen(
- ["openssl", "enc",
- "-d",
- "-" + self.crypto_algorithm,
- "-pass",
- "pass:" + self.passphrase] +
- (["-z"] if self.compressed else []),
- stdin=open(filename, 'rb'),
- stdout=subprocess.PIPE)
-
- run_error = wait_backup_feedback(
- progress_callback=self.compute_progress,
- in_stream=self.decryptor_process.stdout,
- streamproc=self.decryptor_process,
- **common_args)
- elif self.compressed:
- self.decompressor_process = subprocess.Popen(
- ["gzip", "-d"],
- stdin=open(filename, 'rb'),
- stdout=subprocess.PIPE)
-
- run_error = wait_backup_feedback(
- progress_callback=self.compute_progress,
- in_stream=self.decompressor_process.stdout,
- streamproc=self.decompressor_process,
- **common_args)
- else:
- run_error = wait_backup_feedback(
- progress_callback=self.compute_progress,
- in_stream=open(filename, "rb"), streamproc=None,
- **common_args)
-
- try:
- pipe.close()
- except IOError as e:
- if e.errno == errno.EPIPE:
- if BACKUP_DEBUG:
- self.error_callback(
- "Got EPIPE while closing pipe to "
- "the inner tar process")
- # ignore the error
- else:
- raise
- if len(run_error):
- if run_error == "target":
- self.collect_tar_output()
- details = "\n".join(self.tar2_stderr)
- else:
- details = "%s failed" % run_error
- self.tar2_process.terminate()
- self.tar2_process.wait()
- self.tar2_process = None
- self.error_callback("Error while processing '%s': %s " %
- (self.tar2_current_file, details))
-
- # Delete the file as we don't need it anymore
- if BACKUP_DEBUG and callable(self.print_callback):
- self.print_callback("Removing file " + filename)
- os.remove(filename)
-
- os.unlink(self.restore_pipe)
-
- if self.tar2_process is not None:
- if filename == "ERROR":
- self.tar2_process.terminate()
- self.tar2_process.wait()
- elif self.tar2_process.wait() != 0:
- self.collect_tar_output()
- raise QubesException(
- "unable to extract files for {0}.{1} Tar command "
- "output: %s".
- format(self.tar2_current_file,
- (" Perhaps the backup is encrypted?"
- if not self.encrypted else "",
- "\n".join(self.tar2_stderr))))
- else:
- # Finished extracting the tar file
- self.tar2_process = None
-
- if BACKUP_DEBUG and callable(self.print_callback):
- self.print_callback("Finished extracting thread")
-
-
-class ExtractWorker3(ExtractWorker2):
- def __init__(self, queue, base_dir, passphrase, encrypted, total_size,
- print_callback, error_callback, progress_callback, vmproc=None,
- compressed=False, crypto_algorithm=DEFAULT_CRYPTO_ALGORITHM,
- compression_filter=None, verify_only=False):
- super(ExtractWorker3, self).__init__(queue, base_dir, passphrase,
- encrypted, total_size,
- print_callback, error_callback,
- progress_callback, vmproc,
- compressed, crypto_algorithm,
- verify_only)
- self.compression_filter = compression_filter
- os.unlink(self.restore_pipe)
-
- def __run__(self):
- if BACKUP_DEBUG and callable(self.print_callback):
- self.print_callback("Started sending thread")
- self.print_callback("Moving to dir " + self.base_dir)
- os.chdir(self.base_dir)
-
- filename = None
-
- input_pipe = None
- for filename in iter(self.queue.get, None):
- if filename == "FINISHED" or filename == "ERROR":
- break
-
- if BACKUP_DEBUG and callable(self.print_callback):
- self.print_callback("Extracting file " + filename)
-
- if filename.endswith('.000'):
- # next file
- if self.tar2_process is not None:
- input_pipe.close()
- if self.tar2_process.wait() != 0:
- self.collect_tar_output()
- self.error_callback(
- "ERROR: unable to extract files for {0}, tar "
- "output:\n {1}".
- format(self.tar2_current_file,
- "\n ".join(self.tar2_stderr)))
- else:
- # Finished extracting the tar file
- self.tar2_process = None
- self.tar2_current_file = None
-
- tar2_cmdline = ['tar',
- '-%sk%s' % ("t" if self.verify_only else "x",
- "v" if BACKUP_DEBUG else ""),
- os.path.relpath(filename.rstrip('.000'))]
- if self.compressed:
- if self.compression_filter:
- tar2_cmdline.insert(-1,
- "--use-compress-program=%s" %
- self.compression_filter)
- else:
- tar2_cmdline.insert(-1, "--use-compress-program=%s" %
- DEFAULT_COMPRESSION_FILTER)
-
- if BACKUP_DEBUG and callable(self.print_callback):
- self.print_callback("Running command " +
- unicode(tar2_cmdline))
- if self.encrypted:
- # Start decrypt
- self.decryptor_process = subprocess.Popen(
- ["openssl", "enc",
- "-d",
- "-" + self.crypto_algorithm,
- "-pass",
- "pass:" + self.passphrase],
- stdin=subprocess.PIPE,
- stdout=subprocess.PIPE)
-
- self.tar2_process = subprocess.Popen(
- tar2_cmdline,
- stdin=self.decryptor_process.stdout,
- stderr=subprocess.PIPE)
- input_pipe = self.decryptor_process.stdin
- else:
- self.tar2_process = subprocess.Popen(
- tar2_cmdline,
- stdin=subprocess.PIPE,
- stderr=subprocess.PIPE)
- input_pipe = self.tar2_process.stdin
-
- fcntl.fcntl(self.tar2_process.stderr.fileno(), fcntl.F_SETFL,
- fcntl.fcntl(self.tar2_process.stderr.fileno(),
- fcntl.F_GETFL) | os.O_NONBLOCK)
- self.tar2_stderr = []
- elif not self.tar2_process:
- # Extracting of the current archive failed, skip to the next
- # archive
- if not BACKUP_DEBUG:
- os.remove(filename)
- continue
- else:
- if BACKUP_DEBUG and callable(self.print_callback):
- self.print_callback("Releasing next chunck")
- self.tar2_current_file = filename
-
- common_args = {
- 'backup_target': input_pipe,
- 'total_backup_sz': self.total_size,
- 'hmac': None,
- 'vmproc': self.vmproc,
- 'addproc': self.tar2_process
- }
-
- run_error = wait_backup_feedback(
- progress_callback=self.compute_progress,
- in_stream=open(filename, "rb"), streamproc=None,
- **common_args)
-
- if len(run_error):
- if run_error == "target":
- self.collect_tar_output()
- details = "\n".join(self.tar2_stderr)
- else:
- details = "%s failed" % run_error
- if self.decryptor_process:
- self.decryptor_process.terminate()
- self.decryptor_process.wait()
- self.decryptor_process = None
- self.tar2_process.terminate()
- self.tar2_process.wait()
- self.tar2_process = None
- self.error_callback("Error while processing '%s': %s " %
- (self.tar2_current_file, details))
-
- # Delete the file as we don't need it anymore
- if BACKUP_DEBUG and callable(self.print_callback):
- self.print_callback("Removing file " + filename)
- os.remove(filename)
-
- if self.tar2_process is not None:
- input_pipe.close()
- if filename == "ERROR":
- if self.decryptor_process:
- self.decryptor_process.terminate()
- self.decryptor_process.wait()
- self.decryptor_process = None
- self.tar2_process.terminate()
- self.tar2_process.wait()
- elif self.tar2_process.wait() != 0:
- self.collect_tar_output()
- raise QubesException(
- "unable to extract files for {0}.{1} Tar command "
- "output: %s".
- format(self.tar2_current_file,
- (" Perhaps the backup is encrypted?"
- if not self.encrypted else "",
- "\n".join(self.tar2_stderr))))
- else:
- # Finished extracting the tar file
- self.tar2_process = None
-
- if BACKUP_DEBUG and callable(self.print_callback):
- self.print_callback("Finished extracting thread")
-
-
-def get_supported_hmac_algo(hmac_algorithm):
- # Start with provided default
- if hmac_algorithm:
- yield hmac_algorithm
- proc = subprocess.Popen(['openssl', 'list-message-digest-algorithms'],
- stdout=subprocess.PIPE)
- for algo in proc.stdout.readlines():
- if '=>' in algo:
- continue
- yield algo.strip()
- proc.wait()
-
-
-def parse_backup_header(filename):
- header_data = {}
- with open(filename, 'r') as f:
- for line in f.readlines():
- if line.count('=') != 1:
- raise QubesException("Invalid backup header (line %s)" % line)
- (key, value) = line.strip().split('=')
- if not any([key == getattr(BackupHeader, attr) for attr in dir(
- BackupHeader)]):
- # Ignoring unknown option
- continue
- if key in BackupHeader.bool_options:
- value = value.lower() in ["1", "true", "yes"]
- elif key in BackupHeader.int_options:
- value = int(value)
- header_data[key] = value
- return header_data
-
-
-def restore_vm_dirs(backup_source, restore_tmpdir, passphrase, vms_dirs, vms,
- vms_size, print_callback=None, error_callback=None,
- progress_callback=None, encrypted=False, appvm=None,
- compressed=False, hmac_algorithm=DEFAULT_HMAC_ALGORITHM,
- crypto_algorithm=DEFAULT_CRYPTO_ALGORITHM,
- verify_only=False,
- format_version=CURRENT_BACKUP_FORMAT_VERSION,
- compression_filter=None):
- global running_backup_operation
-
- if callable(print_callback):
- if BACKUP_DEBUG:
- print_callback("Working in temporary dir:" + restore_tmpdir)
- print_callback(
- "Extracting data: " + size_to_human(vms_size) + " to restore")
-
- passphrase = passphrase.encode('utf-8')
- header_data = None
- vmproc = None
- if appvm is not None:
- # Prepare the backup target (Qubes service call)
- backup_target = "QUBESRPC qubes.Restore dom0"
-
- # If APPVM, STDOUT is a PIPE
- vmproc = appvm.run(command=backup_target, passio_popen=True,
- passio_stderr=True)
- vmproc.stdin.write(
- backup_source.replace("\r", "").replace("\n", "") + "\n")
-
- # Send to tar2qfile the VMs that should be extracted
- vmproc.stdin.write(" ".join(vms_dirs) + "\n")
- if running_backup_operation:
- running_backup_operation.processes_to_kill_on_cancel.append(vmproc)
-
- backup_stdin = vmproc.stdout
- tar1_command = ['/usr/libexec/qubes/qfile-dom0-unpacker',
- str(os.getuid()), restore_tmpdir, '-v']
- else:
- backup_stdin = open(backup_source, 'rb')
-
- tar1_command = ['tar',
- '-ixvf', backup_source,
- '-C', restore_tmpdir] + vms_dirs
-
- tar1_env = os.environ.copy()
- # TODO: add some safety margin?
- tar1_env['UPDATES_MAX_BYTES'] = str(vms_size)
- # Restoring only header
- if vms_dirs and vms_dirs[0] == HEADER_FILENAME:
- # backup-header, backup-header.hmac, qubes-xml.000, qubes-xml.000.hmac
- tar1_env['UPDATES_MAX_FILES'] = '4'
- else:
- # Currently each VM consists of at most 7 archives (count
- # file_to_backup calls in backup_prepare()), but add some safety
- # margin for further extensions. Each archive is divided into 100MB
- # chunks. Additionally each file have own hmac file. So assume upper
- # limit as 2*(10*COUNT_OF_VMS+TOTAL_SIZE/100MB)
- tar1_env['UPDATES_MAX_FILES'] = str(2 * (10 * len(vms_dirs) +
- int(vms_size /
- (100 * 1024 * 1024))))
- if BACKUP_DEBUG and callable(print_callback):
- print_callback("Run command" + unicode(tar1_command))
- command = subprocess.Popen(
- tar1_command,
- stdin=backup_stdin,
- stdout=vmproc.stdin if vmproc else subprocess.PIPE,
- stderr=subprocess.PIPE,
- env=tar1_env)
- if running_backup_operation:
- running_backup_operation.processes_to_kill_on_cancel.append(command)
-
- # qfile-dom0-unpacker output filelist on stderr (and have stdout connected
- # to the VM), while tar output filelist on stdout
- if appvm:
- filelist_pipe = command.stderr
- # let qfile-dom0-unpacker hold the only open FD to the write end of
- # pipe, otherwise qrexec-client will not receive EOF when
- # qfile-dom0-unpacker terminates
- vmproc.stdin.close()
- else:
- filelist_pipe = command.stdout
-
- expect_tar_error = False
-
- to_extract = Queue()
- nextfile = None
-
- # If want to analyze backup header, do it now
- if vms_dirs and vms_dirs[0] == HEADER_FILENAME:
- filename = filelist_pipe.readline().strip()
- hmacfile = filelist_pipe.readline().strip()
- if not appvm:
- nextfile = filelist_pipe.readline().strip()
-
- if BACKUP_DEBUG and callable(print_callback):
- print_callback("Got backup header and hmac: %s, %s" % (filename,
- hmacfile))
-
- if not filename or filename == "EOF" or \
- not hmacfile or hmacfile == "EOF":
- if appvm:
- vmproc.wait()
- proc_error_msg = vmproc.stderr.read(MAX_STDERR_BYTES)
- else:
- command.wait()
- proc_error_msg = command.stderr.read(MAX_STDERR_BYTES)
- raise QubesException("Premature end of archive while receiving "
- "backup header. Process output:\n" +
- proc_error_msg)
- filename = os.path.join(restore_tmpdir, filename)
- hmacfile = os.path.join(restore_tmpdir, hmacfile)
- file_ok = False
- for hmac_algo in get_supported_hmac_algo(hmac_algorithm):
- try:
- if verify_hmac(filename, hmacfile, passphrase, hmac_algo):
- file_ok = True
- hmac_algorithm = hmac_algo
- break
- except QubesException:
- # Ignore exception here, try the next algo
- pass
- if not file_ok:
- raise QubesException("Corrupted backup header (hmac verification "
- "failed). Is the password correct?")
- if os.path.basename(filename) == HEADER_FILENAME:
- header_data = parse_backup_header(filename)
- if BackupHeader.version in header_data:
- format_version = header_data[BackupHeader.version]
- if BackupHeader.crypto_algorithm in header_data:
- crypto_algorithm = header_data[BackupHeader.crypto_algorithm]
- if BackupHeader.hmac_algorithm in header_data:
- hmac_algorithm = header_data[BackupHeader.hmac_algorithm]
- if BackupHeader.compressed in header_data:
- compressed = header_data[BackupHeader.compressed]
- if BackupHeader.encrypted in header_data:
- encrypted = header_data[BackupHeader.encrypted]
- if BackupHeader.compression_filter in header_data:
- compression_filter = header_data[
- BackupHeader.compression_filter]
- os.unlink(filename)
- else:
- # if no header found, create one with guessed HMAC algo
- header_data = {BackupHeader.hmac_algorithm: hmac_algorithm}
- # If this isn't backup header, pass it to ExtractWorker
- to_extract.put(filename)
- # when tar do not find expected file in archive, it exit with
- # code 2. This will happen because we've requested backup-header
- # file, but the archive do not contain it. Ignore this particular
- # error.
- if not appvm:
- expect_tar_error = True
-
- # Setup worker to extract encrypted data chunks to the restore dirs
- # Create the process here to pass it options extracted from backup header
- extractor_params = {
- 'queue': to_extract,
- 'base_dir': restore_tmpdir,
- 'passphrase': passphrase,
- 'encrypted': encrypted,
- 'compressed': compressed,
- 'crypto_algorithm': crypto_algorithm,
- 'verify_only': verify_only,
- 'total_size': vms_size,
- 'print_callback': print_callback,
- 'error_callback': error_callback,
- 'progress_callback': progress_callback,
- }
- if format_version == 2:
- extract_proc = ExtractWorker2(**extractor_params)
- elif format_version == 3:
- extractor_params['compression_filter'] = compression_filter
- extract_proc = ExtractWorker3(**extractor_params)
- else:
- raise NotImplemented(
- "Backup format version %d not supported" % format_version)
- extract_proc.start()
-
- try:
- filename = None
- while True:
- if running_backup_operation and running_backup_operation.canceled:
- break
- if not extract_proc.is_alive():
- command.terminate()
- command.wait()
- expect_tar_error = True
- if vmproc:
- vmproc.terminate()
- vmproc.wait()
- vmproc = None
- break
- if nextfile is not None:
- filename = nextfile
- else:
- filename = filelist_pipe.readline().strip()
-
- if BACKUP_DEBUG and callable(print_callback):
- print_callback("Getting new file:" + filename)
-
- if not filename or filename == "EOF":
- break
-
- hmacfile = filelist_pipe.readline().strip()
-
- if running_backup_operation and running_backup_operation.canceled:
- break
- # if reading archive directly with tar, wait for next filename -
- # tar prints filename before processing it, so wait for
- # the next one to be sure that whole file was extracted
- if not appvm:
- nextfile = filelist_pipe.readline().strip()
-
- if BACKUP_DEBUG and callable(print_callback):
- print_callback("Getting hmac:" + hmacfile)
- if not hmacfile or hmacfile == "EOF":
- # Premature end of archive, either of tar1_command or
- # vmproc exited with error
- break
-
- if not any(map(lambda x: filename.startswith(x), vms_dirs)):
- if BACKUP_DEBUG and callable(print_callback):
- print_callback("Ignoring VM not selected for restore")
- os.unlink(os.path.join(restore_tmpdir, filename))
- os.unlink(os.path.join(restore_tmpdir, hmacfile))
- continue
-
- if verify_hmac(os.path.join(restore_tmpdir, filename),
- os.path.join(restore_tmpdir, hmacfile),
- passphrase, hmac_algorithm):
- to_extract.put(os.path.join(restore_tmpdir, filename))
-
- if running_backup_operation and running_backup_operation.canceled:
- raise BackupCanceledError("Restore canceled",
- tmpdir=restore_tmpdir)
-
- if command.wait() != 0 and not expect_tar_error:
- raise QubesException(
- "unable to read the qubes backup file {0} ({1}). "
- "Is it really a backup?".format(backup_source, command.wait()))
- if vmproc:
- if vmproc.wait() != 0:
- raise QubesException(
- "unable to read the qubes backup {0} "
- "because of a VM error: {1}".format(
- backup_source, vmproc.stderr.read(MAX_STDERR_BYTES)))
-
- if filename and filename != "EOF":
- raise QubesException(
- "Premature end of archive, the last file was %s" % filename)
- except:
- to_extract.put("ERROR")
- extract_proc.join()
- raise
- else:
- to_extract.put("FINISHED")
-
- if BACKUP_DEBUG and callable(print_callback):
- print_callback("Waiting for the extraction process to finish...")
- extract_proc.join()
- if BACKUP_DEBUG and callable(print_callback):
- print_callback("Extraction process finished with code:" +
- str(extract_proc.exitcode))
- if extract_proc.exitcode != 0:
- raise QubesException(
- "unable to extract the qubes backup. "
- "Check extracting process errors.")
-
- return header_data
-
-
-def backup_restore_set_defaults(options):
- if 'use-default-netvm' not in options:
- options['use-default-netvm'] = False
- if 'use-none-netvm' not in options:
- options['use-none-netvm'] = False
- if 'use-default-template' not in options:
- options['use-default-template'] = False
- if 'dom0-home' not in options:
- options['dom0-home'] = True
- if 'replace-template' not in options:
- options['replace-template'] = []
- if 'ignore-username-mismatch' not in options:
- options['ignore-username-mismatch'] = False
- if 'verify-only' not in options:
- options['verify-only'] = False
- if 'rename-conflicting' not in options:
- options['rename-conflicting'] = False
-
- return options
-
-
-def load_hmac(hmac):
- hmac = hmac.strip().split("=")
- if len(hmac) > 1:
- hmac = hmac[1].strip()
- else:
- raise QubesException("ERROR: invalid hmac file content")
-
- return hmac
-
-
-def backup_detect_format_version(backup_location):
- if os.path.exists(os.path.join(backup_location, 'qubes.xml')):
- return 1
- else:
- # this could mean also 3, but not distinguishable until backup header
- # is read
- return 2
-
-
-def backup_restore_header(source, passphrase,
- print_callback=print_stdout,
- error_callback=print_stderr,
- encrypted=False, appvm=None, compressed=False,
- format_version=None,
- hmac_algorithm=DEFAULT_HMAC_ALGORITHM,
- crypto_algorithm=DEFAULT_CRYPTO_ALGORITHM):
- global running_backup_operation
- running_backup_operation = None
-
- restore_tmpdir = tempfile.mkdtemp(prefix="/var/tmp/restore_")
-
- if format_version is None:
- format_version = backup_detect_format_version(source)
-
- if format_version == 1:
- return restore_tmpdir, os.path.join(source, 'qubes.xml'), None
-
- # tar2qfile matches only beginnings, while tar full path
- if appvm:
- extract_filter = [HEADER_FILENAME, 'qubes.xml.000']
- else:
- extract_filter = [HEADER_FILENAME, HEADER_FILENAME + '.hmac',
- 'qubes.xml.000', 'qubes.xml.000.hmac']
-
- header_data = restore_vm_dirs(source,
- restore_tmpdir,
- passphrase=passphrase,
- vms_dirs=extract_filter,
- vms=None,
- vms_size=HEADER_QUBES_XML_MAX_SIZE,
- format_version=format_version,
- hmac_algorithm=hmac_algorithm,
- crypto_algorithm=crypto_algorithm,
- print_callback=print_callback,
- error_callback=error_callback,
- progress_callback=None,
- encrypted=encrypted,
- compressed=compressed,
- appvm=appvm)
-
- return (restore_tmpdir, os.path.join(restore_tmpdir, "qubes.xml"),
- header_data)
-
-def generate_new_name_for_conflicting_vm(orig_name, host_collection,
- restore_info):
- number = 1
- if len(orig_name) > 29:
- orig_name = orig_name[0:29]
- new_name = orig_name
- while (new_name in restore_info.keys() or
- new_name in map(lambda x: x.get('rename_to', None),
- restore_info.values()) or
- host_collection.get_vm_by_name(new_name)):
- new_name = str('{}{}'.format(orig_name, number))
- number += 1
- if number == 100:
- # give up
- return None
- return new_name
-
-def restore_info_verify(restore_info, host_collection):
- options = restore_info['$OPTIONS$']
- for vm in restore_info.keys():
- if vm in ['$OPTIONS$', 'dom0']:
- continue
-
- vm_info = restore_info[vm]
-
- vm_info.pop('excluded', None)
- if 'exclude' in options.keys():
- if vm in options['exclude']:
- vm_info['excluded'] = True
-
- vm_info.pop('already-exists', None)
- if not options['verify-only'] and \
- host_collection.get_vm_by_name(vm) is not None:
- if options['rename-conflicting']:
- new_name = generate_new_name_for_conflicting_vm(
- vm, host_collection, restore_info
- )
- if new_name is not None:
- vm_info['rename-to'] = new_name
- else:
- vm_info['already-exists'] = True
- else:
- vm_info['already-exists'] = True
-
- # check template
- vm_info.pop('missing-template', None)
- if vm_info['template']:
- template_name = vm_info['template']
- host_template = host_collection.get_vm_by_name(template_name)
- if not host_template or not host_template.is_template():
- # Maybe the (custom) template is in the backup?
- if not (template_name in restore_info.keys() and
- restore_info[template_name]['vm'].is_template()):
- if options['use-default-template']:
- if 'orig-template' not in vm_info.keys():
- vm_info['orig-template'] = template_name
- vm_info['template'] = host_collection \
- .get_default_template().name
- else:
- vm_info['missing-template'] = True
-
- # check netvm
- vm_info.pop('missing-netvm', None)
- if vm_info['vm'].uses_default_netvm:
- default_netvm = host_collection.get_default_netvm()
- vm_info['netvm'] = default_netvm.name if \
- default_netvm else None
- elif vm_info['netvm']:
- netvm_name = vm_info['netvm']
-
- netvm_on_host = host_collection.get_vm_by_name(netvm_name)
-
- # No netvm on the host?
- if not ((netvm_on_host is not None) and netvm_on_host.is_netvm()):
-
- # Maybe the (custom) netvm is in the backup?
- if not (netvm_name in restore_info.keys() and
- restore_info[netvm_name]['vm'].is_netvm()):
- if options['use-default-netvm']:
- default_netvm = host_collection.get_default_netvm()
- vm_info['netvm'] = default_netvm.name if \
- default_netvm else None
- vm_info['vm'].uses_default_netvm = True
- elif options['use-none-netvm']:
- vm_info['netvm'] = None
- else:
- vm_info['missing-netvm'] = True
-
- vm_info['good-to-go'] = not any([(prop in vm_info.keys()) for
- prop in ['missing-netvm',
- 'missing-template',
- 'already-exists',
- 'excluded']])
-
- # update references to renamed VMs:
- for vm in restore_info.keys():
- if vm in ['$OPTIONS$', 'dom0']:
- continue
- vm_info = restore_info[vm]
- template_name = vm_info['template']
- if (template_name in restore_info and
- restore_info[template_name]['good-to-go'] and
- 'rename-to' in restore_info[template_name]):
- vm_info['template'] = restore_info[template_name]['rename-to']
- netvm_name = vm_info['netvm']
- if (netvm_name in restore_info and
- restore_info[netvm_name]['good-to-go'] and
- 'rename-to' in restore_info[netvm_name]):
- vm_info['netvm'] = restore_info[netvm_name]['rename-to']
-
- return restore_info
-
-
-def backup_restore_prepare(backup_location, passphrase, options=None,
- host_collection=None, encrypted=False, appvm=None,
- compressed=False, print_callback=print_stdout,
- error_callback=print_stderr,
- format_version=None,
- hmac_algorithm=DEFAULT_HMAC_ALGORITHM,
- crypto_algorithm=DEFAULT_CRYPTO_ALGORITHM):
- if options is None:
- options = {}
- # Defaults
- backup_restore_set_defaults(options)
- # Options introduced in backup format 3+, which always have a header,
- # so no need for fallback in function parameter
- compression_filter = DEFAULT_COMPRESSION_FILTER
-
- # Private functions begin
- def is_vm_included_in_backup_v1(backup_dir, check_vm):
- if check_vm.qid == 0:
- return os.path.exists(os.path.join(backup_dir, 'dom0-home'))
-
- # DisposableVM
- if check_vm.dir_path is None:
- return False
-
- backup_vm_dir_path = check_vm.dir_path.replace(
- system_path["qubes_base_dir"], backup_dir)
-
- if os.path.exists(backup_vm_dir_path):
- return True
- else:
- return False
-
- def is_vm_included_in_backup_v2(_, check_vm):
- if check_vm.backup_content:
- return True
- else:
- return False
-
- def find_template_name(template, replaces):
- rx_replace = re.compile("(.*):(.*)")
- for r in replaces:
- m = rx_replace.match(r)
- if m.group(1) == template:
- return m.group(2)
-
- return template
-
- # Private functions end
-
- # Format versions:
- # 1 - Qubes R1, Qubes R2 beta1, beta2
- # 2 - Qubes R2 beta3+
-
- if format_version is None:
- format_version = backup_detect_format_version(backup_location)
-
- if format_version == 1:
- is_vm_included_in_backup = is_vm_included_in_backup_v1
- elif format_version in [2, 3]:
- is_vm_included_in_backup = is_vm_included_in_backup_v2
- if not appvm:
- if not os.path.isfile(backup_location):
- raise QubesException("Invalid backup location (not a file or "
- "directory with qubes.xml)"
- ": %s" % unicode(backup_location))
- else:
- raise QubesException(
- "Unknown backup format version: %s" % str(format_version))
-
- (restore_tmpdir, qubes_xml, header_data) = backup_restore_header(
- backup_location,
- passphrase,
- encrypted=encrypted,
- appvm=appvm,
- compressed=compressed,
- hmac_algorithm=hmac_algorithm,
- crypto_algorithm=crypto_algorithm,
- print_callback=print_callback,
- error_callback=error_callback,
- format_version=format_version)
-
- if header_data:
- if BackupHeader.version in header_data:
- format_version = header_data[BackupHeader.version]
- if BackupHeader.crypto_algorithm in header_data:
- crypto_algorithm = header_data[BackupHeader.crypto_algorithm]
- if BackupHeader.hmac_algorithm in header_data:
- hmac_algorithm = header_data[BackupHeader.hmac_algorithm]
- if BackupHeader.compressed in header_data:
- compressed = header_data[BackupHeader.compressed]
- if BackupHeader.encrypted in header_data:
- encrypted = header_data[BackupHeader.encrypted]
- if BackupHeader.compression_filter in header_data:
- compression_filter = header_data[BackupHeader.compression_filter]
-
- if BACKUP_DEBUG:
- print "Loading file", qubes_xml
- backup_collection = QubesVmCollection(store_filename=qubes_xml)
- backup_collection.lock_db_for_reading()
- backup_collection.load()
-
- if host_collection is None:
- host_collection = QubesVmCollection()
- host_collection.lock_db_for_reading()
- host_collection.load()
- host_collection.unlock_db()
-
- backup_vms_list = [vm for vm in backup_collection.values()]
- vms_to_restore = {}
-
- # ... and the actual data
- for vm in backup_vms_list:
- if vm.qid == 0:
- # Handle dom0 as special case later
- continue
- if is_vm_included_in_backup(backup_location, vm):
- if BACKUP_DEBUG:
- print vm.name, "is included in backup"
-
- vms_to_restore[vm.name] = {}
- vms_to_restore[vm.name]['vm'] = vm
-
- if vm.template is None:
- vms_to_restore[vm.name]['template'] = None
- else:
- templatevm_name = find_template_name(vm.template.name, options[
- 'replace-template'])
- vms_to_restore[vm.name]['template'] = templatevm_name
-
- if vm.netvm is None:
- vms_to_restore[vm.name]['netvm'] = None
- else:
- netvm_name = vm.netvm.name
- vms_to_restore[vm.name]['netvm'] = netvm_name
- # Set to None to not confuse QubesVm object from backup
- # collection with host collection (further in clone_attrs). Set
- # directly _netvm to suppress setter action, especially
- # modifying firewall
- vm._netvm = None
-
- # Store restore parameters
- options['location'] = backup_location
- options['restore_tmpdir'] = restore_tmpdir
- options['passphrase'] = passphrase
- options['encrypted'] = encrypted
- options['compressed'] = compressed
- options['compression_filter'] = compression_filter
- options['hmac_algorithm'] = hmac_algorithm
- options['crypto_algorithm'] = crypto_algorithm
- options['appvm'] = appvm
- options['format_version'] = format_version
- vms_to_restore['$OPTIONS$'] = options
-
- vms_to_restore = restore_info_verify(vms_to_restore, host_collection)
-
- # ...and dom0 home
- if options['dom0-home'] and \
- is_vm_included_in_backup(backup_location, backup_collection[0]):
- vm = backup_collection[0]
- vms_to_restore['dom0'] = {}
- if format_version == 1:
- vms_to_restore['dom0']['subdir'] = \
- os.listdir(os.path.join(backup_location, 'dom0-home'))[0]
- vms_to_restore['dom0']['size'] = 0 # unknown
- else:
- vms_to_restore['dom0']['subdir'] = vm.backup_path
- vms_to_restore['dom0']['size'] = vm.backup_size
- local_user = grp.getgrnam('qubes').gr_mem[0]
-
- dom0_home = vms_to_restore['dom0']['subdir']
-
- vms_to_restore['dom0']['username'] = os.path.basename(dom0_home)
- if vms_to_restore['dom0']['username'] != local_user:
- vms_to_restore['dom0']['username-mismatch'] = True
- if options['ignore-username-mismatch']:
- vms_to_restore['dom0']['ignore-username-mismatch'] = True
- else:
- vms_to_restore['dom0']['good-to-go'] = False
-
- if 'good-to-go' not in vms_to_restore['dom0']:
- vms_to_restore['dom0']['good-to-go'] = True
-
- # Not needed - all the data stored in vms_to_restore
- if format_version >= 2:
- os.unlink(qubes_xml)
- return vms_to_restore
-
-
-def backup_restore_print_summary(restore_info, print_callback=print_stdout):
- fields = {
- "qid": {"func": "vm.qid"},
-
- "name": {"func": "('[' if vm.is_template() else '')\
- + ('{' if vm.is_netvm() else '')\
- + vm.name \
- + (']' if vm.is_template() else '')\
- + ('}' if vm.is_netvm() else '')"},
-
- "type": {"func": "'Tpl' if vm.is_template() else \
- 'HVM' if vm.type == 'HVM' else \
- vm.type.replace('VM','')"},
-
- "updbl": {"func": "'Yes' if vm.updateable else ''"},
-
- "template": {"func": "'n/a' if vm.is_template() or vm.template is None else\
- vm_info['template']"},
-
- "netvm": {"func": "'n/a' if vm.is_netvm() and not vm.is_proxyvm() else\
- ('*' if vm.uses_default_netvm else '') +\
- vm_info['netvm'] if vm_info['netvm'] is not None else '-'"},
-
- "label": {"func": "vm.label.name"},
- }
-
- fields_to_display = ["name", "type", "template", "updbl", "netvm", "label"]
-
- # First calculate the maximum width of each field we want to display
- total_width = 0
- for f in fields_to_display:
- fields[f]["max_width"] = len(f)
- for vm_info in restore_info.values():
- if 'vm' in vm_info.keys():
- # noinspection PyUnusedLocal
- vm = vm_info['vm']
- l = len(unicode(eval(fields[f]["func"])))
- if l > fields[f]["max_width"]:
- fields[f]["max_width"] = l
- total_width += fields[f]["max_width"]
-
- print_callback("")
- print_callback("The following VMs are included in the backup:")
- print_callback("")
-
- # Display the header
- s = ""
- for f in fields_to_display:
- fmt = "{{0:-^{0}}}-+".format(fields[f]["max_width"] + 1)
- s += fmt.format('-')
- print_callback(s)
- s = ""
- for f in fields_to_display:
- fmt = "{{0:>{0}}} |".format(fields[f]["max_width"] + 1)
- s += fmt.format(f)
- print_callback(s)
- s = ""
- for f in fields_to_display:
- fmt = "{{0:-^{0}}}-+".format(fields[f]["max_width"] + 1)
- s += fmt.format('-')
- print_callback(s)
-
- for vm_info in restore_info.values():
- # Skip non-VM here
- if 'vm' not in vm_info:
- continue
- # noinspection PyUnusedLocal
- vm = vm_info['vm']
- s = ""
- for f in fields_to_display:
- fmt = "{{0:>{0}}} |".format(fields[f]["max_width"] + 1)
- s += fmt.format(eval(fields[f]["func"]))
-
- if 'excluded' in vm_info and vm_info['excluded']:
- s += " <-- Excluded from restore"
- elif 'already-exists' in vm_info:
- s += " <-- A VM with the same name already exists on the host!"
- elif 'missing-template' in vm_info:
- s += " <-- No matching template on the host or in the backup found!"
- elif 'missing-netvm' in vm_info:
- s += " <-- No matching netvm on the host or in the backup found!"
- else:
- if 'orig-template' in vm_info:
- s += " <-- Original template was '%s'" % (vm_info['orig-template'])
- if 'rename-to' in vm_info:
- s += " <-- Will be renamed to '%s'" % vm_info['rename-to']
-
- print_callback(s)
-
- if 'dom0' in restore_info.keys():
- s = ""
- for f in fields_to_display:
- fmt = "{{0:>{0}}} |".format(fields[f]["max_width"] + 1)
- if f == "name":
- s += fmt.format("Dom0")
- elif f == "type":
- s += fmt.format("Home")
- else:
- s += fmt.format("")
- if 'username-mismatch' in restore_info['dom0']:
- s += " <-- username in backup and dom0 mismatch"
- if 'ignore-username-mismatch' in restore_info['dom0']:
- s += " (ignored)"
-
- print_callback(s)
-
-
-def backup_restore_do(restore_info,
- host_collection=None, print_callback=print_stdout,
- error_callback=print_stderr, progress_callback=None,
- ):
- global running_backup_operation
-
- # Private functions begin
- def restore_vm_dir_v1(backup_dir, src_dir, dst_dir):
-
- backup_src_dir = src_dir.replace(system_path["qubes_base_dir"],
- backup_dir)
-
- # We prefer to use Linux's cp, because it nicely handles sparse files
- cp_retcode = subprocess.call(["cp", "-rp", "--reflink=auto", backup_src_dir, dst_dir])
- if cp_retcode != 0:
- raise QubesException(
- "*** Error while copying file {0} to {1}".format(backup_src_dir,
- dst_dir))
-
- # Private functions end
-
- options = restore_info['$OPTIONS$']
- backup_location = options['location']
- restore_tmpdir = options['restore_tmpdir']
- passphrase = options['passphrase']
- encrypted = options['encrypted']
- compressed = options['compressed']
- compression_filter = options['compression_filter']
- hmac_algorithm = options['hmac_algorithm']
- crypto_algorithm = options['crypto_algorithm']
- verify_only = options['verify-only']
- appvm = options['appvm']
- format_version = options['format_version']
-
- if format_version is None:
- format_version = backup_detect_format_version(backup_location)
-
- lock_obtained = False
- if host_collection is None:
- host_collection = QubesVmCollection()
- host_collection.lock_db_for_writing()
- host_collection.load()
- lock_obtained = True
-
- # Perform VM restoration in backup order
- vms_dirs = []
- vms_size = 0
- vms = {}
- for vm_info in restore_info.values():
- if 'vm' not in vm_info:
- continue
- if not vm_info['good-to-go']:
- continue
- vm = vm_info['vm']
- if format_version >= 2:
- vms_size += vm.backup_size
- vms_dirs.append(vm.backup_path)
- vms[vm.name] = vm
-
- running_backup_operation = BackupOperationInfo()
-
- if format_version >= 2:
- if 'dom0' in restore_info.keys() and restore_info['dom0']['good-to-go']:
- vms_dirs.append(os.path.dirname(restore_info['dom0']['subdir']))
- vms_size += restore_info['dom0']['size']
-
- try:
- restore_vm_dirs(backup_location,
- restore_tmpdir,
- passphrase=passphrase,
- vms_dirs=vms_dirs,
- vms=vms,
- vms_size=vms_size,
- format_version=format_version,
- hmac_algorithm=hmac_algorithm,
- crypto_algorithm=crypto_algorithm,
- verify_only=verify_only,
- print_callback=print_callback,
- error_callback=error_callback,
- progress_callback=progress_callback,
- encrypted=encrypted,
- compressed=compressed,
- compression_filter=compression_filter,
- appvm=appvm)
- except QubesException:
- if verify_only:
- raise
- else:
- if callable(print_callback):
- print_callback(
- "Some errors occurred during data extraction, "
- "continuing anyway to restore at least some "
- "VMs")
- else:
- if verify_only:
- if callable(print_callback):
- print_callback("WARNING: Backup verification not supported for "
- "this backup format.")
-
- if verify_only:
- shutil.rmtree(restore_tmpdir)
- return
-
- # Add VM in right order
- for (vm_class_name, vm_class) in sorted(QubesVmClasses.items(),
- key=lambda _x: _x[1].load_order):
- if running_backup_operation.canceled:
- break
- for vm in vms.values():
- if running_backup_operation.canceled:
- # only break the loop to save qubes.xml with already restored
- # VMs
- break
- if not vm.__class__ == vm_class:
- continue
- if callable(print_callback):
- print_callback("-> Restoring {type} {0}...".
- format(vm.name, type=vm_class_name))
- retcode = subprocess.call(
- ["mkdir", "-p", os.path.dirname(vm.dir_path)])
- if retcode != 0:
- error_callback("*** Cannot create directory: {0}?!".format(
- vm.dir_path))
- error_callback("Skipping...")
- continue
-
- template = None
- if vm.template is not None:
- template_name = restore_info[vm.name]['template']
- template = host_collection.get_vm_by_name(template_name)
-
- new_vm = None
- vm_name = vm.name
- if 'rename-to' in restore_info[vm.name]:
- vm_name = restore_info[vm.name]['rename-to']
-
- try:
- new_vm = host_collection.add_new_vm(vm_class_name, name=vm_name,
- template=template,
- installed_by_rpm=False)
- if os.path.exists(new_vm.dir_path):
- move_to_path = tempfile.mkdtemp('', os.path.basename(
- new_vm.dir_path), os.path.dirname(new_vm.dir_path))
- try:
- os.rename(new_vm.dir_path, move_to_path)
- error_callback(
- "*** Directory {} already exists! It has "
- "been moved to {}".format(new_vm.dir_path,
- move_to_path))
- except OSError:
- error_callback(
- "*** Directory {} already exists and "
- "cannot be moved!".format(new_vm.dir_path))
- error_callback("Skipping...")
- continue
-
- if format_version == 1:
- restore_vm_dir_v1(backup_location,
- vm.dir_path,
- os.path.dirname(new_vm.dir_path))
- elif format_version >= 2:
- shutil.move(os.path.join(restore_tmpdir, vm.backup_path),
- new_vm.dir_path)
-
- new_vm.verify_files()
- except Exception as err:
- error_callback("ERROR: {0}".format(err))
- error_callback("*** Skipping VM: {0}".format(vm.name))
- if new_vm:
- host_collection.pop(new_vm.qid)
- continue
-
- # FIXME: cannot check for 'kernel' property, because it is always
- # defined - accessing it touches non-existent '_kernel'
- if not isinstance(vm, QubesVmClasses['QubesHVm']):
- # TODO: add a setting for this?
- if vm.kernel and vm.kernel not in \
- os.listdir(system_path['qubes_kernels_base_dir']):
- if callable(print_callback):
- print_callback("WARNING: Kernel %s not installed, "
- "using default one" % vm.kernel)
- vm.uses_default_kernel = True
- vm.kernel = host_collection.get_default_kernel()
- try:
- new_vm.clone_attrs(vm)
- except Exception as err:
- error_callback("ERROR: {0}".format(err))
- error_callback("*** Some VM property will not be restored")
-
- try:
- for service, value in vm.services.items():
- new_vm.services[service] = value
- except Exception as err:
- error_callback("ERROR: {0}".format(err))
- error_callback("*** Some VM property will not be restored")
-
- try:
- new_vm.appmenus_create(verbose=callable(print_callback))
- except Exception as err:
- error_callback("ERROR during appmenu restore: {0}".format(err))
- error_callback(
- "*** VM '{0}' will not have appmenus".format(vm.name))
-
- # Set network dependencies - only non-default netvm setting
- for vm in vms.values():
- vm_name = vm.name
- if 'rename-to' in restore_info[vm.name]:
- vm_name = restore_info[vm.name]['rename-to']
- host_vm = host_collection.get_vm_by_name(vm_name)
-
- if host_vm is None:
- # Failed/skipped VM
- continue
-
- if not vm.uses_default_netvm:
- if restore_info[vm.name]['netvm'] is not None:
- host_vm.netvm = host_collection.get_vm_by_name(
- restore_info[vm.name]['netvm'])
- else:
- host_vm.netvm = None
-
- host_collection.save()
- if lock_obtained:
- host_collection.unlock_db()
-
- if running_backup_operation.canceled:
- if format_version >= 2:
- raise BackupCanceledError("Restore canceled",
- tmpdir=restore_tmpdir)
- else:
- raise BackupCanceledError("Restore canceled")
-
- # ... and dom0 home as last step
- if 'dom0' in restore_info.keys() and restore_info['dom0']['good-to-go']:
- backup_path = restore_info['dom0']['subdir']
- local_user = grp.getgrnam('qubes').gr_mem[0]
- home_dir = pwd.getpwnam(local_user).pw_dir
- if format_version == 1:
- backup_dom0_home_dir = os.path.join(backup_location, backup_path)
- else:
- backup_dom0_home_dir = os.path.join(restore_tmpdir, backup_path)
- restore_home_backupdir = "home-pre-restore-{0}".format(
- time.strftime("%Y-%m-%d-%H%M%S"))
-
- if callable(print_callback):
- print_callback(
- "-> Restoring home of user '{0}'...".format(local_user))
- print_callback(
- "--> Existing files/dirs backed up in '{0}' dir".format(
- restore_home_backupdir))
- os.mkdir(home_dir + '/' + restore_home_backupdir)
- for f in os.listdir(backup_dom0_home_dir):
- home_file = home_dir + '/' + f
- if os.path.exists(home_file):
- os.rename(home_file,
- home_dir + '/' + restore_home_backupdir + '/' + f)
- if format_version == 1:
- subprocess.call(
- ["cp", "-nrp", "--reflink=auto", backup_dom0_home_dir + '/' + f, home_file])
- elif format_version >= 2:
- shutil.move(backup_dom0_home_dir + '/' + f, home_file)
- retcode = subprocess.call(['sudo', 'chown', '-R', local_user, home_dir])
- if retcode != 0:
- error_callback("*** Error while setting home directory owner")
-
- if callable(print_callback):
- print_callback("-> Done. Please install updates for all the restored "
- "templates.")
-
- shutil.rmtree(restore_tmpdir)
-
-# vim:sw=4:et:
diff --git a/core/guihelpers.py b/core/guihelpers.py
deleted file mode 100644
index 0c6f8b77..00000000
--- a/core/guihelpers.py
+++ /dev/null
@@ -1,57 +0,0 @@
-#!/usr/bin/python2
-# -*- coding: utf-8 -*-
-#
-# The Qubes OS Project, http://www.qubes-os.org
-#
-# Copyright (C) 2011 Marek Marczykowski
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-#
-
-import sys
-from optparse import OptionParser
-from PyQt4.QtGui import QApplication,QMessageBox
-
-app = None
-system_bus = None
-
-def prepare_app():
- global app
- app = QApplication(sys.argv)
- app.setOrganizationName("The Qubes Project")
- app.setOrganizationDomain("http://qubes-os.org")
- app.setApplicationName("Qubes")
-
-def ask(text, title="Question", yestoall=False):
- global app
- if app is None:
- prepare_app()
-
- buttons = QMessageBox.Yes | QMessageBox.No
- if yestoall:
- buttons |= QMessageBox.YesToAll
-
- reply = QMessageBox.question(None, title, text, buttons, defaultButton=QMessageBox.Yes)
- if reply == QMessageBox.Yes:
- return 0
- elif reply == QMessageBox.No:
- return 1
- elif reply == QMessageBox.YesToAll:
- return 2
- else:
- #?!
- return 127
-
diff --git a/core/modules b/core/modules
deleted file mode 120000
index c448da83..00000000
--- a/core/modules
+++ /dev/null
@@ -1 +0,0 @@
-../core-modules
\ No newline at end of file
diff --git a/core/notify.py b/core/notify.py
deleted file mode 100644
index 5ca75f3f..00000000
--- a/core/notify.py
+++ /dev/null
@@ -1,78 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# The Qubes OS Project, http://www.qubes-os.org
-#
-# Copyright (C) 2014 Marek Marczykowski-Górecki
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-#
-
-import sys
-
-system_bus = None
-session_bus = None
-
-notify_object = None
-
-def tray_notify_init():
- import dbus
- global notify_object
- try:
- notify_object = dbus.SessionBus().get_object("org.freedesktop.Notifications", "/org/freedesktop/Notifications")
- except dbus.DBusException as ex:
- print >>sys.stderr, "WARNING: failed connect to tray notification service: %s" % str(ex)
-
-def tray_notify(msg, label, timeout = 3000):
- if notify_object:
- if label:
- if not isinstance(label, str):
- label = label.icon
- notify_object.Notify("Qubes", 0, label, "Qubes", msg, [], [], timeout,
- dbus_interface="org.freedesktop.Notifications")
-
-def tray_notify_error(msg, timeout = 3000):
- if notify_object:
- notify_object.Notify("Qubes", 0, "dialog-error", "Qubes", msg, [], [],
- timeout, dbus_interface="org.freedesktop.Notifications")
-
-def notify_error_qubes_manager(name, message):
- import dbus
- global system_bus
- if system_bus is None:
- system_bus = dbus.SystemBus()
-
- try:
- qubes_manager = system_bus.get_object('org.qubesos.QubesManager',
- '/org/qubesos/QubesManager')
- qubes_manager.notify_error(name, message, dbus_interface='org.qubesos.QubesManager')
- except dbus.DBusException:
- # ignore the case when no qubes-manager is running
- pass
-
-def clear_error_qubes_manager(name, message):
- import dbus
- global system_bus
- if system_bus is None:
- system_bus = dbus.SystemBus()
-
- try:
- qubes_manager = system_bus.get_object('org.qubesos.QubesManager',
- '/org/qubesos/QubesManager')
- qubes_manager.clear_error_exact(name, message, dbus_interface='org.qubesos.QubesManager')
- except dbus.DBusException:
- # ignore the case when no qubes-manager is running
- pass
-
diff --git a/core/qubes.py b/core/qubes.py
deleted file mode 100755
index 1ed74c1d..00000000
--- a/core/qubes.py
+++ /dev/null
@@ -1,965 +0,0 @@
-#!/usr/bin/python2
-# -*- coding: utf-8 -*-
-#
-# The Qubes OS Project, http://www.qubes-os.org
-#
-# Copyright (C) 2010 Joanna Rutkowska
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-#
-
-from __future__ import absolute_import
-
-import atexit
-import grp
-import logging
-import os
-import os.path
-import sys
-import tempfile
-import time
-import warnings
-import xml.parsers.expat
-
-import lxml.etree
-
-if os.name == 'posix':
- import fcntl
-elif os.name == 'nt':
- import win32con
- import win32file
- import pywintypes
-else:
- raise RuntimeError, "Qubes works only on POSIX or WinNT systems"
-
-# Do not use XenAPI or create/read any VM files
-# This is for testing only!
-dry_run = False
-#dry_run = True
-
-if not dry_run:
- import libvirt
- try:
- import xen.lowlevel.xs
- except ImportError:
- pass
-
-
-qubes_base_dir = "/var/lib/qubes"
-system_path = {
- 'qubes_guid_path': '/usr/bin/qubes-guid',
- 'qrexec_daemon_path': '/usr/lib/qubes/qrexec-daemon',
- 'qrexec_client_path': '/usr/lib/qubes/qrexec-client',
- 'qubesdb_daemon_path': '/usr/sbin/qubesdb-daemon',
-
- 'qubes_base_dir': qubes_base_dir,
-
- # Relative to qubes_base_dir
- 'qubes_appvms_dir': 'appvms',
- 'qubes_templates_dir': 'vm-templates',
- 'qubes_servicevms_dir': 'servicevms',
- 'qubes_store_filename': 'qubes.xml',
- 'qubes_kernels_base_dir': 'vm-kernels',
-
- # qubes_icon_dir is obsolete
- # use QIcon.fromTheme() where applicable
- 'qubes_icon_dir': '/usr/share/icons/hicolor/128x128/devices',
-
- 'qrexec_policy_dir': '/etc/qubes-rpc/policy',
-
- 'config_template_pv': '/usr/share/qubes/vm-template.xml',
-
- 'qubes_pciback_cmd': '/usr/lib/qubes/unbind-pci-device.sh',
- 'prepare_volatile_img_cmd': '/usr/lib/qubes/prepare-volatile-img.sh',
-}
-
-vm_files = {
- 'root_img': 'root.img',
- 'rootcow_img': 'root-cow.img',
- 'volatile_img': 'volatile.img',
- 'private_img': 'private.img',
- 'kernels_subdir': 'kernels',
- 'firewall_conf': 'firewall.xml',
- 'whitelisted_appmenus': 'whitelisted-appmenus.list',
- 'updates_stat_file': 'updates.stat',
-}
-
-defaults = {
- 'libvirt_uri': 'xen:///',
- 'memory': 400,
- 'kernelopts': "nopat",
- 'kernelopts_pcidevs': "nopat iommu=soft swiotlb=8192",
-
- 'dom0_update_check_interval': 6*3600,
-
- 'private_img_size': 2*1024*1024*1024,
- 'root_img_size': 10*1024*1024*1024,
-
- 'storage_class': None,
-
- # how long (in sec) to wait for VMs to shutdown,
- # before killing them (when used qvm-run with --wait option),
- 'shutdown_counter_max': 60,
-
- 'vm_default_netmask': "255.255.255.0",
-
- # Set later
- 'appvm_label': None,
- 'template_label': None,
- 'servicevm_label': None,
-}
-
-qubes_max_qid = 254
-qubes_max_netid = 254
-
-class QubesException (Exception):
- pass
-
-class QubesVMMConnection(object):
- def __init__(self):
- self._libvirt_conn = None
- self._xs = None
- self._xc = None
- self._offline_mode = False
-
- @property
- def offline_mode(self):
- return self._offline_mode
-
- @offline_mode.setter
- def offline_mode(self, value):
- if not value and self._libvirt_conn is not None:
- raise QubesException("Cannot change offline mode while already connected")
-
- self._offline_mode = value
-
- def _libvirt_error_handler(self, ctx, error):
- pass
-
- def init_vmm_connection(self):
- if self._libvirt_conn is not None:
- # Already initialized
- return
- if self._offline_mode:
- # Do not initialize in offline mode
- return
-
- if 'xen.lowlevel.xs' in sys.modules:
- self._xs = xen.lowlevel.xs.xs()
- self._libvirt_conn = libvirt.open(defaults['libvirt_uri'])
- if self._libvirt_conn == None:
- raise QubesException("Failed connect to libvirt driver")
- libvirt.registerErrorHandler(self._libvirt_error_handler, None)
- atexit.register(self._libvirt_conn.close)
-
- def _common_getter(self, name):
- if self._offline_mode:
- # Do not initialize in offline mode
- raise QubesException("VMM operations disabled in offline mode")
-
- if self._libvirt_conn is None:
- self.init_vmm_connection()
- return getattr(self, name)
-
- @property
- def libvirt_conn(self):
- return self._common_getter('_libvirt_conn')
-
- @property
- def xs(self):
- if 'xen.lowlevel.xs' in sys.modules:
- return self._common_getter('_xs')
- else:
- return None
-
-
-##### VMM global variable definition #####
-
-if not dry_run:
- vmm = QubesVMMConnection()
-
-##########################################
-
-class QubesHost(object):
- def __init__(self):
- (model, memory, cpus, mhz, nodes, socket, cores, threads) = vmm.libvirt_conn.getInfo()
- self._total_mem = long(memory)*1024
- self._no_cpus = cpus
-
-# print "QubesHost: total_mem = {0}B".format (self.xen_total_mem)
-# print "QubesHost: free_mem = {0}".format (self.get_free_xen_memory())
-# print "QubesHost: total_cpus = {0}".format (self.xen_no_cpus)
-
- @property
- def memory_total(self):
- return self._total_mem
-
- @property
- def no_cpus(self):
- return self._no_cpus
-
- # TODO
- def measure_cpu_usage(self, qvmc, previous=None, previous_time = None,
- wait_time=1):
- """measure cpu usage for all domains at once"""
- if previous is None:
- previous_time = time.time()
- previous = {}
- for vm in qvmc.values():
- if not vm.is_running():
- continue
- cputime = vm.get_cputime()
- previous[vm.xid] = {}
- previous[vm.xid]['cpu_time'] = (
- cputime / max(vm.vcpus, 1))
- previous[vm.xid]['cpu_usage'] = 0
- time.sleep(wait_time)
-
- current_time = time.time()
- current = {}
- for vm in qvmc.values():
- if not vm.is_running():
- continue
- cputime = vm.get_cputime()
- current[vm.xid] = {}
- current[vm.xid]['cpu_time'] = (
- cputime / max(vm.vcpus, 1))
- if vm.xid in previous.keys():
- current[vm.xid]['cpu_usage'] = (
- float(current[vm.xid]['cpu_time'] -
- previous[vm.xid]['cpu_time']) /
- long(1000**3) / (current_time-previous_time) * 100)
- if current[vm.xid]['cpu_usage'] < 0:
- # VM has been rebooted
- current[vm.xid]['cpu_usage'] = 0
- else:
- current[vm.xid]['cpu_usage'] = 0
-
- return (current_time, current)
-
-class QubesVmLabel(object):
- def __init__(self, index, color, name, dispvm=False):
- self.index = index
- self.color = color
- self.name = name
- self.dispvm = dispvm
-
- self.icon = '{}-{}'.format(('dispvm' if dispvm else 'appvm'), name)
-
- def __repr__(self):
- return '{}({!r}, {!r}, {!r}, dispvm={!r})'.format(
- self.__class__.__name__,
- self.index,
- self.color,
- self.name,
- self.dispvm)
-
- # self.icon_path is obsolete
- # use QIcon.fromTheme(label.icon) where applicable
- @property
- def icon_path(self):
- return os.path.join(system_path['qubes_icon_dir'], self.icon) + ".png"
-
-def register_qubes_vm_class(vm_class):
- QubesVmClasses[vm_class.__name__] = vm_class
- # register class as local for this module - to make it easy to import from
- # other modules
- setattr(sys.modules[__name__], vm_class.__name__, vm_class)
-
-class QubesVmCollection(dict):
- """
- A collection of Qubes VMs indexed by Qubes id (qid)
- """
-
- def __init__(self, store_filename=None):
- super(QubesVmCollection, self).__init__()
- self.default_netvm_qid = None
- self.default_fw_netvm_qid = None
- self.default_template_qid = None
- self.default_kernel = None
- self.updatevm_qid = None
- self.qubes_store_filename = store_filename
- if not store_filename:
- self.qubes_store_filename = system_path["qubes_store_filename"]
- self.clockvm_qid = None
- self.qubes_store_file = None
-
- self.log = logging.getLogger('qubes.qvmc.{:x}'.format(id(self)))
- self.log.debug('instantiated store_filename={!r}'.format(
- self.qubes_store_filename))
-
- def __repr__(self):
- return '<{} {!r}>'.format(self.__class__.__name__, list(sorted(self.keys())))
-
- def clear(self):
- self.log.debug('clear()')
- # Hack for releasing FDs, which otherwise would be leaked because of
- # circular dependencies on QubesVMs objects (so garbage collector
- # doesn't handle them). See #1380 for details
- for vm in self.values():
- try:
- if vm._qdb_connection:
- vm._qdb_connection.close()
- vm._qdb_connection = None
- except AttributeError:
- pass
- super(QubesVmCollection, self).clear()
-
- def values(self):
- for qid in self.keys():
- yield self[qid]
-
- def items(self):
- for qid in self.keys():
- yield (qid, self[qid])
-
- def __iter__(self):
- for qid in sorted(super(QubesVmCollection, self).keys()):
- yield qid
-
- keys = __iter__
-
- def __setitem__(self, key, value):
- self.log.debug('[{!r}] = {!r}'.format(key, value))
- if key not in self:
- return super(QubesVmCollection, self).__setitem__(key, value)
- else:
- assert False, "Attempt to add VM with qid that already exists in the collection!"
-
- def add_new_vm(self, vm_type, **kwargs):
- self.log.debug('add_new_vm(vm_type={}, **kwargs={!r})'.format(
- vm_type, kwargs))
- if vm_type not in QubesVmClasses.keys():
- raise ValueError("Unknown VM type: %s" % vm_type)
-
- qid = self.get_new_unused_qid()
- 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
-
- def add_new_appvm(self, name, template,
- dir_path = None, conf_file = None,
- private_img = None,
- label = None):
-
- warnings.warn("Call to deprecated function, use add_new_vm instead",
- DeprecationWarning, stacklevel=2)
- return self.add_new_vm("QubesAppVm", name=name, template=template,
- dir_path=dir_path, conf_file=conf_file,
- private_img=private_img,
- netvm = self.get_default_netvm(),
- kernel = self.get_default_kernel(),
- uses_default_kernel = True,
- label=label)
-
- def add_new_hvm(self, name, label = None):
-
- warnings.warn("Call to deprecated function, use add_new_vm instead",
- DeprecationWarning, stacklevel=2)
- return self.add_new_vm("QubesHVm", name=name, label=label)
-
- def add_new_disposablevm(self, name, template, dispid,
- label = None, netvm = None):
-
- warnings.warn("Call to deprecated function, use add_new_vm instead",
- DeprecationWarning, stacklevel=2)
- return self.add_new_vm("QubesDisposableVm", name=name, template=template,
- netvm = netvm,
- label=label, dispid=dispid)
-
- def add_new_templatevm(self, name,
- dir_path = None, conf_file = None,
- root_img = None, private_img = None,
- installed_by_rpm = True):
-
- warnings.warn("Call to deprecated function, use add_new_vm instead",
- DeprecationWarning, stacklevel=2)
- return self.add_new_vm("QubesTemplateVm", name=name,
- dir_path=dir_path, conf_file=conf_file,
- root_img=root_img, private_img=private_img,
- installed_by_rpm=installed_by_rpm,
- netvm = self.get_default_netvm(),
- kernel = self.get_default_kernel(),
- uses_default_kernel = True)
-
- def add_new_netvm(self, name, template,
- dir_path = None, conf_file = None,
- private_img = None, installed_by_rpm = False,
- label = None):
-
- warnings.warn("Call to deprecated function, use add_new_vm instead",
- DeprecationWarning, stacklevel=2)
- return self.add_new_vm("QubesNetVm", name=name, template=template,
- label=label,
- private_img=private_img, installed_by_rpm=installed_by_rpm,
- uses_default_kernel = True,
- dir_path=dir_path, conf_file=conf_file)
-
- def add_new_proxyvm(self, name, template,
- dir_path = None, conf_file = None,
- private_img = None, installed_by_rpm = False,
- label = None):
-
- warnings.warn("Call to deprecated function, use add_new_vm instead",
- DeprecationWarning, stacklevel=2)
- return self.add_new_vm("QubesProxyVm", name=name, template=template,
- label=label,
- private_img=private_img, installed_by_rpm=installed_by_rpm,
- dir_path=dir_path, conf_file=conf_file,
- uses_default_kernel = True,
- netvm = self.get_default_fw_netvm())
-
- def set_default_template(self, vm):
- self.log.debug('set_default_template({!r})'.format(vm))
- if vm is None:
- self.default_template_qid = None
- else:
- assert vm.is_template(), "VM {0} is not a TemplateVM!".format(vm.name)
- self.default_template_qid = vm.qid
-
- def get_default_template(self):
- if self.default_template_qid is None:
- return None
- else:
- return self[self.default_template_qid]
-
- def set_default_netvm(self, vm):
- self.log.debug('set_default_netvm({!r})'.format(vm))
- if vm is None:
- self.default_netvm_qid = None
- else:
- assert vm.is_netvm(), "VM {0} does not provide network!".format(vm.name)
- self.default_netvm_qid = vm.qid
-
- def get_default_netvm(self):
- if self.default_netvm_qid is None:
- return None
- else:
- return self[self.default_netvm_qid]
-
- def set_default_kernel(self, kernel):
- self.log.debug('set_default_kernel({!r})'.format(kernel))
- assert os.path.exists(
- os.path.join(system_path["qubes_kernels_base_dir"], kernel)), \
- "Kerel {0} not installed!".format(kernel)
- self.default_kernel = kernel
-
- def get_default_kernel(self):
- return self.default_kernel
-
- def set_default_fw_netvm(self, vm):
- self.log.debug('set_default_fw_netvm({!r})'.format(vm))
- if vm is None:
- self.default_fw_netvm_qid = None
- else:
- assert vm.is_netvm(), "VM {0} does not provide network!".format(vm.name)
- self.default_fw_netvm_qid = vm.qid
-
- def get_default_fw_netvm(self):
- if self.default_fw_netvm_qid is None:
- return None
- else:
- return self[self.default_fw_netvm_qid]
-
- def set_updatevm_vm(self, vm):
- self.log.debug('set_updatevm_vm({!r})'.format(vm))
- if vm is None:
- self.updatevm_qid = None
- else:
- self.updatevm_qid = vm.qid
-
- def get_updatevm_vm(self):
- if self.updatevm_qid is None:
- return None
- else:
- return self[self.updatevm_qid]
-
- def set_clockvm_vm(self, vm):
- self.log.debug('set_clockvm({!r})'.format(vm))
- if vm is None:
- self.clockvm_qid = None
- else:
- self.clockvm_qid = vm.qid
-
- def get_clockvm_vm(self):
- if self.clockvm_qid is None:
- return None
- else:
- return self[self.clockvm_qid]
-
- def get_vm_by_name(self, name):
- for vm in self.values():
- if (vm.name == name):
- return vm
- return None
-
- def get_qid_by_name(self, name):
- vm = self.get_vm_by_name(name)
- return vm.qid if vm is not None else None
-
- def get_vms_based_on(self, template_qid):
- vms = set([vm for vm in self.values()
- if (vm.template and vm.template.qid == template_qid)])
- return vms
-
- def get_vms_connected_to(self, netvm_qid):
- new_vms = [ netvm_qid ]
- dependend_vms_qid = []
-
- # Dependency resolving only makes sense on NetVM (or derivative)
- if not self[netvm_qid].is_netvm():
- return set([])
-
- while len(new_vms) > 0:
- cur_vm = new_vms.pop()
- for vm in self[cur_vm].connected_vms.values():
- if vm.qid not in dependend_vms_qid:
- dependend_vms_qid.append(vm.qid)
- if vm.is_netvm():
- new_vms.append(vm.qid)
-
- vms = [vm for vm in self.values() if vm.qid in dependend_vms_qid]
- return vms
-
- def verify_new_vm(self, new_vm):
-
- # Verify that qid is unique
- for vm in self.values():
- if vm.qid == new_vm.qid:
- print >> sys.stderr, "ERROR: The qid={0} is already used by VM '{1}'!".\
- format(vm.qid, vm.name)
- return False
-
- # Verify that name is unique
- for vm in self.values():
- if vm.name == new_vm.name:
- print >> sys.stderr, \
- "ERROR: The name={0} is already used by other VM with qid='{1}'!".\
- format(vm.name, vm.qid)
- return False
-
- return True
-
- def get_new_unused_qid(self):
- used_ids = set([vm.qid for vm in self.values()])
- for id in range (1, qubes_max_qid):
- if id not in used_ids:
- return id
- raise LookupError ("Cannot find unused qid!")
-
- def get_new_unused_netid(self):
- used_ids = set([vm.netid for vm in self.values() if vm.is_netvm()])
- for id in range (1, qubes_max_netid):
- if id not in used_ids:
- return id
- raise LookupError ("Cannot find unused netid!")
-
-
- def check_if_storage_exists(self):
- try:
- f = open (self.qubes_store_filename, 'r')
- except IOError:
- return False
- f.close()
- return True
-
- def create_empty_storage(self):
- self.log.debug('create_empty_storage()')
- self.qubes_store_file = open (self.qubes_store_filename, 'w')
- self.clear()
- self.save()
-
- def lock_db_for_reading(self):
- if self.qubes_store_file is not None:
- raise QubesException("lock already taken")
- # save() would rename the file over qubes.xml, _then_ release lock,
- # so we need to ensure that the file for which we've got the lock is
- # still the right file
- self.log.debug('lock_db_for_reading()')
- while True:
- self.qubes_store_file = open (self.qubes_store_filename, 'r')
- if os.name == 'posix':
- fcntl.lockf (self.qubes_store_file, fcntl.LOCK_SH)
- elif os.name == 'nt':
- overlapped = pywintypes.OVERLAPPED()
- win32file.LockFileEx(win32file._get_osfhandle(self.qubes_store_file.fileno()),
- 0, 0, -0x10000, overlapped)
- if os.fstat(self.qubes_store_file.fileno()) == os.stat(
- self.qubes_store_filename):
- break
- self.qubes_store_file.close()
-
- def lock_db_for_writing(self):
- if self.qubes_store_file is not None:
- raise QubesException("lock already taken")
- # save() would rename the file over qubes.xml, _then_ release lock,
- # so we need to ensure that the file for which we've got the lock is
- # still the right file
- self.log.debug('lock_db_for_writing()')
- while True:
- self.qubes_store_file = open (self.qubes_store_filename, 'r+')
- if os.name == 'posix':
- fcntl.lockf (self.qubes_store_file, fcntl.LOCK_EX)
- elif os.name == 'nt':
- overlapped = pywintypes.OVERLAPPED()
- win32file.LockFileEx(win32file._get_osfhandle(self.qubes_store_file.fileno()),
- win32con.LOCKFILE_EXCLUSIVE_LOCK, 0, -0x10000, overlapped)
- if os.fstat(self.qubes_store_file.fileno()) == os.stat(
- self.qubes_store_filename):
- break
- self.qubes_store_file.close()
-
- def unlock_db(self):
- if self.qubes_store_file is None:
- return
- # intentionally do not call explicit unlock to not unlock the file
- # before all buffers are flushed
- self.log.debug('unlock_db()')
- self.qubes_store_file.close()
- self.qubes_store_file = None
-
- def save(self):
- self.log.debug('save()')
- root = lxml.etree.Element(
- "QubesVmCollection",
-
- default_template=str(self.default_template_qid) \
- if self.default_template_qid is not None else "None",
-
- default_netvm=str(self.default_netvm_qid) \
- if self.default_netvm_qid is not None else "None",
-
- default_fw_netvm=str(self.default_fw_netvm_qid) \
- if self.default_fw_netvm_qid is not None else "None",
-
- updatevm=str(self.updatevm_qid) \
- if self.updatevm_qid is not None else "None",
-
- clockvm=str(self.clockvm_qid) \
- if self.clockvm_qid is not None else "None",
-
- default_kernel=str(self.default_kernel) \
- if self.default_kernel is not None else "None",
- )
-
- for vm in self.values():
- element = vm.create_xml_element()
- if element is not None:
- root.append(element)
- tree = lxml.etree.ElementTree(root)
-
- try:
-
- new_store_file = tempfile.NamedTemporaryFile(prefix=self.qubes_store_filename, delete=False)
- if os.name == 'posix':
- fcntl.lockf (new_store_file, fcntl.LOCK_EX)
- elif os.name == 'nt':
- overlapped = pywintypes.OVERLAPPED()
- win32file.LockFileEx(win32file._get_osfhandle(new_store_file.fileno()),
- win32con.LOCKFILE_EXCLUSIVE_LOCK, 0, -0x10000, overlapped)
- tree.write(new_store_file, encoding="UTF-8", pretty_print=True)
- new_store_file.flush()
- os.chmod(new_store_file.name, 0660)
- os.chown(new_store_file.name, -1, grp.getgrnam('qubes').gr_gid)
- os.rename(new_store_file.name, self.qubes_store_filename)
- self.qubes_store_file.close()
- self.qubes_store_file = new_store_file
- except EnvironmentError as err:
- print("{0}: export error: {1}".format(
- os.path.basename(sys.argv[0]), err))
- return False
- return True
-
- def set_netvm_dependency(self, element):
- kwargs = {}
- attr_list = ("qid", "netvm_qid")
-
- for attribute in attr_list:
- kwargs[attribute] = element.get(attribute)
-
- vm = self[int(kwargs["qid"])]
-
- if vm.uses_default_netvm is True:
- if vm.is_proxyvm():
- netvm = self.get_default_fw_netvm()
- else:
- netvm = self.get_default_netvm()
- kwargs.pop("netvm_qid")
- else:
- if kwargs["netvm_qid"] == "none" or kwargs["netvm_qid"] is None:
- netvm = None
- kwargs.pop("netvm_qid")
- else:
- netvm_qid = int(kwargs.pop("netvm_qid"))
- if netvm_qid not in self:
- netvm = None
- else:
- netvm = self[netvm_qid]
-
- # directly set internal attr to not call setters...
- vm._netvm = netvm
- if netvm:
- netvm.connected_vms[vm.qid] = vm
-
-
- def load_globals(self, element):
- default_template = element.get("default_template")
- self.default_template_qid = int(default_template) \
- if default_template.lower() != "none" else None
-
- default_netvm = element.get("default_netvm")
- if default_netvm is not None:
- self.default_netvm_qid = int(default_netvm) \
- if default_netvm != "None" else None
- #assert self.default_netvm_qid is not None
-
- default_fw_netvm = element.get("default_fw_netvm")
- if default_fw_netvm is not None:
- self.default_fw_netvm_qid = int(default_fw_netvm) \
- if default_fw_netvm != "None" else None
- #assert self.default_netvm_qid is not None
-
- updatevm = element.get("updatevm")
- if updatevm is not None:
- self.updatevm_qid = int(updatevm) \
- if updatevm != "None" else None
- #assert self.default_netvm_qid is not None
-
- clockvm = element.get("clockvm")
- if clockvm is not None:
- self.clockvm_qid = int(clockvm) \
- if clockvm != "None" else None
-
- self.default_kernel = element.get("default_kernel")
-
-
- def _check_global(self, attr, default):
- qid = getattr(self, attr)
- if qid is None:
- return
- try:
- self[qid]
- except KeyError:
- setattr(self, attr, default)
-
-
- def check_globals(self):
- '''Ensure that all referenced qids are present in the collection'''
- self._check_global('default_template_qid', None)
- self._check_global('default_fw_netvm_qid', None)
- self._check_global('default_netvm_qid', self.default_fw_netvm_qid)
- self._check_global('updatevm_qid', self.default_netvm_qid)
- self._check_global('clockvm_qid', self.default_netvm_qid)
-
-
- def load(self):
- self.log.debug('load()')
- self.clear()
-
- try:
- self.qubes_store_file.seek(0)
- tree = lxml.etree.parse(self.qubes_store_file)
- except (EnvironmentError,
- xml.parsers.expat.ExpatError) as err:
- print("{0}: import error: {1}".format(
- os.path.basename(sys.argv[0]), err))
- return False
-
- self.load_globals(tree.getroot())
-
- for (vm_class_name, vm_class) in sorted(QubesVmClasses.items(),
- key=lambda _x: _x[1].load_order):
- vms_of_class = tree.findall(vm_class_name)
- # first non-template based, then template based
- sorted_vms_of_class = sorted(vms_of_class, key= \
- lambda x: str(x.get('template_qid')).lower() != "none")
- for element in sorted_vms_of_class:
- try:
- vm = vm_class(xml_element=element, collection=self)
- self[vm.qid] = vm
- except (ValueError, LookupError) as err:
- print("{0}: import error ({1}): {2}".format(
- os.path.basename(sys.argv[0]), vm_class_name, err))
- raise
- return False
-
- # After importing all VMs, set netvm references, in the same order
- for (vm_class_name, vm_class) in sorted(QubesVmClasses.items(),
- key=lambda _x: _x[1].load_order):
- for element in tree.findall(vm_class_name):
- try:
- self.set_netvm_dependency(element)
- except (ValueError, LookupError) as err:
- print("{0}: import error2 ({}): {}".format(
- os.path.basename(sys.argv[0]), vm_class_name, err))
- return False
-
- self.check_globals()
-
- # if there was no clockvm entry in qubes.xml, try to determine default:
- # root of default NetVM chain
- if tree.getroot().get("clockvm") is None:
- if self.default_netvm_qid is not None:
- clockvm = self[self.default_netvm_qid]
- # Find root of netvm chain
- while clockvm.netvm is not None:
- clockvm = clockvm.netvm
-
- self.clockvm_qid = clockvm.qid
-
- # Disable ntpd in ClockVM - to not conflict with ntpdate (both are
- # using 123/udp port)
- if self.clockvm_qid is not None:
- self[self.clockvm_qid].services['ntpd'] = False
-
- # Add dom0 if wasn't present in qubes.xml
- if not 0 in self.keys():
- dom0vm = QubesAdminVm (collection=self)
- self[dom0vm.qid] = dom0vm
-
- return True
-
- def pop(self, qid):
- self.log.debug('pop({})'.format(qid))
-
- if self.default_netvm_qid == qid:
- self.default_netvm_qid = None
- if self.default_fw_netvm_qid == qid:
- self.default_fw_netvm_qid = None
- if self.clockvm_qid == qid:
- self.clockvm_qid = None
- if self.updatevm_qid == qid:
- self.updatevm_qid = None
- if self.default_template_qid == qid:
- self.default_template_qid = None
-
- return super(QubesVmCollection, self).pop(qid)
-
-class QubesDaemonPidfile(object):
- def __init__(self, name):
- self.name = name
- self.path = "/var/run/qubes/" + name + ".pid"
-
- def create_pidfile(self):
- f = open (self.path, 'w')
- f.write(str(os.getpid()))
- f.close()
-
- def pidfile_exists(self):
- return os.path.exists(self.path)
-
- def read_pid(self):
- f = open (self.path)
- pid = f.read ().strip()
- f.close()
- return int(pid)
-
- def pidfile_is_stale(self):
- if not self.pidfile_exists():
- return False
-
- # check if the pid file is valid...
- proc_path = "/proc/" + str(self.read_pid()) + "/cmdline"
- if not os.path.exists (proc_path):
- print >> sys.stderr, \
- "Path {0} doesn't exist, assuming stale pidfile.".\
- format(proc_path)
- return True
-
- return False # It's a good pidfile
-
- def remove_pidfile(self):
- os.remove (self.path)
-
- def __enter__ (self):
- # assumes the pidfile doesn't exist -- you should ensure it before opening the context
- self.create_pidfile()
-
- def __exit__ (self, exc_type, exc_val, exc_tb):
- self.remove_pidfile()
- return False
-
-### Initialization code
-
-# Globally defined lables
-QubesVmLabels = {
- "red": QubesVmLabel(1, "0xcc0000", "red" ),
- "orange": QubesVmLabel(2, "0xf57900", "orange" ),
- "yellow": QubesVmLabel(3, "0xedd400", "yellow" ),
- "green": QubesVmLabel(4, "0x73d216", "green" ),
- "gray": QubesVmLabel(5, "0x555753", "gray" ),
- "blue": QubesVmLabel(6, "0x3465a4", "blue" ),
- "purple": QubesVmLabel(7, "0x75507b", "purple" ),
- "black": QubesVmLabel(8, "0x000000", "black" ),
-}
-
-QubesDispVmLabels = {
- k: QubesVmLabel(index=v.index, color=v.color, name=v.name, dispvm=True)
- for k, v in QubesVmLabels.iteritems()
-}
-
-defaults["appvm_label"] = QubesVmLabels["red"]
-defaults["template_label"] = QubesVmLabels["black"]
-defaults["servicevm_label"] = QubesVmLabels["red"]
-
-
-QubesVmClasses = {}
-modules_dir = os.path.join(os.path.dirname(__file__), 'modules')
-for module_file in sorted(os.listdir(modules_dir)):
- if not module_file.endswith(".py") or module_file == "__init__.py":
- continue
- __import__('qubes.modules.%s' % module_file[:-3])
-
-try:
- import qubes.settings
- qubes.settings.apply(system_path, vm_files, defaults)
-except ImportError:
- pass
-
-for path_key in system_path.keys():
- if not os.path.isabs(system_path[path_key]):
- system_path[path_key] = os.path.join(
- system_path['qubes_base_dir'], system_path[path_key])
-
-# vim:sw=4:et:
diff --git a/core/qubesutils.py b/core/qubesutils.py
deleted file mode 100644
index bd080559..00000000
--- a/core/qubesutils.py
+++ /dev/null
@@ -1,879 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-#
-# The Qubes OS Project, http://www.qubes-os.org
-#
-# Copyright (C) 2011 Marek Marczykowski
-# Copyright (C) 2014 Wojciech Porczyk
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-#
-
-from __future__ import absolute_import
-
-import string
-import errno
-from lxml import etree
-from lxml.etree import ElementTree, SubElement, Element
-
-from qubes.qubes import QubesException
-from qubes.qubes import vmm,defaults
-from qubes.qubes import system_path,vm_files
-import sys
-import os
-import subprocess
-import re
-import time
-import stat
-import libvirt
-from qubes.qdb import QubesDB,Error,DisconnectedError
-
-import xen.lowlevel.xc
-import xen.lowlevel.xs
-
-BLKSIZE = 512
-
-# all frontends, prefer xvdi
-# TODO: get this from libvirt driver?
-AVAILABLE_FRONTENDS = ['xvd'+c for c in
- string.lowercase[8:]+string.lowercase[:8]]
-
-class USBProxyNotInstalled(QubesException):
- pass
-
-def mbytes_to_kmg(size):
- if size > 1024:
- return "%d GiB" % (size/1024)
- else:
- return "%d MiB" % size
-
-def kbytes_to_kmg(size):
- if size > 1024:
- return mbytes_to_kmg(size/1024)
- else:
- return "%d KiB" % size
-
-def bytes_to_kmg(size):
- if size > 1024:
- return kbytes_to_kmg(size/1024)
- else:
- return "%d B" % size
-
-def size_to_human (size):
- """Humane readable size, with 1/10 precission"""
- if size < 1024:
- return str (size);
- elif size < 1024*1024:
- return str(round(size/1024.0,1)) + ' KiB'
- elif size < 1024*1024*1024:
- return str(round(size/(1024.0*1024),1)) + ' MiB'
- else:
- return str(round(size/(1024.0*1024*1024),1)) + ' GiB'
-
-def parse_size(size):
- units = [ ('K', 1024), ('KB', 1024),
- ('M', 1024*1024), ('MB', 1024*1024),
- ('G', 1024*1024*1024), ('GB', 1024*1024*1024),
- ]
-
- size = size.strip().upper()
- if size.isdigit():
- return int(size)
-
- for unit, multiplier in units:
- if size.endswith(unit):
- size = size[:-len(unit)].strip()
- return int(size)*multiplier
-
- raise QubesException("Invalid size: {0}.".format(size))
-
-def get_disk_usage_one(st):
- try:
- return st.st_blocks * BLKSIZE
- except AttributeError:
- return st.st_size
-
-def get_disk_usage(path):
- try:
- st = os.lstat(path)
- except OSError:
- return 0
-
- ret = get_disk_usage_one(st)
-
- # if path is not a directory, this is skipped
- for dirpath, dirnames, filenames in os.walk(path):
- for name in dirnames + filenames:
- ret += get_disk_usage_one(os.lstat(os.path.join(dirpath, name)))
-
- return ret
-
-def print_stdout(text):
- print (text)
-
-def print_stderr(text):
- print >> sys.stderr, (text)
-
-###### Block devices ########
-
-def block_devid_to_name(devid):
- major = devid / 256
- minor = devid % 256
-
- dev_class = ""
- if major == 202:
- dev_class = "xvd"
- elif major == 8:
- dev_class = "sd"
- else:
- raise QubesException("Unknown device class %d" % major)
-
- if minor % 16 == 0:
- return "%s%c" % (dev_class, ord('a')+minor/16)
- else:
- return "%s%c%d" % (dev_class, ord('a')+minor/16, minor%16)
-
-def block_name_to_majorminor(name):
- # check if it is already devid
- if isinstance(name, int):
- return (name / 256, name % 256)
- if name.isdigit():
- return (int(name) / 256, int(name) % 256)
-
- if os.path.exists('/dev/%s' % name):
- blk_info = os.stat(os.path.realpath('/dev/%s' % name))
- if stat.S_ISBLK(blk_info.st_mode):
- return (blk_info.st_rdev / 256, blk_info.st_rdev % 256)
-
- major = 0
- minor = 0
- dXpY_style = False
- disk = True
-
- if name.startswith("xvd"):
- major = 202
- elif name.startswith("sd"):
- major = 8
- elif name.startswith("mmcblk"):
- dXpY_style = True
- major = 179
- elif name.startswith("scd"):
- disk = False
- major = 11
- elif name.startswith("sr"):
- disk = False
- major = 11
- elif name.startswith("loop"):
- dXpY_style = True
- disk = False
- major = 7
- elif name.startswith("md"):
- dXpY_style = True
- major = 9
- elif name.startswith("dm-"):
- disk = False
- major = 253
- else:
- # Unknown device
- return (0, 0)
-
- if not dXpY_style:
- name_match = re.match(r"^([a-z]+)([a-z-])([0-9]*)$", name)
- else:
- name_match = re.match(r"^([a-z]+)([0-9]*)(?:p([0-9]+))?$", name)
- if not name_match:
- raise QubesException("Invalid device name: %s" % name)
-
- if disk:
- if dXpY_style:
- minor = int(name_match.group(2))*8
- else:
- minor = (ord(name_match.group(2))-ord('a')) * 16
- else:
- minor = 0
- if name_match.group(3):
- minor += int(name_match.group(3))
-
- return (major, minor)
-
-
-def block_name_to_devid(name):
- # check if it is already devid
- if isinstance(name, int):
- return name
- if name.isdigit():
- return int(name)
-
- (major, minor) = block_name_to_majorminor(name)
- return major << 8 | minor
-
-def block_find_unused_frontend(vm = None):
- assert vm is not None
- assert vm.is_running()
-
- xml = vm.libvirt_domain.XMLDesc()
- parsed_xml = etree.fromstring(xml)
- used = [target.get('dev', None) for target in
- parsed_xml.xpath("//domain/devices/disk/target")]
- for dev in AVAILABLE_FRONTENDS:
- if dev not in used:
- return dev
- return None
-
-def block_list_vm(vm, system_disks = False):
- name_re = re.compile(r"^[a-z0-9-]{1,12}$")
- device_re = re.compile(r"^[a-z0-9/-]{1,64}$")
- # FIXME: any better idea of desc_re?
- desc_re = re.compile(r"^.{1,255}$")
- mode_re = re.compile(r"^[rw]$")
-
- assert vm is not None
-
- if not vm.is_running():
- return []
-
- devices_list = {}
-
- try:
- untrusted_devices = vm.qdb.multiread('/qubes-block-devices/')
- except Error:
- vm.refresh()
- return {}
-
- def get_dev_item(dev, item):
- return untrusted_devices.get(
- '/qubes-block-devices/%s/%s' % (dev, item),
- None)
-
- untrusted_devices_names = list(set(map(lambda x: x.split("/")[2],
- untrusted_devices.keys())))
- for untrusted_dev_name in untrusted_devices_names:
- if name_re.match(untrusted_dev_name):
- dev_name = untrusted_dev_name
- untrusted_device_size = get_dev_item(dev_name, 'size')
- untrusted_device_desc = get_dev_item(dev_name, 'desc')
- untrusted_device_mode = get_dev_item(dev_name, 'mode')
- untrusted_device_device = get_dev_item(dev_name, 'device')
- if untrusted_device_desc is None or untrusted_device_mode is None\
- or untrusted_device_size is None:
- print >>sys.stderr, "Missing field in %s device parameters" %\
- dev_name
- continue
- if untrusted_device_device is None:
- untrusted_device_device = '/dev/' + dev_name
- if not device_re.match(untrusted_device_device):
- print >> sys.stderr, "Invalid %s device path in VM '%s'" % (
- dev_name, vm.name)
- continue
- device_device = untrusted_device_device
- if not untrusted_device_size.isdigit():
- print >> sys.stderr, "Invalid %s device size in VM '%s'" % (
- dev_name, vm.name)
- continue
- device_size = int(untrusted_device_size)
- if not desc_re.match(untrusted_device_desc):
- print >> sys.stderr, "Invalid %s device desc in VM '%s'" % (
- dev_name, vm.name)
- continue
- device_desc = untrusted_device_desc
- if not mode_re.match(untrusted_device_mode):
- print >> sys.stderr, "Invalid %s device mode in VM '%s'" % (
- dev_name, vm.name)
- continue
- device_mode = untrusted_device_mode
-
- if not system_disks:
- if vm.qid == 0 and device_desc.startswith(system_path[
- "qubes_base_dir"]):
- continue
-
- visible_name = "%s:%s" % (vm.name, dev_name)
- devices_list[visible_name] = {
- "name": visible_name,
- "vm": vm.name,
- "device": device_device,
- "size": device_size,
- "desc": device_desc,
- "mode": device_mode
- }
-
- return devices_list
-
-def block_list(qvmc = None, vm = None, system_disks = False):
- if vm is not None:
- if not vm.is_running():
- return []
- else:
- vm_list = [ vm ]
- else:
- if qvmc is None:
- raise QubesException("You must pass either qvm or vm argument")
- vm_list = qvmc.values()
-
- devices_list = {}
- for vm in vm_list:
- devices_list.update(block_list_vm(vm, system_disks))
- return devices_list
-
-def block_check_attached(qvmc, device):
- """
-
- @type qvmc: QubesVmCollection
- """
- if qvmc is None:
- # TODO: ValueError
- raise QubesException("You need to pass qvmc argument")
-
- for vm in qvmc.values():
- if vm.qid == 0:
- # Connecting devices to dom0 not supported
- continue
- if not vm.is_running():
- continue
- try:
- libvirt_domain = vm.libvirt_domain
- if libvirt_domain:
- xml = libvirt_domain.XMLDesc()
- else:
- xml = None
- except libvirt.libvirtError:
- if vmm.libvirt_conn.virConnGetLastError()[0] == libvirt.VIR_ERR_NO_DOMAIN:
- xml = None
- else:
- raise
- if xml:
- parsed_xml = etree.fromstring(xml)
- disks = parsed_xml.xpath("//domain/devices/disk")
- for disk in disks:
- backend_name = 'dom0'
- if disk.find('backenddomain') is not None:
- backend_name = disk.find('backenddomain').get('name')
- source = disk.find('source')
- if disk.get('type') == 'file':
- path = source.get('file')
- elif disk.get('type') == 'block':
- path = source.get('dev')
- else:
- # TODO: logger
- print >>sys.stderr, "Unknown disk type '%s' attached to " \
- "VM '%s'" % (source.get('type'),
- vm.name)
- continue
- if backend_name == device['vm'] and (path == device['device']
- or not path.startswith('/dev/') and path == device[
- 'desc']):
- return {
- "frontend": disk.find('target').get('dev'),
- "vm": vm}
- return None
-
-def device_attach_check(vm, backend_vm, device, frontend, mode):
- """ Checks all the parameters, dies on errors """
- if not vm.is_running():
- raise QubesException("VM %s not running" % vm.name)
-
- if not backend_vm.is_running():
- raise QubesException("VM %s not running" % backend_vm.name)
-
- if device['mode'] == 'r' and mode == 'w':
- raise QubesException("Cannot attach read-only device in read-write "
- "mode")
-
-def block_attach(qvmc, vm, device, frontend=None, mode="w", auto_detach=False, wait=True):
- backend_vm = qvmc.get_vm_by_name(device['vm'])
- device_attach_check(vm, backend_vm, device, frontend, mode)
- if frontend is None:
- frontend = block_find_unused_frontend(vm)
- if frontend is None:
- raise QubesException("No unused frontend found")
- else:
- # Check if any device attached at this frontend
- xml = vm.libvirt_domain.XMLDesc()
- parsed_xml = etree.fromstring(xml)
- disks = parsed_xml.xpath("//domain/devices/disk/target[@dev='%s']" %
- frontend)
- if len(disks):
- raise QubesException("Frontend %s busy in VM %s, detach it first" % (frontend, vm.name))
-
- # Check if this device is attached to some domain
- attached_vm = block_check_attached(qvmc, device)
- if attached_vm:
- if auto_detach:
- block_detach(attached_vm['vm'], attached_vm['frontend'])
- else:
- raise QubesException("Device %s from %s already connected to VM "
- "%s as %s" % (device['device'],
- backend_vm.name, attached_vm['vm'], attached_vm['frontend']))
-
- disk = Element("disk")
- disk.set('type', 'block')
- disk.set('device', 'disk')
- SubElement(disk, 'driver').set('name', 'phy')
- SubElement(disk, 'source').set('dev', device['device'])
- SubElement(disk, 'target').set('dev', frontend)
- if backend_vm.qid != 0:
- SubElement(disk, 'backenddomain').set('name', device['vm'])
- if mode == "r":
- SubElement(disk, 'readonly')
- vm.libvirt_domain.attachDevice(etree.tostring(disk, encoding='utf-8'))
- try:
- # trigger watches to update device status
- # FIXME: this should be removed once libvirt will report such
- # events itself
- vm.qdb.write('/qubes-block-devices', '')
- except Error:
- pass
-
-def block_detach(vm, frontend = "xvdi"):
-
- xml = vm.libvirt_domain.XMLDesc()
- parsed_xml = etree.fromstring(xml)
- attached = parsed_xml.xpath("//domain/devices/disk")
- for disk in attached:
- if frontend is not None and disk.find('target').get('dev') != frontend:
- # Not the device we are looking for
- continue
- if frontend is None:
- # ignore system disks
- if disk.find('domain') == None and \
- disk.find('source').get('dev').startswith(system_path[
- "qubes_base_dir"]):
- continue
- vm.libvirt_domain.detachDevice(etree.tostring(disk, encoding='utf-8'))
- try:
- # trigger watches to update device status
- # FIXME: this should be removed once libvirt will report such
- # events itself
- vm.qdb.write('/qubes-block-devices', '')
- except Error:
- pass
-
-def block_detach_all(vm):
- """ Detach all non-system devices"""
-
- block_detach(vm, None)
-
-####### USB devices ######
-
-usb_ver_re = re.compile(r"^(1|2)$")
-usb_device_re = re.compile(r"^[0-9]+-[0-9]+(_[0-9]+)?$")
-usb_port_re = re.compile(r"^$|^[0-9]+-[0-9]+(\.[0-9]+)?$")
-usb_desc_re = re.compile(r"^[ -~]{1,255}$")
-# should match valid VM name
-usb_connected_to_re = re.compile(r"^[a-zA-Z][a-zA-Z0-9_.-]*$")
-
-def usb_decode_device_from_qdb(qdb_encoded_device):
- """ recover actual device name (xenstore doesn't allow dot in key names, so it was translated to underscore) """
- return qdb_encoded_device.replace('_', '.')
-
-def usb_encode_device_for_qdb(device):
- """ encode actual device name (xenstore doesn't allow dot in key names, so translated it into underscore) """
- return device.replace('.', '_')
-
-def usb_list_vm(qvmc, vm):
- if not vm.is_running():
- return {}
-
- try:
- untrusted_devices = vm.qdb.multiread('/qubes-usb-devices/')
- except Error:
- vm.refresh()
- return {}
-
- def get_dev_item(dev, item):
- return untrusted_devices.get(
- '/qubes-usb-devices/%s/%s' % (dev, item),
- None)
-
- devices = {}
-
- untrusted_devices_names = list(set(map(lambda x: x.split("/")[2],
- untrusted_devices.keys())))
- for untrusted_dev_name in untrusted_devices_names:
- if usb_device_re.match(untrusted_dev_name):
- dev_name = untrusted_dev_name
- untrusted_device_desc = get_dev_item(dev_name, 'desc')
- if not usb_desc_re.match(untrusted_device_desc):
- print >> sys.stderr, "Invalid %s device desc in VM '%s'" % (
- dev_name, vm.name)
- continue
- device_desc = untrusted_device_desc
-
- untrusted_connected_to = get_dev_item(dev_name, 'connected-to')
- if untrusted_connected_to:
- if not usb_connected_to_re.match(untrusted_connected_to):
- print >>sys.stderr, \
- "Invalid %s device 'connected-to' in VM '%s'" % (
- dev_name, vm.name)
- continue
- connected_to = qvmc.get_vm_by_name(untrusted_connected_to)
- if connected_to is None:
- print >>sys.stderr, \
- "Device {} appears to be connected to {}, " \
- "but such VM doesn't exist".format(
- dev_name, untrusted_connected_to)
- else:
- connected_to = None
-
- device = usb_decode_device_from_qdb(dev_name)
-
- full_name = vm.name + ':' + device
-
- devices[full_name] = {
- 'vm': vm,
- 'device': device,
- 'qdb_path': '/qubes-usb-devices/' + dev_name,
- 'name': full_name,
- 'desc': device_desc,
- 'connected-to': connected_to,
- }
- return devices
-
-
-def usb_list(qvmc, vm=None):
- """
- Returns a dictionary of USB devices (for PVUSB backends running in all VM).
- The dictionary is keyed by 'name' (see below), each element is a dictionary itself:
- vm = backend domain object
- device = device ID
- name = :
- desc = description
- """
- if vm is not None:
- if not vm.is_running():
- return {}
- else:
- vm_list = [vm]
- else:
- vm_list = qvmc.values()
-
- devices_list = {}
- for vm in vm_list:
- devices_list.update(usb_list_vm(qvmc, vm))
- return devices_list
-
-def usb_check_attached(qvmc, device):
- """Reread device attachment status"""
- vm = device['vm']
- untrusted_connected_to = vm.qdb.read(
- '{}/connected-to'.format(device['qdb_path']))
- if untrusted_connected_to:
- if not usb_connected_to_re.match(untrusted_connected_to):
- raise QubesException(
- "Invalid %s device 'connected-to' in VM '%s'" % (
- device['device'], vm.name))
- connected_to = qvmc.get_vm_by_name(untrusted_connected_to)
- if connected_to is None:
- print >>sys.stderr, \
- "Device {} appears to be connected to {}, " \
- "but such VM doesn't exist".format(
- device['device'], untrusted_connected_to)
- else:
- connected_to = None
- return connected_to
-
-def usb_attach(qvmc, vm, device, auto_detach=False, wait=True):
- if not vm.is_running():
- raise QubesException("VM {} not running".format(vm.name))
-
- if not device['vm'].is_running():
- raise QubesException("VM {} not running".format(device['vm'].name))
-
- connected_to = usb_check_attached(qvmc, device)
- if connected_to:
- if auto_detach:
- usb_detach(qvmc, device)
- else:
- raise QubesException("Device {} already connected, to {}".format(
- device['name'], connected_to
- ))
-
- # set qrexec policy to allow this device
- policy_line = '{} {} allow\n'.format(vm.name, device['vm'].name)
- policy_path = '/etc/qubes-rpc/policy/qubes.USB+{}'.format(device['device'])
- policy_exists = os.path.exists(policy_path)
- if not policy_exists:
- try:
- fd = os.open(policy_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
- with os.fdopen(fd, 'w') as f:
- f.write(policy_line)
- except OSError as e:
- if e.errno == errno.EEXIST:
- pass
- else:
- raise
- else:
- with open(policy_path, 'r+') as f:
- policy = f.readlines()
- policy.insert(0, policy_line)
- f.truncate(0)
- f.seek(0)
- f.write(''.join(policy))
- try:
- # and actual attach
- p = vm.run_service('qubes.USBAttach', passio_popen=True, user='root')
- (stdout, stderr) = p.communicate(
- '{} {}\n'.format(device['vm'].name, device['device']))
- if p.returncode == 127:
- raise USBProxyNotInstalled(
- "qubes-usb-proxy not installed in the VM")
- elif p.returncode != 0:
- # TODO: sanitize and include stdout
- sanitized_stderr = ''.join([c for c in stderr if ord(c) >= 0x20])
- raise QubesException('Device attach failed: {}'.format(
- sanitized_stderr))
- finally:
- # FIXME: there is a race condition here - some other process might
- # modify the file in the meantime. This may result in unexpected
- # denials, but will not allow too much
- if not policy_exists:
- os.unlink(policy_path)
- else:
- with open(policy_path, 'r+') as f:
- policy = f.readlines()
- policy.remove('{} {} allow\n'.format(vm.name, device['vm'].name))
- f.truncate(0)
- f.seek(0)
- f.write(''.join(policy))
-
-def usb_detach(qvmc, vm, device):
- connected_to = usb_check_attached(qvmc, device)
- # detect race conditions; there is still race here, but much smaller
- if connected_to is None or connected_to.qid != vm.qid:
- raise QubesException(
- "Device {} not connected to VM {}".format(
- device['name'], vm.name))
-
- p = device['vm'].run_service('qubes.USBDetach', passio_popen=True,
- user='root')
- (stdout, stderr) = p.communicate(
- '{}\n'.format(device['device']))
- if p.returncode != 0:
- # TODO: sanitize and include stdout
- raise QubesException('Device detach failed')
-
-def usb_detach_all(qvmc, vm):
- for dev in usb_list(qvmc).values():
- connected_to = dev['connected-to']
- if connected_to is not None and connected_to.qid == vm.qid:
- usb_detach(qvmc, connected_to, dev)
-
-####### QubesWatch ######
-
-def only_in_first_list(l1, l2):
- ret=[]
- for i in l1:
- if not i in l2:
- ret.append(i)
- return ret
-
-class QubesWatch(object):
- def __init__(self):
- self._qdb = {}
- self._qdb_events = {}
- self.block_callback = None
- self.meminfo_callback = None
- self.domain_callback = None
- libvirt.virEventRegisterDefaultImpl()
- # open new libvirt connection because above
- # virEventRegisterDefaultImpl is in practice effective only for new
- # connections
- self.libvirt_conn = libvirt.open(defaults['libvirt_uri'])
- self.libvirt_conn.domainEventRegisterAny(
- None,
- libvirt.VIR_DOMAIN_EVENT_ID_LIFECYCLE,
- self._domain_list_changed, None)
- self.libvirt_conn.domainEventRegisterAny(
- None,
- libvirt.VIR_DOMAIN_EVENT_ID_DEVICE_REMOVED,
- self._device_removed, None)
- # TODO: device attach libvirt event
- for vm in vmm.libvirt_conn.listAllDomains():
- try:
- if vm.isActive():
- self._register_watches(vm)
- except libvirt.libvirtError as e:
- # this will happen if we loose a race with another tool,
- # which can just remove the domain
- if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
- pass
- else:
- raise
- # and for dom0
- self._register_watches(None)
-
- def _qdb_handler(self, watch, fd, events, domain_name):
- try:
- path = self._qdb[domain_name].read_watch()
- except DisconnectedError:
- libvirt.virEventRemoveHandle(watch)
- del(self._qdb_events[domain_name])
- self._qdb[domain_name].close()
- del(self._qdb[domain_name])
- return
- if path.startswith('/qubes-block-devices'):
- if self.block_callback is not None:
- self.block_callback(domain_name)
-
-
- def setup_block_watch(self, callback):
- self.block_callback = callback
-
- def setup_meminfo_watch(self, callback):
- raise NotImplementedError
-
- def setup_domain_watch(self, callback):
- self.domain_callback = callback
-
- def get_meminfo_key(self, xid):
- return '/local/domain/%s/memory/meminfo' % xid
-
- def _register_watches(self, libvirt_domain):
- if libvirt_domain and libvirt_domain.ID() == 0:
- # don't use libvirt object for dom0, to always have the same
- # hardcoded "dom0" name
- libvirt_domain = None
- if libvirt_domain:
- name = libvirt_domain.name()
- if name in self._qdb:
- return
- if not libvirt_domain.isActive():
- return
- # open separate connection to Qubes DB:
- # 1. to not confuse pull() with responses to real commands sent from
- # other threads (like read, write etc) with watch events
- # 2. to not think whether QubesDB is thread-safe (it isn't)
- try:
- self._qdb[name] = QubesDB(name)
- except Error as e:
- if e.args[0] != 2:
- raise
- libvirt.virEventAddTimeout(500, self._retry_register_watches,
- libvirt_domain)
- return
- else:
- name = "dom0"
- if name in self._qdb:
- return
- self._qdb[name] = QubesDB(name)
- try:
- self._qdb[name].watch('/qubes-block-devices')
- except Error as e:
- if e.args[0] == 102: # Connection reset by peer
- # QubesDB daemon not running - most likely we've connected to
- # stale daemon which just exited; retry later
- libvirt.virEventAddTimeout(500, self._retry_register_watches,
- libvirt_domain)
- return
- self._qdb_events[name] = libvirt.virEventAddHandle(
- self._qdb[name].watch_fd(),
- libvirt.VIR_EVENT_HANDLE_READABLE,
- self._qdb_handler, name)
-
- def _retry_register_watches(self, timer, libvirt_domain):
- libvirt.virEventRemoveTimeout(timer)
- self._register_watches(libvirt_domain)
-
- def _unregister_watches(self, libvirt_domain):
- if libvirt_domain and libvirt_domain.ID() == 0:
- name = "dom0"
- else:
- name = libvirt_domain.name()
- if name in self._qdb_events:
- libvirt.virEventRemoveHandle(self._qdb_events[name])
- del(self._qdb_events[name])
- if name in self._qdb:
- self._qdb[name].close()
- del(self._qdb[name])
-
- def _domain_list_changed(self, conn, domain, event, reason, param):
- # use VIR_DOMAIN_EVENT_RESUMED instead of VIR_DOMAIN_EVENT_STARTED to
- # make sure that qubesdb daemon is already running
- if event == libvirt.VIR_DOMAIN_EVENT_RESUMED:
- self._register_watches(domain)
- elif event == libvirt.VIR_DOMAIN_EVENT_STOPPED:
- self._unregister_watches(domain)
- else:
- # ignore other events for now
- return None
- if self.domain_callback:
- self.domain_callback(name=domain.name(), uuid=domain.UUID())
-
- def _device_removed(self, conn, domain, device, param):
- if self.block_callback is not None:
- self.block_callback(domain.name())
-
- def watch_loop(self):
- while True:
- libvirt.virEventRunDefaultImpl()
-
-##### updates check #####
-
-UPDATES_DOM0_DISABLE_FLAG='/var/lib/qubes/updates/disable-updates'
-UPDATES_DEFAULT_VM_DISABLE_FLAG=\
- '/var/lib/qubes/updates/vm-default-disable-updates'
-
-def updates_vms_toggle(qvm_collection, value):
- # Flag for new VMs
- if value:
- if os.path.exists(UPDATES_DEFAULT_VM_DISABLE_FLAG):
- os.unlink(UPDATES_DEFAULT_VM_DISABLE_FLAG)
- else:
- open(UPDATES_DEFAULT_VM_DISABLE_FLAG, "w").close()
-
- # Change for existing VMs
- for vm in qvm_collection.values():
- if vm.qid == 0:
- continue
- if value:
- vm.services.pop('qubes-update-check', None)
- if vm.is_running():
- try:
- vm.run("systemctl start qubes-update-check.timer",
- user="root")
- except:
- pass
- else:
- vm.services['qubes-update-check'] = False
- if vm.is_running():
- try:
- vm.run("systemctl stop qubes-update-check.timer",
- user="root")
- except:
- pass
-def updates_dom0_toggle(qvm_collection, value):
- if value:
- if os.path.exists(UPDATES_DOM0_DISABLE_FLAG):
- os.unlink(UPDATES_DOM0_DISABLE_FLAG)
- else:
- open(UPDATES_DOM0_DISABLE_FLAG, "w").close()
-
-def updates_dom0_status(qvm_collection):
- return not os.path.exists(UPDATES_DOM0_DISABLE_FLAG)
-
-def updates_vms_status(qvm_collection):
- # default value:
- status = not os.path.exists(UPDATES_DEFAULT_VM_DISABLE_FLAG)
- # check if all the VMs uses the default value
- for vm in qvm_collection.values():
- if vm.qid == 0:
- continue
- if vm.services.get('qubes-update-check', True) != status:
- # "mixed"
- return None
- return status
-
-# vim:sw=4:et:
diff --git a/core/settings-xen-Linux.py b/core/settings-xen-Linux.py
deleted file mode 100644
index c413e8ae..00000000
--- a/core/settings-xen-Linux.py
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/usr/bin/python2
-
-from __future__ import absolute_import
-
-from qubes.storage.xen import XenStorage, XenPool
-
-
-def apply(system_path, vm_files, defaults):
- defaults['storage_class'] = XenStorage
- defaults['pool_drivers'] = {'xen': XenPool}
- defaults['pool_config'] = {'dir_path': '/var/lib/qubes/'}
diff --git a/core/storage/Makefile b/core/storage/Makefile
deleted file mode 100644
index 7c7af60e..00000000
--- a/core/storage/Makefile
+++ /dev/null
@@ -1,24 +0,0 @@
-OS ?= Linux
-
-SYSCONFDIR ?= /etc
-PYTHON_QUBESPATH = $(PYTHON_SITEPATH)/qubes
-
-all:
- python -m compileall .
- python -O -m compileall .
-
-install:
-ifndef PYTHON_SITEPATH
- $(error PYTHON_SITEPATH not defined)
-endif
- mkdir -p $(DESTDIR)$(PYTHON_QUBESPATH)/storage
- cp __init__.py $(DESTDIR)$(PYTHON_QUBESPATH)/storage
- cp __init__.py[co] $(DESTDIR)$(PYTHON_QUBESPATH)/storage
- mkdir -p $(DESTDIR)$(SYSCONFDIR)/qubes
- cp storage.conf $(DESTDIR)$(SYSCONFDIR)/qubes/
-ifneq ($(BACKEND_VMM),)
- if [ -r $(BACKEND_VMM).py ]; then \
- cp $(BACKEND_VMM).py $(DESTDIR)$(PYTHON_QUBESPATH)/storage && \
- cp $(BACKEND_VMM).py[co] $(DESTDIR)$(PYTHON_QUBESPATH)/storage; \
- fi
-endif
diff --git a/core/storage/__init__.py b/core/storage/__init__.py
deleted file mode 100644
index b5a744aa..00000000
--- a/core/storage/__init__.py
+++ /dev/null
@@ -1,446 +0,0 @@
-#!/usr/bin/python2
-#
-# The Qubes OS Project, http://www.qubes-os.org
-#
-# Copyright (C) 2013 Marek Marczykowski
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-#
-
-from __future__ import absolute_import
-
-import ConfigParser
-import os
-import os.path
-import shutil
-import subprocess
-import sys
-
-import qubes.qubesutils
-from qubes.qubes import QubesException, defaults, system_path
-
-CONFIG_FILE = '/etc/qubes/storage.conf'
-
-
-class QubesVmStorage(object):
- """
- Class for handling VM virtual disks. This is base class for all other
- implementations, mostly with Xen on Linux in mind.
- """
-
- def __init__(self, vm,
- private_img_size = None,
- root_img_size = None,
- modules_img = None,
- modules_img_rw = False):
- self.vm = vm
- self.vmdir = vm.dir_path
- if private_img_size:
- self.private_img_size = private_img_size
- else:
- self.private_img_size = defaults['private_img_size']
- if root_img_size:
- self.root_img_size = root_img_size
- else:
- self.root_img_size = defaults['root_img_size']
-
- self.root_dev = "xvda"
- self.private_dev = "xvdb"
- self.volatile_dev = "xvdc"
- self.modules_dev = "xvdd"
-
- # For now compute this path still in QubesVm
- self.modules_img = modules_img
- self.modules_img_rw = modules_img_rw
-
- # Additional drive (currently used only by HVM)
- self.drive = None
-
- def format_disk_dev(self, path, script, vdev, rw=True, type="disk",
- domain=None):
- if path is None:
- return ''
- template = " \n" \
- " \n" \
- " \n" \
- " \n" \
- "{params}" \
- " \n"
- params = ""
- if not rw:
- params += " \n"
- if domain:
- params += " \n" % domain
- if script:
- params += " \n" % script
- return template.format(path=path, vdev=vdev, type=type, params=params)
-
- def get_config_params(self):
- args = {}
- args['rootdev'] = self.root_dev_config()
- args['privatedev'] = self.private_dev_config()
- args['volatiledev'] = self.volatile_dev_config()
- args['otherdevs'] = self.other_dev_config()
-
- return args
-
- def root_dev_config(self):
- raise NotImplementedError
-
- def private_dev_config(self):
- raise NotImplementedError
-
- def volatile_dev_config(self):
- raise NotImplementedError
-
- def other_dev_config(self):
- if self.modules_img is not None:
- return self.format_disk_dev(self.modules_img,
- None,
- self.modules_dev,
- self.modules_img_rw)
- elif self.drive is not None:
- (drive_type, drive_domain, drive_path) = self.drive.split(":")
- if drive_type == "hd":
- drive_type = "disk"
-
- writable = False
- if drive_type == "disk":
- writable = True
-
- if drive_domain.lower() == "dom0":
- drive_domain = None
-
- return self.format_disk_dev(drive_path, None,
- self.modules_dev,
- rw=writable,
- type=drive_type,
- domain=drive_domain)
- else:
- return ''
-
- def _copy_file(self, source, destination):
- """
- Effective file copy, preserving sparse files etc.
- """
- # TODO: Windows support
-
- # We prefer to use Linux's cp, because it nicely handles sparse files
- retcode = subprocess.call (["cp", "--reflink=auto", source, destination])
- if retcode != 0:
- raise IOError ("Error while copying {0} to {1}".\
- format(source, destination))
-
- def get_disk_utilization(self):
- return qubes.qubesutils.get_disk_usage(self.vmdir)
-
- def get_disk_utilization_private_img(self):
- return qubes.qubesutils.get_disk_usage(self.private_img)
-
- def get_private_img_sz(self):
- if not os.path.exists(self.private_img):
- return 0
-
- return os.path.getsize(self.private_img)
-
- def resize_private_img(self, size):
- raise NotImplementedError
-
- def create_on_disk_private_img(self, verbose, source_template = None):
- raise NotImplementedError
-
- def create_on_disk_root_img(self, verbose, source_template = None):
- raise NotImplementedError
-
- def create_on_disk(self, verbose, source_template = None):
- if source_template is None:
- source_template = self.vm.template
-
- old_umask = os.umask(002)
- if verbose:
- print >> sys.stderr, "--> Creating directory: {0}".format(self.vmdir)
- os.mkdir (self.vmdir)
-
- self.create_on_disk_private_img(verbose, source_template)
- self.create_on_disk_root_img(verbose, source_template)
- self.reset_volatile_storage(verbose, source_template)
-
- os.umask(old_umask)
-
- def clone_disk_files(self, src_vm, verbose):
- if verbose:
- print >> sys.stderr, "--> Creating directory: {0}".format(self.vmdir)
- os.mkdir (self.vmdir)
-
- if src_vm.private_img is not None and self.private_img is not None:
- if verbose:
- print >> sys.stderr, "--> Copying the private image:\n{0} ==>\n{1}".\
- format(src_vm.private_img, self.private_img)
- self._copy_file(src_vm.private_img, self.private_img)
-
- if src_vm.updateable and src_vm.root_img is not None and self.root_img is not None:
- if verbose:
- print >> sys.stderr, "--> Copying the root image:\n{0} ==>\n{1}".\
- format(src_vm.root_img, self.root_img)
- self._copy_file(src_vm.root_img, self.root_img)
-
- # TODO: modules?
-
- def rename(self, old_name, new_name):
- old_vmdir = self.vmdir
- new_vmdir = os.path.join(os.path.dirname(self.vmdir), new_name)
- os.rename(self.vmdir, new_vmdir)
- self.vmdir = new_vmdir
- if self.private_img:
- self.private_img = self.private_img.replace(old_vmdir, new_vmdir)
- if self.root_img:
- self.root_img = self.root_img.replace(old_vmdir, new_vmdir)
- if self.volatile_img:
- self.volatile_img = self.volatile_img.replace(old_vmdir, new_vmdir)
-
- def verify_files(self):
- if not os.path.exists (self.vmdir):
- raise QubesException (
- "VM directory doesn't exist: {0}".\
- format(self.vmdir))
-
- if self.root_img and not os.path.exists (self.root_img):
- raise QubesException (
- "VM root image file doesn't exist: {0}".\
- format(self.root_img))
-
- if self.private_img and not os.path.exists (self.private_img):
- raise QubesException (
- "VM private image file doesn't exist: {0}".\
- format(self.private_img))
- if self.modules_img is not None:
- if not os.path.exists(self.modules_img):
- raise QubesException (
- "VM kernel modules image does not exists: {0}".\
- format(self.modules_img))
-
- def remove_from_disk(self):
- shutil.rmtree (self.vmdir)
-
- def reset_volatile_storage(self, verbose = False, source_template = None):
- if source_template is None:
- source_template = self.vm.template
-
- # Re-create only for template based VMs
- if source_template is not None and self.volatile_img:
- if os.path.exists(self.volatile_img):
- os.remove(self.volatile_img)
-
- # For StandaloneVM create it only if not already exists (eg after backup-restore)
- if self.volatile_img and not os.path.exists(self.volatile_img):
- if verbose:
- print >> sys.stderr, "--> Creating volatile image: {0}...".\
- format(self.volatile_img)
- subprocess.check_call([system_path["prepare_volatile_img_cmd"],
- self.volatile_img, str(self.root_img_size / 1024 / 1024)])
-
- def prepare_for_vm_startup(self, verbose):
- self.reset_volatile_storage(verbose=verbose)
-
- if self.private_img and not os.path.exists (self.private_img):
- print >>sys.stderr, "WARNING: Creating empty VM private image file: {0}".\
- format(self.private_img)
- self.create_on_disk_private_img(verbose=False)
-
-
-def dump(o):
- """ Returns a string represention of the given object
-
- Args:
- o (object): anything that response to `__module__` and `__class__`
-
- Given the class :class:`qubes.storage.QubesVmStorage` it returns
- 'qubes.storage.QubesVmStorage' as string
- """
- return o.__module__ + '.' + o.__class__.__name__
-
-
-def load(string):
- """ Given a dotted full module string representation of a class it loads it
-
- Args:
- string (str) i.e. 'qubes.storage.xen.QubesXenVmStorage'
-
- Returns:
- type
-
- See also:
- :func:`qubes.storage.dump`
- """
- if not type(string) is str:
- # This is a hack which allows giving a real class to a vm instead of a
- # string as string_class parameter.
- return string
-
- components = string.split(".")
- module_path = ".".join(components[:-1])
- klass = components[-1:][0]
- module = __import__(module_path, fromlist=[klass])
- return getattr(module, klass)
-
-
-def get_pool(name, vm):
- """ Instantiates the storage for the specified vm """
- config = _get_storage_config_parser()
-
- klass = _get_pool_klass(name, config)
-
- keys = [k for k in config.options(name) if k != 'driver' and k != 'class']
- values = [config.get(name, o) for o in keys]
- config_kwargs = dict(zip(keys, values))
-
- if name == 'default':
- kwargs = defaults['pool_config'].copy()
- kwargs.update(keys)
- else:
- kwargs = config_kwargs
-
- return klass(vm, **kwargs)
-
-
-def pool_exists(name):
- """ Check if the specified pool exists """
- try:
- _get_pool_klass(name)
- return True
- except StoragePoolException:
- return False
-
-def add_pool(name, **kwargs):
- """ Add a storage pool to config."""
- config = _get_storage_config_parser()
- config.add_section(name)
- for key, value in kwargs.iteritems():
- config.set(name, key, value)
- _write_config(config)
-
-def remove_pool(name):
- """ Remove a storage pool from config file. """
- config = _get_storage_config_parser()
- config.remove_section(name)
- _write_config(config)
-
-def _write_config(config):
- with open(CONFIG_FILE, 'w') as configfile:
- config.write(configfile)
-
-def _get_storage_config_parser():
- """ Instantiates a `ConfigParaser` for specified storage config file.
-
- Returns:
- RawConfigParser
- """
- config = ConfigParser.RawConfigParser()
- config.read(CONFIG_FILE)
- return config
-
-
-def _get_pool_klass(name, config=None):
- """ Returns the storage klass for the specified pool.
-
- Args:
- name: The pool name.
- config: If ``config`` is not specified
- `_get_storage_config_parser()` is called.
-
- Returns:
- type: A class inheriting from `QubesVmStorage`
- """
- if config is None:
- config = _get_storage_config_parser()
-
- if not config.has_section(name):
- raise StoragePoolException('Uknown storage pool ' + name)
-
- if config.has_option(name, 'class'):
- klass = load(config.get(name, 'class'))
- elif config.has_option(name, 'driver'):
- pool_driver = config.get(name, 'driver')
- klass = defaults['pool_drivers'][pool_driver]
- else:
- raise StoragePoolException('Uknown storage pool driver ' + name)
- return klass
-
-
-class StoragePoolException(QubesException):
- pass
-
-
-class Pool(object):
- def __init__(self, vm, dir_path):
- assert vm is not None
- assert dir_path is not None
-
- self.vm = vm
- self.dir_path = dir_path
-
- self.create_dir_if_not_exists(self.dir_path)
-
- self.vmdir = self.vmdir_path(vm, self.dir_path)
-
- appvms_path = os.path.join(self.dir_path, 'appvms')
- self.create_dir_if_not_exists(appvms_path)
-
- servicevms_path = os.path.join(self.dir_path, 'servicevms')
- self.create_dir_if_not_exists(servicevms_path)
-
- vm_templates_path = os.path.join(self.dir_path, 'vm-templates')
- self.create_dir_if_not_exists(vm_templates_path)
-
- def vmdir_path(self, vm, pool_dir):
- """ Returns the path to vmdir depending on the type of the VM.
-
- The default QubesOS file storage saves the vm images in three
- different directories depending on the ``QubesVM`` type:
-
- * ``appvms`` for ``QubesAppVm`` or ``QubesHvm``
- * ``vm-templates`` for ``QubesTemplateVm`` or ``QubesTemplateHvm``
- * ``servicevms`` for any subclass of ``QubesNetVm``
-
- Args:
- vm: a QubesVM
- pool_dir: the root directory of the pool
-
- Returns:
- string (str) absolute path to the directory where the vm files
- are stored
- """
- if vm.is_appvm():
- subdir = 'appvms'
- elif vm.is_template():
- subdir = 'vm-templates'
- elif vm.is_netvm():
- subdir = 'servicevms'
- elif vm.is_disposablevm():
- subdir = 'appvms'
- return os.path.join(pool_dir, subdir, vm.template.name + '-dvm')
- else:
- raise QubesException(vm.type() + ' unknown vm type')
-
- return os.path.join(pool_dir, subdir, vm.name)
-
- def create_dir_if_not_exists(self, path):
- """ Check if a directory exists in if not create it.
-
- This method does not create any parent directories.
- """
- if not os.path.exists(path):
- os.mkdir(path)
diff --git a/core/storage/storage.conf b/core/storage/storage.conf
deleted file mode 100644
index e9d067e5..00000000
--- a/core/storage/storage.conf
+++ /dev/null
@@ -1,12 +0,0 @@
-[default] ; poolname
-driver=xen ; the default xen storage
-; class = qubes.storage.xen.XenStorage ; class always overwrites the driver
-;
-; To use our own storage adapter, you need just to specify the module path and
-; class name
-; [pool-b]
-; class = foo.bar.MyStorage
-;
-; [test-dummy]
-; driver=dummy
-
diff --git a/core/storage/xen.py b/core/storage/xen.py
deleted file mode 100644
index 9ea7ba73..00000000
--- a/core/storage/xen.py
+++ /dev/null
@@ -1,240 +0,0 @@
-#!/usr/bin/python2
-#
-# The Qubes OS Project, http://www.qubes-os.org
-#
-# Copyright (C) 2013 Marek Marczykowski
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-#
-
-from __future__ import absolute_import
-
-import os
-import os.path
-import re
-import subprocess
-import sys
-
-from qubes.qubes import QubesException, vm_files
-from qubes.storage import Pool, QubesVmStorage
-
-
-class XenStorage(QubesVmStorage):
- """
- Class for VM storage of Xen VMs.
- """
-
- def __init__(self, vm, vmdir, **kwargs):
- """ Instantiate the storage.
-
- Args:
- vm: a QubesVM
- vmdir: the root directory of the pool
- """
- assert vm is not None
- assert vmdir is not None
-
- super(XenStorage, self).__init__(vm, **kwargs)
-
- self.vmdir = vmdir
-
- if self.vm.is_template():
- self.rootcow_img = os.path.join(self.vmdir,
- vm_files["rootcow_img"])
- else:
- self.rootcow_img = None
-
- self.private_img = os.path.join(vmdir, 'private.img')
- if self.vm.template:
- self.root_img = self.vm.template.root_img
- else:
- self.root_img = os.path.join(vmdir, 'root.img')
- self.volatile_img = os.path.join(vmdir, 'volatile.img')
-
- def root_dev_config(self):
- if self.vm.is_template() and self.rootcow_img:
- return self.format_disk_dev(
- "{root}:{rootcow}".format(
- root=self.root_img, rootcow=self.rootcow_img),
- "block-origin", self.root_dev, True)
- elif self.vm.template and not hasattr(self.vm, 'kernel'):
- # HVM template-based VM - only one device-mapper layer, in dom0 (
- # root+volatile)
- # HVM detection based on 'kernel' property is massive hack,
- # but taken from assumption that VM needs Qubes-specific kernel (
- # actually initramfs) to assemble the second layer of device-mapper
- return self.format_disk_dev(
- "{root}:{volatile}".format(
- root=self.vm.template.storage.root_img,
- volatile=self.volatile_img),
- "block-snapshot", self.root_dev, True)
- elif self.vm.template:
- # any other template-based VM - two device-mapper layers: one
- # in dom0 (here) from root+root-cow, and another one from
- # this+volatile.img
- return self.format_disk_dev(
- "{root}:{rootcow}".format(
- root=self.root_img,
- rootcow=self.vm.template.storage.rootcow_img),
- "block-snapshot", self.root_dev, False)
- else:
- return self.format_disk_dev(
- "{root}".format(root=self.root_img),
- None, self.root_dev, True)
-
- def private_dev_config(self):
- return self.format_disk_dev(self.private_img, None,
- self.private_dev, True)
-
- def volatile_dev_config(self):
- return self.format_disk_dev(self.volatile_img, None,
- self.volatile_dev, True)
-
- def create_on_disk_private_img(self, verbose, source_template = None):
- if source_template:
- template_priv = source_template.private_img
- if verbose:
- print >> sys.stderr, "--> Copying the template's private image: {0}".\
- format(template_priv)
- self._copy_file(template_priv, self.private_img)
- else:
- f_private = open (self.private_img, "a+b")
- f_private.truncate (self.private_img_size)
- f_private.close ()
-
- def create_on_disk_root_img(self, verbose, source_template = None):
- if source_template:
- if not self.vm.updateable:
- # just use template's disk
- return
- else:
- template_root = source_template.root_img
- if verbose:
- print >> sys.stderr, "--> Copying the template's root image: {0}".\
- format(template_root)
-
- self._copy_file(template_root, self.root_img)
- else:
- f_root = open (self.root_img, "a+b")
- f_root.truncate (self.root_img_size)
- f_root.close ()
- if self.vm.is_template():
- self.commit_template_changes()
-
- def rename(self, old_name, new_name):
- super(XenStorage, self).rename(old_name, new_name)
-
- old_dirpath = os.path.join(os.path.dirname(self.vmdir), old_name)
- if self.rootcow_img:
- self.rootcow_img = self.rootcow_img.replace(old_dirpath,
- self.vmdir)
-
- def resize_private_img(self, size):
- f_private = open (self.private_img, "a+b")
- f_private.truncate (size)
- f_private.close ()
-
- # find loop device if any
- p = subprocess.Popen (["sudo", "losetup", "--associated", self.private_img],
- stdout=subprocess.PIPE)
- result = p.communicate()
- m = re.match(r"^(/dev/loop\d+):\s", result[0])
- if m is not None:
- loop_dev = m.group(1)
-
- # resize loop device
- subprocess.check_call(["sudo", "losetup", "--set-capacity", loop_dev])
-
- def commit_template_changes(self):
- assert self.vm.is_template()
- if not self.rootcow_img:
- return
- if os.path.exists (self.rootcow_img):
- os.rename (self.rootcow_img, self.rootcow_img + '.old')
-
- old_umask = os.umask(002)
- f_cow = open (self.rootcow_img, "w")
- f_root = open (self.root_img, "r")
- f_root.seek(0, os.SEEK_END)
- f_cow.truncate (f_root.tell()) # make empty sparse file of the same size as root.img
- f_cow.close ()
- f_root.close()
- os.umask(old_umask)
-
- def reset_volatile_storage(self, verbose = False, source_template = None):
- if source_template is None:
- source_template = self.vm.template
-
- if source_template is not None:
- # template-based VM with only one device-mapper layer -
- # volatile.img used as upper layer on root.img, no root-cow.img
- # intermediate layer
- if not source_template.storage.rootcow_img:
- if os.path.exists(self.volatile_img):
- if self.vm.debug:
- if os.path.getmtime(source_template.storage.root_img)\
- > os.path.getmtime(self.volatile_img):
- if verbose:
- print >>sys.stderr, "--> WARNING: template have changed, resetting root.img"
- else:
- if verbose:
- print >>sys.stderr, "--> Debug mode: not resetting root.img"
- print >>sys.stderr, "--> Debug mode: if you want to force root.img reset, either update template VM, or remove volatile.img file"
- return
- os.remove(self.volatile_img)
-
- f_volatile = open(self.volatile_img, "w")
- f_root = open(source_template.storage.root_img, "r")
- f_root.seek(0, os.SEEK_END)
- f_volatile.truncate(f_root.tell()) # make empty sparse file of the same size as root.img
- f_volatile.close()
- f_root.close()
- return
- super(XenStorage, self).reset_volatile_storage(
- verbose=verbose, source_template=source_template)
-
- def prepare_for_vm_startup(self, verbose):
- super(XenStorage, self).prepare_for_vm_startup(verbose=verbose)
-
- if self.drive is not None:
- (drive_type, drive_domain, drive_path) = self.drive.split(":")
- if drive_domain.lower() != "dom0":
- try:
- # FIXME: find a better way to access QubesVmCollection
- drive_vm = self.vm._collection.get_vm_by_name(drive_domain)
- # prepare for improved QubesVmCollection
- if drive_vm is None:
- raise KeyError
- if not drive_vm.is_running():
- raise QubesException(
- "VM '{}' holding '{}' isn't running".format(
- drive_domain, drive_path))
- except KeyError:
- raise QubesException(
- "VM '{}' holding '{}' does not exists".format(
- drive_domain, drive_path))
- if self.rootcow_img and not os.path.exists(self.rootcow_img):
- self.commit_template_changes()
-
-
-class XenPool(Pool):
-
- def __init__(self, vm, dir_path):
- super(XenPool, self).__init__(vm, dir_path)
-
- def getStorage(self):
- """ Returns an instantiated ``XenStorage``. """
- return XenStorage(self.vm, vmdir=self.vmdir)
diff --git a/core/tests/test_qubesutils.py b/core/tests/test_qubesutils.py
deleted file mode 100644
index caa1593c..00000000
--- a/core/tests/test_qubesutils.py
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/usr/bin/python -O
-
-#
-# The Qubes OS Project, http://www.qubes-os.org
-#
-# Copyright (C) 2014 Wojciech Porczyk
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-
-import subprocess
-import unittest
-
-import qubes.qubesutils
-
-
-class TestCaseFunctionsAndConstants(unittest.TestCase):
- def check_output_int(self, cmd):
- return int(subprocess.check_output(cmd).strip().split(None, 1)[0])
-
- def test_00_BLKSIZE(self):
- # this may fail on systems without st_blocks
- self.assertEqual(qubes.qubesutils.BLKSIZE, self.check_output_int(['stat', '-c%B', '.']))
-
- def test_01_get_size_one(self):
- # this may fail on systems without st_blocks
- self.assertEqual(qubes.qubesutils.get_disk_usage_one(os.stat('.')),
- self.check_output_int(['stat', '-c%b', '.']) * qubes.qubesutils.BLKSIZE)
-
- def test_02_get_size(self):
- self.assertEqual(qubes.qubesutils.get_disk_usage('.'),
- self.check_output_int(['du', '-s', '--block-size=1', '.']))
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/dispvm/.gitignore b/dispvm/.gitignore
deleted file mode 100644
index 9804657b..00000000
--- a/dispvm/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-qubes_restore
-xenstore-watch
diff --git a/dispvm/Makefile b/dispvm/Makefile
deleted file mode 100644
index 9fab7451..00000000
--- a/dispvm/Makefile
+++ /dev/null
@@ -1,21 +0,0 @@
-UNITDIR ?= /usr/lib/systemd/system
-
-all:
- true
-
-clean:
- true
-
-install:
- mkdir -p $(DESTDIR)/etc/xen/scripts
- cp block.qubes $(DESTDIR)/etc/xen/scripts
- mkdir -p $(DESTDIR)/usr/bin $(DESTDIR)/usr/lib/qubes
- cp qubes-prepare-saved-domain.sh $(DESTDIR)/usr/lib/qubes
- cp qubes-update-dispvm-savefile-with-progress.sh $(DESTDIR)/usr/lib/qubes
- cp qfile-daemon-dvm $(DESTDIR)/usr/lib/qubes
- mkdir -p $(DESTDIR)$(UNITDIR)
- cp startup-dvm.sh $(DESTDIR)/usr/lib/qubes
- cp qubes-setupdvm.service $(DESTDIR)$(UNITDIR)
-
-
-
diff --git a/dispvm/block.qubes b/dispvm/block.qubes
deleted file mode 100755
index 8f35cafc..00000000
--- a/dispvm/block.qubes
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/bin/bash
-
-HOTPLUG_STORE="/var/run/xen-hotplug/${XENBUS_PATH//\//-}"
-
-hd_arr[10]=a
-hd_arr[11]=b
-hd_arr[12]=c
-hd_arr[13]=d
-hd_arr[14]=e
-hd_arr[15]=f
-
-hexdigit()
-{
- if [ $1 -lt 10 ] ; then
- RET=$1
- else
- RET=${hd_arr[$1]}
- fi
-}
-
-hexnumber()
-{
- hexdigit $(($1/16))
- ret2=$RET
- hexdigit $(($1%16))
- HEXNUMBER="$ret2"$RET
-}
-
-
-process()
-{
- if ! [ "x""$1" = "xfile" ] ; then
- exec flock /var/run/qubes/hotplug-block /etc/xen/scripts/block $ORIG_ARGS
- fi
- while true ; do
- dev=$(losetup -f --show $2)
- if [ -n "$dev" ] ; then break ; fi
- done
- hexnumber ${dev:9:70}
- xenstore-write "$XENBUS_PATH/node" "$dev" \
- "$XENBUS_PATH/physical-device" "7:"$HEXNUMBER \
- "$XENBUS_PATH/hotplug-status" connected
- echo "$dev" > "$HOTPLUG_STORE-node"
- echo "file" > "$HOTPLUG_STORE-type"
-}
-
-#exec 2>>/tmp/block.$$
-#set -x
-export PATH="/sbin:/bin:/usr/bin:/usr/sbin:$PATH"
-
-XENBUS_PATH="${XENBUS_PATH:?}"
-if ! [ "$1" = "add" ] || ! [ -f /var/run/qubes/fast-block-attach ] ; then
- script=$(xenstore-read "$XENBUS_PATH/script")
- exec flock /var/run/qubes/hotplug-block $script "$@"
-fi
-
-ORIG_ARGS="$@"
-
-vars=$(xenstore-read "$XENBUS_PATH/type" "$XENBUS_PATH/params")
-process $vars
-exit 0
diff --git a/dispvm/qfile-daemon-dvm b/dispvm/qfile-daemon-dvm
deleted file mode 100755
index 209e2228..00000000
--- a/dispvm/qfile-daemon-dvm
+++ /dev/null
@@ -1,200 +0,0 @@
-#!/usr/bin/python2
-# coding=utf-8
-#
-# The Qubes OS Project, http://www.qubes-os.org
-#
-# Copyright (C) 2010 Rafal Wojtczuk
-# Copyright (C) 2013-2015 Marek Marczykowski-Górecki
-#
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-#
-import os
-import subprocess
-import sys
-import shutil
-import time
-
-from qubes.qubes import QubesVmCollection, QubesException
-from qubes.qubes import QubesDispVmLabels
-from qubes.notify import tray_notify, tray_notify_error, tray_notify_init
-
-
-current_savefile = '/var/run/qubes/current-savefile'
-current_savefile_vmdir = '/var/lib/qubes/dvmdata/vmdir'
-
-
-class QfileDaemonDvm:
- def __init__(self, name):
- self.name = name
-
- @staticmethod
- def get_disp_templ():
- vmdir = os.readlink(current_savefile_vmdir)
- return vmdir.split('/')[-1]
-
- def do_get_dvm(self):
- tray_notify("Starting new DispVM...", "red")
-
- qvm_collection = QubesVmCollection()
- qvm_collection.lock_db_for_writing()
- try:
-
- tar_process = subprocess.Popen(
- ['bsdtar', '-C', current_savefile_vmdir,
- '-xSUf', os.path.join(current_savefile_vmdir, 'saved-cows.tar')])
-
- qvm_collection.load()
- print >>sys.stderr, "time=%s, collection loaded" % (str(time.time()))
-
- vm = qvm_collection.get_vm_by_name(self.name)
- if vm is None:
- sys.stderr.write('Domain ' + self.name + ' does not exist ?')
- return None
- label = vm.label
- if len(sys.argv) > 4 and len(sys.argv[4]) > 0:
- assert sys.argv[4] in QubesDispVmLabels.keys(), "Invalid label"
- label = QubesDispVmLabels[sys.argv[4]]
- disp_templ = self.get_disp_templ()
- vm_disptempl = qvm_collection.get_vm_by_name(disp_templ)
- if vm_disptempl is None:
- sys.stderr.write('Domain ' + disp_templ + ' does not exist ?')
- return None
- dispvm = qvm_collection.add_new_vm('QubesDisposableVm',
- disp_template=vm_disptempl,
- label=label)
- print >>sys.stderr, "time=%s, VM created" % (str(time.time()))
- # By default inherit firewall rules from calling VM
- disp_firewall_conf = '/var/run/qubes/%s-firewall.xml' % dispvm.name
- dispvm.firewall_conf = disp_firewall_conf
- if os.path.exists(vm.firewall_conf):
- shutil.copy(vm.firewall_conf, disp_firewall_conf)
- elif vm.qid == 0 and os.path.exists(vm_disptempl.firewall_conf):
- # for DispVM called from dom0, copy use rules from DispVM template
- shutil.copy(vm_disptempl.firewall_conf, disp_firewall_conf)
- if len(sys.argv) > 5 and len(sys.argv[5]) > 0:
- assert os.path.exists(sys.argv[5]), "Invalid firewall.conf location"
- dispvm.firewall_conf = sys.argv[5]
- if vm.qid != 0:
- dispvm.uses_default_netvm = False
- # netvm can be changed before restore,
- # but cannot be enabled/disabled
- if (dispvm.netvm is None) == (vm.dispvm_netvm is None):
- dispvm.netvm = vm.dispvm_netvm
- # Wait for tar to finish
- if tar_process.wait() != 0:
- sys.stderr.write('Failed to unpack saved-cows.tar')
- return None
- print >>sys.stderr, "time=%s, VM starting" % (str(time.time()))
- try:
- dispvm.start()
- except (MemoryError, QubesException) as e:
- tray_notify_error(str(e))
- raise
- if vm.qid != 0:
- # if need to enable/disable netvm, do it while DispVM is alive
- if (dispvm.netvm is None) != (vm.dispvm_netvm is None):
- dispvm.netvm = vm.dispvm_netvm
- print >>sys.stderr, "time=%s, VM started" % (str(time.time()))
- qvm_collection.save()
- finally:
- qvm_collection.unlock_db()
- # Reload firewall rules
- print >>sys.stderr, "time=%s, reloading firewall" % (str(time.time()))
- for vm in qvm_collection.values():
- if vm.is_proxyvm() and vm.is_running():
- vm.write_iptables_qubesdb_entry()
-
- return dispvm
-
- @staticmethod
- def dvm_setup_ok():
- dvmdata_dir = '/var/lib/qubes/dvmdata/'
- if not os.path.isfile(current_savefile):
- return False
- if not os.path.isfile(dvmdata_dir+'default-savefile') or \
- not os.path.isfile(dvmdata_dir+'savefile-root'):
- return False
- dvm_mtime = os.stat(current_savefile).st_mtime
- root_mtime = os.stat(dvmdata_dir+'savefile-root').st_mtime
- if dvm_mtime < root_mtime:
- template_name = os.path.basename(
- os.path.dirname(os.readlink(dvmdata_dir+'savefile-root')))
- if subprocess.call(["xl", "domid", template_name],
- stdout=open(os.devnull, "w")) == 0:
- tray_notify("For optimum performance, you should not "
- "start DispVM when its template is running.", "red")
- return False
- return True
-
- def get_dvm(self):
- if not self.dvm_setup_ok():
- if os.system("/usr/lib/qubes/"
- "qubes-update-dispvm-savefile-with-progress.sh"
- " >/dev/null >sys.stderr, "time=%s, qfile-daemon-dvm init" % (str(time.time()))
- tray_notify_init()
- print >>sys.stderr, "time=%s, creating DispVM" % (str(time.time()))
- qfile = QfileDaemonDvm(src_vmname)
- dispvm = qfile.get_dvm()
- if dispvm is not None:
- if exec_index == "LAUNCH":
- print dispvm.name
- return
-
- print >>sys.stderr, "time=%s, starting VM process" % (str(time.time()))
- subprocess.call(['/usr/lib/qubes/qrexec-client', '-d', dispvm.name,
- user+':exec /usr/lib/qubes/qubes-rpc-multiplexer ' +
- exec_index + " " + src_vmname])
- QfileDaemonDvm.finish_disposable(dispvm.name)
-
-main()
diff --git a/dispvm/qubes-prepare-saved-domain.sh b/dispvm/qubes-prepare-saved-domain.sh
deleted file mode 100755
index d5c3ad96..00000000
--- a/dispvm/qubes-prepare-saved-domain.sh
+++ /dev/null
@@ -1,86 +0,0 @@
-#!/bin/bash
-
-set -o pipefail
-
-get_encoded_script()
-{
- ENCODED_SCRIPT=`
- if [ "$1" == "vm-default" ]; then
- echo /usr/lib/qubes/dispvm-prerun.sh
- else
- cat "$1"
- fi | base64 -w0` || exit 1
-}
-
-if [ $# != 2 -a $# != 3 ] ; then
- echo "usage: $0 domainname savefile_to_be_created [preload script]" >&2
- exit 1
-fi
-export PATH=$PATH:/sbin:/usr/sbin
-if [ $# = 3 ] ; then
- get_encoded_script $3
-fi
-VMDIR=/var/lib/qubes/appvms/$1
-if ! [ -d $VMDIR ] ; then
- echo "$VMDIR does not exist ?" >&2
- exit 1
-fi
-if ! qvm-start $1 --dvm ; then
- exit 1
-fi
-
-ID=`virsh -c xen:/// domid $1`
-echo "Waiting for DVM $1 ..." >&2
-if [ -n "$ENCODED_SCRIPT" ] ; then
- qubesdb-write -d $1 /qubes-save-script "$ENCODED_SCRIPT"
-fi
-#set -x
-qubesdb-write -d $1 /qubes-save-request 1
-qubesdb-watch -d $1 /qubes-used-mem
-qubesdb-read -d $1 /qubes-gateway | \
- cut -d . -f 3 | tr -d "\n" > $VMDIR/netvm-id.txt
-kill `cat /var/run/qubes/guid-running.$ID`
-# FIXME: get connection URI from core scripts
-virsh -c xen:/// detach-disk $1 xvdb
-MEM=$(qubesdb-read -d $1 /qubes-used-mem | grep '^[0-9]\+$' | head -n 1)
-echo "DVM boot complete, memory used=$MEM. Saving image..." >&2
-QMEMMAN_STOP=/var/run/qubes/do-not-membalance
-touch $QMEMMAN_STOP
-virsh -c xen:/// setmem $1 $MEM
-# Add some safety margin
-virsh -c xen:/// setmaxmem $1 $[ $MEM + 1024 ]
-# Stop qubesdb daemon now, so VM can restart it later
-kill `cat /var/run/qubes/qubesdb.$1.pid`
-sleep 1
-touch $2
-if ! virsh -c xen:/// save $1 $2; then
- rm -f $QMEMMAN_STOP
- qvm-kill $1
- exit 1
-fi
-rm -f $QMEMMAN_STOP
-# Do not allow smaller allocation than 400MB. If that small number comes from
-# an error, it would prevent further savefile regeneration (because VM would
-# not start with too little memory). Also 'maxmem' depends on 'memory', so
-# 400MB is sane compromise.
-if [ "$MEM" -lt 409600 ]; then
- qvm-prefs -s $1 memory 400
-else
- qvm-prefs -s $1 memory $[ $MEM / 1024 ]
-fi
-ln -snf $VMDIR /var/lib/qubes/dvmdata/vmdir
-cd $VMDIR
-fstype=`df --output=fstype $VMDIR | tail -n 1`
-if [ "$fstype" = "tmpfs" ]; then
- # bsdtar doesn't work on tmpfs because FS_IOC_FIEMAP ioctl isn't supported
- # there
- tar -cSf saved-cows.tar volatile.img || exit 1
-else
- errors=`bsdtar -cSf saved-cows.tar volatile.img 2>&1`
- if [ -n "$errors" ]; then
- echo "Failed to create saved-cows.tar: $errors" >&2
- rm -f saved-cows.tar
- exit 1
- fi
-fi
-echo "DVM savefile created successfully."
diff --git a/dispvm/qubes-setupdvm.service b/dispvm/qubes-setupdvm.service
deleted file mode 100644
index 4b552f73..00000000
--- a/dispvm/qubes-setupdvm.service
+++ /dev/null
@@ -1,12 +0,0 @@
-[Unit]
-Description=Qubes DispVM startup setup
-After=qubes-core.service
-
-[Service]
-Type=oneshot
-ExecStart=/usr/lib/qubes/startup-dvm.sh
-
-[Install]
-WantedBy=multi-user.target
-# Cover legacy init.d script
-Alias=qubes_setupdvm.service
diff --git a/dispvm/qubes-update-dispvm-savefile-with-progress.sh b/dispvm/qubes-update-dispvm-savefile-with-progress.sh
deleted file mode 100755
index 576ddd7e..00000000
--- a/dispvm/qubes-update-dispvm-savefile-with-progress.sh
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/bin/sh
-
-line1="Please wait (up to 120s) while the DispVM savefile is being updated."
-line2="This only happens when you have updated the template."
-line3="Next time will be much faster."
-
-if [ -n "$KDE_FULL_SESSION" ]; then
- br="
"
-else
- br="
-"
-fi
-notify-send --icon=/usr/share/qubes/icons/qubes.png --expire-time=120000 \
- "Updating default DispVM savefile" "$line1$br$line2$br$line3"
-
-ret=0
-
-rm -f /var/run/qubes/qvm-create-default-dvm.stdout
-if ! qvm-create-default-dvm --used-template --default-script >/var/run/qubes/qvm-create-default-dvm.stdout /var/run/qubes/dispVM.seq
-chown root:qubes /var/run/qubes/dispVM.seq
-chmod 660 /var/run/qubes/dispVM.seq
-DEFAULT=/var/lib/qubes/dvmdata/default-savefile
-# setup DispVM files only when they exists
-if [ -r $DEFAULT ]; then
- if [ -f /var/lib/qubes/dvmdata/dont-use-shm ] ; then
- ln -s $DEFAULT /var/run/qubes/current-savefile
- else
- mkdir -m 770 /dev/shm/qubes
- chown root.qubes /dev/shm/qubes
- cp -a $(readlink $DEFAULT) /dev/shm/qubes/current-savefile
- chown root.qubes /dev/shm/qubes/current-savefile
- chmod 660 /dev/shm/qubes/current-savefile
- ln -s /dev/shm/qubes/current-savefile /var/run/qubes/current-savefile
- fi
-fi
-
diff --git a/doc/.gitignore b/doc/.gitignore
index 10d00b57..63b63ff9 100644
--- a/doc/.gitignore
+++ b/doc/.gitignore
@@ -1 +1,3 @@
-*.gz
+_build
+sandbox.rst
+autoxml.rst
diff --git a/doc/Makefile b/doc/Makefile
index 2caaa691..70ec5821 100644
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -1,30 +1,169 @@
-QVM_DIR=qvm-tools
-QUBES_DIR=qubes-tools
-PANDOC=pandoc -s -f rst -t man
+# Makefile for Sphinx documentation
+#
-QVM_DOCS=$(patsubst %.rst,%.1.gz,$(wildcard $(QVM_DIR)/*.rst))
-QUBES_DOCS=$(patsubst %.rst,%.1.gz,$(wildcard $(QUBES_DIR)/*.rst))
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD ?= sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+DEPEND = autoxml.rst
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
- @echo "make rst=example.rst preview -- generate manpage preview from example.rst"
- @echo "make manpages -- generate manpages"
- @echo "make install -- generate manpages and copy them to /usr/share/man"
-
-install: manpages
- mkdir -p $(DESTDIR)/usr/share/man/man1
- cp $(QVM_DOCS) $(DESTDIR)/usr/share/man/man1/
- cp $(QUBES_DOCS) $(DESTDIR)/usr/share/man/man1/
-
-%.1: %.rst
- $(PANDOC) $< > $@
-
-%.1.gz: %.1
- gzip -f $<
-
-manpages: $(QVM_DOCS) $(QUBES_DOCS) $(VM_DOCS)
-
-preview: $(rst)
- $(PANDOC) $(rst) | groff -mandoc -Tlatin1 | less -R
+ @echo "Please use \`make ' where is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+ @echo
+ @echo " install to generate manpages and copy them to \$$(DESTDIR)/usr/share/man"
clean:
- rm -f $(QVM_DOCS) $(QUBES_DOCS) $(VM_DOCS)
+ -rm -rf $(BUILDDIR)/* $(DEPEND)
+
+html: $(DEPEND)
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml: $(DEPEND)
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml: $(DEPEND)
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle: $(DEPEND)
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json: $(DEPEND)
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp: $(DEPEND)
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp: $(DEPEND)
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/core-admin.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/core-admin.qhc"
+
+devhelp: $(DEPEND)
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/core-admin"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/core-admin"
+ @echo "# devhelp"
+
+epub: $(DEPEND)
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex: $(DEPEND)
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf: $(DEPEND)
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text: $(DEPEND)
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man: $(DEPEND)
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ for file in $(BUILDDIR)/man/*.[12345678]; do \
+ gzip -f $$file; \
+ done
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo: $(DEPEND)
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info: $(DEPEND)
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext: $(DEPEND)
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes: $(DEPEND)
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck: $(DEPEND)
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest: $(DEPEND)
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+
+autoxml.rst: ../relaxng/qubes.rng example.xml
+ $(PYTHON) ../qubes/rngdoc.py $+ > $@
+
+.PHONY: install
+install: man
+ mkdir -p $(DESTDIR)/usr/share/man/man1
+ cp $(BUILDDIR)/man/* $(DESTDIR)/usr/share/man/man1/
diff --git a/doc/README.pvusb b/doc/README.pvusb
deleted file mode 100644
index a2f4e492..00000000
--- a/doc/README.pvusb
+++ /dev/null
@@ -1,69 +0,0 @@
-Dedicated usbvm (optional)
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-In dom0, once:
- qvm-create -l red usbvm
-
- # FIXME: use your own PCI device IDs
- qvm-pci -a usbvm 00:1d.0
- qvm-pci -a usbvm 00:1d.1
- qvm-pci -a usbvm 00:1d.2
- qvm-pci -a usbvm 00:1d.7
-
-After each dom0 reboot:
- qvm-start usbvm
-
-List
-~~~~
-
-In dom0:
- qvm-usb -l
-
-Example output:
- dom0:7-4 0718:061a TDKMedia_Trans-It_Drive_070326AE8AF92D95 (attached to qdvp:0-1)
- dom0:7-5 0b05:1706 ASUS_802.11g_WLAN_Drive (attached to sys-net:0-1)
- dom0:1-1 045e:0084 Microsoft_Basic_Optical_Mouse
- usbvm:4-6 05e3:0723 Generic_USB_Storage (attached to qdvp:1-1)
-
-Attach
-~~~~~~
-
-In dom0:
- qvm-usb -a [--no-auto-detach] :-
-
-Example:
- qvm-usb -a sys-net usbvm:4-1
-
-Detach
-~~~~~~
-
-In dom0:
- qvm-usb -d :-
-
-Example:
- qvm-usb -d sys-net:0-1
-
-Known issues
-~~~~~~~~~~~~
-
-List/attach/detach operations seem to work ok, devices are recognized by the target VM etc.
-But actual usage of the attached devices is unstable at best. In fact the only working device
-I saw was one USB stick (and this only after it took a minute to time out and reset the bus
-couple times). Kernel crashes are normal as well. I have not investigated these issues yet,
-I had similar experience with Marek's scripts.
-
-* System keyboard / mouse are listed and can be detached away
-* Virtual USB devices (ones created by PVUSB frontend) may be listed
-* The installation/configuration is not persistent, not retained between reboots
-* No debugging / logging / audit trail
-* When an attached device is physically unplugged, USB port remains mapped but not displayed
-in the list. If device is plugged back it continues to work. Unlisted device cannot be detached.
-* We are not attaching actual devices, but USB ports (different behavior from VMWare, might be confusing)
-* After device is detached from the frontend and returned back to the backend it is not alwayws usable there
-* Code changing configuration of pvusb fe/be and vusb bind/unbind helper are located
-misc/xl-qvm-usb-attach.py misc/xl-qvm-usb-detach.py misc/vusb-ctl.py. These helpers are
-deployed into the backend domain. The initialization code is qubesutils.py in usb_setup(),
-should probably also be moved into an external helper. Perhaps the functionality of these
-external helpers should be merged into libxl? The is one catch is invokation of vusb helper
-in the backend domain -- now it relies on qubes-specific API.
-* After reboot attached USB devices are not listed by 'qvm-usb -l' until replugged.
diff --git a/doc/_static/qubes.css b/doc/_static/qubes.css
new file mode 100644
index 00000000..3e0078d5
--- /dev/null
+++ b/doc/_static/qubes.css
@@ -0,0 +1,9 @@
+/* http://stackoverflow.com/questions/21027105/how-can-i-have-sphinx-tables-fit-to-width */
+
+table.docutils col {
+ width: auto;
+}
+
+/*
+vim: ts=4 sw=4 et
+*/
diff --git a/doc/_templates/layout.html b/doc/_templates/layout.html
new file mode 100644
index 00000000..efbcf4b7
--- /dev/null
+++ b/doc/_templates/layout.html
@@ -0,0 +1,4 @@
+{% extends '!layout.html' %}
+{% set css_files = css_files + ['_static/qubes.css'] %}
+
+{# vim: ts=4 sw=4 et #}
diff --git a/doc/conf.py b/doc/conf.py
new file mode 100644
index 00000000..f3f5480a
--- /dev/null
+++ b/doc/conf.py
@@ -0,0 +1,279 @@
+# -*- coding: utf-8 -*-
+#
+# core-admin documentation build configuration file, created by
+# sphinx-quickstart on Thu Nov 13 15:02:15 2014.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import os
+import subprocess
+import sys
+import time
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+sys.path.insert(0, os.path.abspath('../'))
+sys.path.insert(1, os.path.abspath('../test-packages'))
+
+# -- General configuration -----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.autosummary',
+ 'sphinx.ext.coverage',
+ 'sphinx.ext.doctest',
+ 'sphinx.ext.graphviz',
+ 'sphinx.ext.inheritance_diagram',
+ 'sphinx.ext.intersphinx',
+ 'sphinx.ext.todo',
+ 'sphinx.ext.viewcode',
+
+ 'qubes.dochelpers',
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+source_encoding = 'utf-8'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'core-admin'
+copyright = u'2010-{}, Invisible Things Lab'.format(time.strftime('%Y'))
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = open('../version').read().strip()
+# The full version, including alpha/beta/rc tags.
+release = subprocess.check_output(['git', 'describe', '--long', '--dirty']).strip().decode()
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+today_fmt = '%d.%m.%Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+autodoc_member_order = 'groupwise'
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#html_theme = 'default'
+html_theme = 'nature'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+html_theme_options = {
+# 'collapsiblesidebar': True,
+}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# " v documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+html_last_updated_fmt = '%d.%m.%Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# html links do not work with svg!
+graphviz_output_format = 'png'
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'core-admin-doc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+ ('index', 'core-admin.tex', u'core-admin Documentation',
+ u'Invisible Things Lab', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+
+# authors should be empty and authors should be specified in each man page,
+# because html builder will omit them
+_man_pages_author = []
+
+man_pages = [
+ ('manpages/qubesd-query', 'qubesd-query',
+ u'Low-level qubesd interrogation tool', _man_pages_author, 1),
+]
+
+if os.path.exists('sandbox.rst'):
+ man_pages.append(('sandbox', 'sandbox',
+ u'Sandbox manpage', 'Sandbox Author', 1))
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output ------------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'core-admin', u'core-admin Documentation',
+ u'Invisible Things Lab', 'core-admin', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {
+ 'python': ('http://docs.python.org/', None)}
diff --git a/doc/example.xml b/doc/example.xml
new file mode 100644
index 00000000..6d63723f
--- /dev/null
+++ b/doc/example.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+ netvm
+
+
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+ 2
+ appvm
+
+
+
+
+ qwe123
+
+
+
+
+
+
diff --git a/doc/index.rst b/doc/index.rst
new file mode 100644
index 00000000..a9c95a9f
--- /dev/null
+++ b/doc/index.rst
@@ -0,0 +1,43 @@
+.. core-admin documentation master file, created by
+ sphinx-quickstart on Thu Nov 13 15:02:15 2014.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+Welcome to core-admin's documentation!
+======================================
+
+This page contains documentation autogenerated from source tree. It includes
+manpages and API documentation. For primary user documentation, see
+`https://wiki.qubes-os.org `_.
+
+.. toctree::
+ :maxdepth: 2
+
+ qubes
+ qubes-vm/index
+ qubes-events
+ qubes-exc
+ qubes-ext
+ qubes-log
+ qubes-mgmt
+ qubes-policy
+ qubes-backup
+ qubes-tools/index
+ qubes-tests
+ qubes-dochelpers
+
+.. toctree::
+ :maxdepth: 1
+
+ libvirt
+ autoxml
+ manpages/index
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
+.. vim: ts=3 sw=3 et
diff --git a/doc/libvirt.rst b/doc/libvirt.rst
new file mode 100644
index 00000000..bd35fe49
--- /dev/null
+++ b/doc/libvirt.rst
@@ -0,0 +1,114 @@
+Custom libvirt config
+=====================
+
+Starting from Qubes OS R4.0, libvirt domain config is generated using jinja
+templates. Those templates can be overridden by the user in a couple of ways.
+A basic knowledge of jinja template language and libvirt xml spec is needed.
+
+.. seealso::
+
+ https://libvirt.org/formatdomain.html
+ Format of the domain XML in libvirt.
+
+ http://jinja.pocoo.org/docs/dev/templates/
+ Template format documentation.
+
+File paths
+----------
+
+In order of increasing precedence: the main template, from which the config is
+generated is :file:`/usr/share/templates/libvirt/xen.xml`).
+The distributor may put a file at
+:file:`/usr/share/qubes/template/xen-dist.xml`) to override this file.
+User may put a file at either
+:file:`/etc/qubes/templates/libvirt/xen-user.xml` or
+:file:`/etc/qubes/templates/libvirt/by-name/.xml`, where ```` is
+full name of the domain. Wildcards are not supported but symlinks are.
+
+Jinja has a concept of template names, which basically is the path below some
+load point, which in Qubes' case is :file:`/etc/qubes/templates` and
+:file:`/usr/share/qubes/templates`. Thus names of those templates are
+respectively ``'libvirt/xen.xml'``, ``'libvirt/xen-dist.xml'``,
+``'libvirt/xen-user.xml'`` and ``'libvirt/by-name/.xml'``.
+This will be important later.
+
+.. note::
+
+ Those who know jinja python API will know that the abovementioned locations
+ aren't the only possibilities. Yes, it's a lie, but a justified one.
+
+What to put in the template
+---------------------------
+
+In principle the user may put anything in the template and there is no attempt
+to constrain the user from doing stupid things. One obvious thing is to copy the
+original config file and make changes.
+
+.. code-block:: jinja
+
+
+ {{ vm.name }}
+ ...
+
+The better way is to inherit from the original template and override any number
+of blocks. This is the point when we need the name of the original template.
+
+.. code-block:: jinja
+
+ {% extends 'libvirt/xen.xml' %}
+ {% block devices %}
+ {{ super() }}
+
+
+
+ {% endblock %}
+
+``{% extends %}`` specifies which template we inherit from. Then you may put any
+block by putting new content inside ``{% block %}{% endblock %}``.
+``{{ super() }}`` is substituted with original content of the block as specified
+in the parent template. Untouched blocks remain as they were.
+
+The example above adds serial device.
+
+Template API
+------------
+
+.. warning::
+
+ This API is provisional and subject to change at the minor releases until
+ further notice. No backwards compatibility is promised.
+
+Globals
+```````
+vm
+ the domain object (instance of subclass of
+ :py:class:`qubes.vm.qubesvm.QubesVM`)
+
+Filters
+```````
+
+No custom filters at the moment.
+
+Blocks in the default template
+``````````````````````````````
+basic
+ Contains ````, ````, ````, ```` and
+ ```` nodes.
+
+os
+ Contents of ```` node.
+
+features
+ Contents of ```` node.
+
+clock
+ Contains the ```` node.
+
+on
+ Contains ```` nodes.
+
+devices
+ Contents of ```` node.
+
+
+.. vim: ts=3 sts=3 sw=3 et
diff --git a/doc/manpages/index.rst b/doc/manpages/index.rst
new file mode 100644
index 00000000..4162df0e
--- /dev/null
+++ b/doc/manpages/index.rst
@@ -0,0 +1,10 @@
+Command line utilities
+======================
+
+Those are manual pages provided for command line tools, just formatted in HTML.
+
+.. toctree::
+ :maxdepth: 1
+ :glob:
+
+ *
diff --git a/doc/manpages/qubes-create.rst b/doc/manpages/qubes-create.rst
new file mode 100644
index 00000000..9a3d2e2d
--- /dev/null
+++ b/doc/manpages/qubes-create.rst
@@ -0,0 +1,38 @@
+.. program:: qubes-create
+
+:program:`qubes-create` -- Create new Qubes OS store.
+=====================================================
+
+This command is the only supported way to create new qubes.xml. It is intended
+to be readable though, so you can probably create it manually if you like.
+
+Synopsis
+--------
+
+:command:`qubes-create` [-h] [--qubesxml *XMLFILE*] [--property *NAME*=*VALUE*]
+
+Options
+-------
+
+.. option:: --help, -h
+
+ show help message and exit
+
+.. option:: --verbose, -v
+
+ Increase verbosity.
+
+.. option:: --quiet, -q
+
+ Decrease verbosity.
+
+.. option:: --property=NAME=VALUE, --prop=NAME=VALUE, -p NAME=VALUE
+
+ On creation, set global property *NAME* to *VALUE*.
+
+Authors
+-------
+
+| Wojtek Porczyk
+
+.. vim: ts=3 sw=3 et tw=80
diff --git a/doc/manpages/qubesd-query.rst b/doc/manpages/qubesd-query.rst
new file mode 100644
index 00000000..396b0e78
--- /dev/null
+++ b/doc/manpages/qubesd-query.rst
@@ -0,0 +1,42 @@
+.. program:: qubesd-query
+
+:program:`qubesd-query` -- low-level qubesd interrogation tool
+==============================================================
+
+Synopsis
+--------
+
+:command:`qubesd-query` [-h] [--connect *PATH*] *SRC* *METHOD* *DEST* [*ARGUMENT*]
+
+Options
+-------
+
+.. option:: --help, -h
+
+ Show the help message and exit.
+
+.. option:: --connect=PATH, -c PATH
+
+ Change path to qubesd UNIX socket from default.
+
+.. option:: --empty, -e
+
+ Send empty payload. Do not attempt to read anything from standard input, but
+ send the request immediately.
+
+Description
+-----------
+
+This tool is used to directly invoke qubesd. The parameters of RPC call shall be
+given as arguments to the command. Payload should be written to standard input.
+Result can be read from standard output.
+
+Authors
+-------
+
+| Joanna Rutkowska
+| Rafal Wojtczuk
+| Marek Marczykowski
+| Wojtek Porczyk
+
+.. vim: ts=3 sw=3 et tw=80
diff --git a/doc/qubes-backup.rst b/doc/qubes-backup.rst
new file mode 100644
index 00000000..d77437e5
--- /dev/null
+++ b/doc/qubes-backup.rst
@@ -0,0 +1,8 @@
+:py:mod:`qubes.backup` -- Backup
+================================
+
+.. automodule:: qubes.backup
+ :members:
+ :show-inheritance:
+
+.. vim: ts=3 sw=3 et
diff --git a/doc/qubes-dochelpers.rst b/doc/qubes-dochelpers.rst
new file mode 100644
index 00000000..0ba92754
--- /dev/null
+++ b/doc/qubes-dochelpers.rst
@@ -0,0 +1,8 @@
+:py:mod:`qubes.dochelpers` -- Helpers for Sphinx documentation
+==============================================================
+
+.. automodule:: qubes.dochelpers
+ :members:
+ :show-inheritance:
+
+.. vim: ts=3 sw=3 et
diff --git a/doc/qubes-events.rst b/doc/qubes-events.rst
new file mode 100644
index 00000000..37aa3573
--- /dev/null
+++ b/doc/qubes-events.rst
@@ -0,0 +1,153 @@
+:py:mod:`qubes.events` -- Qubes events
+======================================
+
+Some objects in qubes (most notably domains) emit events. You may hook them and
+execute your code when particular event is fired. Events in qubes are added
+class-wide -- it is not possible to add event handler to one instance only, you
+have to add handler for whole class.
+
+
+Firing events
+-------------
+
+Events are fired by calling :py:meth:`qubes.events.Emitter.fire_event`. The
+first argument is event name (a string). You can fire any event you wish, the
+names are not checked in any way, however each class' documentation tells what
+standard events will be fired on it. When firing an event, caller may specify
+some optional keyword arguments. Those are dependent on the particular event in
+question -- they are passed as-is to handlers.
+
+Event handlers are fired in reverse method resolution order, that is, first for
+parent class and then for it's child. For each class, first are called handlers
+defined in it's source, then handlers from extensions and last the callers added
+manually.
+
+There is second method, :py:meth:`qubes.events.Emitter.fire_event_pre`, which
+fires events in reverse order. It is suitable for events fired before some
+action is performed. You may at your own responsibility raise exceptions from
+such events to try to prevent such action.
+
+Events handlers may yield values. Those values are aggregated and returned
+to the caller as a list of those values. See below for details.
+
+Handling events
+---------------
+
+There are several ways to handle events. In all cases you supply a callable
+(most likely function or method) that will be called when someone fires the
+event. The first argument passed to the callable will be the object instance on
+which the event was fired and the second one is the event name. The rest are
+passed from :py:meth:`qubes.events.Emitter.fire_event` as described previously.
+One callable can handle more than one event.
+
+The easiest way to hook an event is to use
+:py:func:`qubes.events.handler` decorator.
+
+.. code-block:: python
+
+ import qubes.events
+
+ class MyClass(qubes.events.Emitter):
+ @qubes.events.handler('event1', 'event2')
+ def event_handler(self, event):
+ if event == 'event1':
+ print('Got event 1')
+ elif event == 'event2':
+ print('Got event 2')
+
+ o = MyClass()
+ o.fire_event('event1')
+
+Note that your handler will be called for all instances of this class.
+
+.. TODO: extensions
+.. TODO: add/remove_handler
+
+
+Handling events with variable signature
+---------------------------------------
+
+Some events are specified with variable signature (i.e. they may have different
+number of arguments on each call to handlers). You can write handlers just like
+every other python function with variable signature.
+
+.. code-block:: python
+
+ import qubes
+
+ def on_property_change(subject, event, name, newvalue, oldvalue=None):
+ if oldvalue is None:
+ print('Property {} initialised to {!r}'.format(name, newvalue))
+ else:
+ print('Property {} changed {!r} -> {!r}'.format(name, oldvalue, newvalue))
+
+ app = qubes.Qubes()
+ app.add_handler('property-set:default_netvm')
+
+If you expect :py:obj:`None` to be a reasonable value of the property, you have
+a problem. One way to solve it is to invent your very own, magic
+:py:class:`object` instance.
+
+.. code-block:: python
+
+ import qubes
+
+ MAGIC_NO_VALUE = object()
+ def on_property_change(subject, event, name, newvalue, oldvalue=MAGIC_NO_VALUE):
+ if oldvalue is MAGIC_NO_VALUE:
+ print('Property {} initialised to {!r}'.format(name, newvalue))
+ else:
+ print('Property {} changed {!r} -> {!r}'.format(name, oldvalue, newvalue))
+
+ app = qubes.Qubes()
+ app.add_handler('property-set:default_netvm')
+
+There is no possible way of collision other than intentionally passing this very
+object (not even passing similar featureless ``object()``), because ``is``
+python syntax checks object's :py:meth:`id`\ entity, which will be different for
+each :py:class:`object` instance.
+
+
+Returning values from events
+----------------------------
+
+Some events may be called to collect values from the handlers. For example the
+event ``is-fully-usable`` allows plugins to report a domain as not fully usable.
+Such handlers, instead of returning :py:obj:`None` (which is the default when
+the function does not include ``return`` statement), should return an iterable
+or itself be a generator. Those values are aggregated from all handlers and
+returned to the caller as list. The order of this list is undefined.
+
+.. code-block:: python
+
+ import qubes.events
+
+ class MyClass(qubes.events.Emitter):
+ @qubes.events.handler('event1')
+ def event1_handler1(self, event):
+ # do not return anything, equivalent to "return" and "return None"
+ pass
+
+ @qubes.events.handler('event1')
+ def event1_handler2(self, event):
+ yield 'aqq'
+ yield 'zxc'
+
+ @qubes.events.handler('event1')
+ def event1_handler3(self, event):
+ return ('123', '456')
+
+ o = MyClass()
+
+ # returns ['aqq', 'zxc', '123', '456'], possibly not in order
+ effect = o.fire_event('event1')
+
+
+Module contents
+---------------
+
+.. automodule:: qubes.events
+ :members:
+ :show-inheritance:
+
+.. vim: ts=3 sw=3 et
diff --git a/doc/qubes-exc.rst b/doc/qubes-exc.rst
new file mode 100644
index 00000000..f3a644b1
--- /dev/null
+++ b/doc/qubes-exc.rst
@@ -0,0 +1,63 @@
+:py:mod:`qubes.exc` -- Exceptions
+=================================
+
+As most of the modern programming languages, Python has exceptions, which can be
+thrown (``raise``\ d) when something goes bad. What exactly means "bad" depends
+on several circumstances.
+
+One of those circumstances is who exactly is to blame: programmer or user? Some
+errors are commited by programmer and will most probably result it program
+failure. But most errors are caused by the user, notably by specifying invalid
+commands or data input. Those errors *should not* result in failure, but fault
+and be handled gracefuly. One more time, what "gracefuly" means depends on
+specific program and its interface (for example GUI programs should most likely
+display some admonition, but will not crash).
+
+In Qubes we have special exception class, :py:class:`qubes.exc.QubesException`,
+which is dedicated to handling user-caused problems. Programmer errors should
+not result in raising QubesException, but it should instead result in one of the
+standard Python exception. QubesExceptions should have a nice message that can
+be shown to the user. On the other hand, some children classes of QubesException
+also inherit from children of :py:class:`StandardException` to allow uniform
+``except`` clauses.
+
+Often the error relates to some domain, because we expect it to be in certain
+state, but it is not. For example to start a machine, it should be halted. For
+that we have the children of the :py:class:`qubes.exc.QubesVMError` class. They
+all take the domain in question as their first argument and an (optional)
+message as the second. If not specified, there is stock message which is
+generally informative enough.
+
+
+On writing error messages
+-------------------------
+
+As a general rule, error messages should be short but precise. They should not
+blame user for error, but the user should know, what had been done wrong and
+what to do next.
+
+If possible, write the message that is stating the fact, for example "Domain is
+not running" instead of "You forgot to start the domain" (you fool!). Avoid
+commanding user, like "Start the domain first" (user is not a function you can
+call for effect). Instead consider writing in negative form, implying expected
+state: "Domain is not running" instead of "Domain is paused" (yeah, what's wrong
+with that?).
+
+Also avoid implying the personhood of the computer, including adressing user in
+second person. For example, write "Sending message failed" instead of "I failed
+to send the message".
+
+
+Inheritance diagram
+-------------------
+
+.. inheritance-diagram:: qubes.exc
+
+Module contents
+---------------
+
+.. automodule:: qubes.exc
+ :members:
+ :show-inheritance:
+
+.. vim: ts=3 sw=3 et tw=80
diff --git a/doc/qubes-ext.rst b/doc/qubes-ext.rst
new file mode 100644
index 00000000..0947efe4
--- /dev/null
+++ b/doc/qubes-ext.rst
@@ -0,0 +1,8 @@
+:py:mod:`qubes.ext` -- Qubes extensions
+=======================================
+
+.. automodule:: qubes.ext
+ :members:
+ :show-inheritance:
+
+.. vim: ts=3 sw=3 et
diff --git a/doc/qubes-log.rst b/doc/qubes-log.rst
new file mode 100644
index 00000000..3d12a621
--- /dev/null
+++ b/doc/qubes-log.rst
@@ -0,0 +1,8 @@
+:py:mod:`qubes.log` -- Logging routines
+=======================================
+
+.. automodule:: qubes.log
+ :members:
+ :show-inheritance:
+
+.. vim: ts=3 sw=3 et
diff --git a/doc/qubes-policy.rst b/doc/qubes-policy.rst
new file mode 100644
index 00000000..0be819ca
--- /dev/null
+++ b/doc/qubes-policy.rst
@@ -0,0 +1,87 @@
+:py:mod:`qubes.policy` -- Qubes RPC policy
+==========================================
+
+Every Qubes domain can trigger various RPC services, but if such call would be
+allowed depends on Qubes RPC policy (qrexec policy in short).
+
+Qrexec policy format
+--------------------
+
+Policy consists of a file, which is parsed line-by-line. First matching line
+is used as an action.
+
+Each line consist of three values separated by white characters (space(s), tab(s)):
+1. Source specification, which is one of:
+
+ - domain name
+ - `$anyvm` - any domain
+ - `$tag:some-tag` - VM having tag `some-tag`
+ - `$type:vm-type` - VM of `vm-type` type, available types:
+ AppVM, TemplateVM, StandaloneVM, DispVM
+
+2. Target specification, one of:
+
+ - domain name
+ - `$anyvm` - any domain, excluding dom0
+ - `$tag:some-tag` - domain having tag `some-tag`
+ - `$type:vm-type` - domain of `vm-type` type, available types:
+ AppVM, TemplateVM, StandaloneVM, DispVM
+ - `$default` - used when caller did not specified any VM
+ - `$dispvm:vm-name` - _new_ Disposable VM created from AppVM `vm-name`
+ - `$dispvm` - _new_ Disposable VM created from AppVM pointed by caller
+ property `default_dispvm`, which defaults to global property `default_dispvm`
+
+3. Action and optional action parameters, one of:
+
+ - `allow` - allow the call, without further questions; optional parameters:
+ - `target=` - override caller provided call target -
+ possible values are: domain name, `$dispvm` or `$dispvm:vm-name`
+ - `user=` - call the service using this user, instead of the user
+ pointed by target VM's `default_user` property
+ - `deny` - deny the call, without further questions; no optional
+ parameters are supported
+ - `ask` - ask the user for confirmation; optional parameters:
+ - `target=` - override user provided call target
+ - `user=` - call the service using this user, instead of the user
+ pointed by target VM's `default_user` property
+ - `default_target=` - suggest this target when prompting the user for
+ confirmation
+
+Alternatively, a line may consist of a single keyword `$include:` followed by a
+path. This will load a given file as its content would be in place of
+`$include` line. Relative paths are resolved relative to
+`/etc/qubes-rpc/policy` directory.
+
+Evaluating `ask` action
+-----------------------
+
+When qrexec policy specify `ask` action, the user is asked whether the call
+should be allowed or denied. In addition to that, user also need to choose
+target domain. User have to choose from a set of targets specified by the
+policy. Such set is calculated using the algorithm below:
+
+1. If `ask` action have `target=` option specified, only that target is
+considered. A prompt window will allow to choose only this value and it will
+also be pre-filled value.
+
+2. If no `target=` option is specified, all rules are evaluated to see what
+target domains (for a given source domain) would result in `ask` or `allow`
+action. If any of them have `target=` option set, that value is used instead of
+the one specified in "target" column (for this particular line). Then the user
+is presented with a confirmation dialog and an option to choose from those
+domains.
+
+3. If `default_target=` option is set, it is used as
+suggested value, otherwise no suggestion is made (regardless of calling domain
+specified any target or not).
+
+
+
+Module contents
+---------------
+
+.. automodule:: qubespolicy
+ :members:
+ :show-inheritance:
+
+.. vim: ts=3 sw=3 et
diff --git a/doc/qubes-tests.rst b/doc/qubes-tests.rst
new file mode 100644
index 00000000..a5dd06ec
--- /dev/null
+++ b/doc/qubes-tests.rst
@@ -0,0 +1,144 @@
+:py:mod:`qubes.tests` -- Writing tests for qubes
+================================================
+
+Writing tests is very important for ensuring quality of code that is delivered.
+Given test case may check for variety of conditions, but they generally fall
+inside those two categories of conformance tests:
+
+* Unit tests: these test smallest units of code, probably methods of functions,
+ or even combination of arguments for one specific method.
+
+* Integration tests: these test interworking of units.
+
+We are interested in both categories.
+
+There is also distinguished category of regression tests (both unit- and
+integration-level), which are included because they check for specific bugs that
+were fixed in the past and should not happen in the future. Those should be
+accompanied with reference to closed ticked that describes the bug.
+
+Qubes' tests are written using :py:mod:`unittest` module from Python Standard
+Library for both unit test and integration tests.
+
+Test case organisation
+----------------------
+
+Every module (like :py:mod:`qubes.vm.qubesvm`) should have its companion (like
+``qubes.tests.vm.qubesvm``). Packages ``__init__.py`` files should be
+accompanied by ``init.py`` inside respective directory under :file:`tests/`.
+Inside tests module there should be one :py:class:`qubes.tests.QubesTestCase`
+class for each class in main module plus one class for functions and global
+variables. :py:class:`qubes.tests.QubesTestCase` classes should be named
+``TC_xx_ClassName``, where ``xx`` is two-digit number. Test functions should be
+named ``test_xxx_test_name``, where ``xxx`` is three-digit number. You may
+introduce some structure of your choice in this number.
+
+FIXME: where are placed integration tests?
+
+Writing tests
+-------------
+
+First of all, testing is art, not science. Testing is not panaceum and won't
+solve all of your problems. Rules given in this guide and elsewhere should be
+followed, but shouldn't be worshipped.
+
+Test can be divided into three phases. The first part is setup phase. In this
+part you should arrange for a test condition to occur. You intentionally put
+system under test in some specific state. Phase two is executing test condition
+-- for example you check some variable for equality or expect that some
+exception is raised. Phase three is responsible for returning a verdict. This is
+largely done by the framework.
+
+When writing test, you should think about order of execution. This is the reason
+of numbers in names of the classes and test methods. Tests should be written
+bottom-to-top, that is, test setups that are ran later may depend on features
+that are tested after but not the other way around. This is important, because
+when encountering failure we expect the reason happen *before*, and not after
+failure occured. Therefore, when encountering multiple errors, we may instantly
+focus on fixing the first one and not wondering if any later problems may be
+relevant or not. Some people also like to enable
+:py:attr:`unittest.TestResult.failfast` feature, which stops on the first failed
+test -- with wrong order this messes up their workflow.
+
+Test should fail for one reason only and test one specific issue. This does not
+mean that you can use one ``.assert*`` method per ``test_`` function: for
+example when testing one regular expression you are welcome to test many valid
+and/or invalid inputs, especcialy when test setup is complicated. However, if
+you encounter problems during setup phase, you should *skip* the test, and not
+fail it. This also aids interpretation of results.
+
+You may, when it makes sense, manipulate private members of classes under tests.
+This violates one of the founding principles of object-oriented programming, but
+may be required to write tests in correct order if your class provides public
+methods with circular dependencies. For example containers may check if added
+item is already in container, but you can't test ``__contains__`` method without
+something already inside. Don't forget to test the other method later.
+
+Special Qubes-specific considerations
+-------------------------------------
+
+Events
+^^^^^^
+
+:py:class:`qubes.tests.QubesTestCase` provides convenient methods for checking
+if event fired or not: :py:meth:`qubes.tests.QubesTestCase.assertEventFired` and
+:py:meth:`qubes.tests.QubesTestCase.assertEventNotFired`. These require that
+emitter is subclass of :py:class:`qubes.tests.TestEmitter`. You may instantiate
+it directly::
+
+ import qubes.tests
+
+ class TC_10_SomeClass(qubes.tests.QubesTestCase):
+ def test_000_event(self):
+ emitter = qubes.tests.TestEmitter()
+ emitter.fire_event('did-fire')
+ self.assertEventFired(emitter, 'did-fire')
+
+If you need to snoop specific class (which already is a child of
+:py:class:`qubes.events.Emitter`, possibly indirect), you can define derivative
+class which uses :py:class:`qubes.tests.TestEmitter` as mix-in::
+
+ import qubes
+ import qubes.tests
+
+ class TestHolder(qubes.tests.TestEmitter, qubes.PropertyHolder):
+ pass
+
+ class TC_20_PropertyHolder(qubes.tests.QubesTestCase):
+ def test_000_event(self):
+ emitter = TestHolder()
+ self.assertEventNotFired(emitter, 'did-not-fire')
+
+Dom0
+^^^^
+
+Qubes is a complex piece of software and depends on number other complex pieces,
+notably VM hypervisor or some other isolation provider. Not everything may be
+testable under all conditions. Some tests (mainly unit tests) are expected to
+run during compilation, but many tests (probably all of the integration tests
+and more) can run only inside already deployed Qubes installation. There is
+special decorator, :py:func:`qubes.tests.skipUnlessDom0` which causes test (or
+even entire class) to be skipped outside dom0. Use it freely::
+
+ import qubes.tests
+
+ class TC_30_SomeClass(qubes.tests.QubesTestCase):
+ @qubes.tests.skipUnlessDom0
+ def test_000_inside_dom0(self):
+ # this is skipped outside dom0
+ pass
+
+ @qubes.tests.skipUnlessDom0
+ class TC_31_SomeOtherClass(qubes.tests.QubesTestCase):
+ # all tests in this class are skipped
+ pass
+
+
+Module contents
+---------------
+
+.. automodule:: qubes.tests
+ :members:
+ :show-inheritance:
+
+.. vim: ts=3 sw=3 et tw=80
diff --git a/doc/qubes-tools/index.rst b/doc/qubes-tools/index.rst
new file mode 100644
index 00000000..185fe896
--- /dev/null
+++ b/doc/qubes-tools/index.rst
@@ -0,0 +1,32 @@
+:py:mod:`qubes.tools` -- Command line utilities
+===============================================
+
+Those are Python modules that house actual functionality of CLI tools -- the
+files installed in :file:`/usr/bin` only import these modules and run ``main()``
+function.
+
+The modules should make available for import theirs command line parsers
+(instances of :py:class:`argparse.ArgumentParser`) as either ``.parser``
+attribute or function ``get_parser()``, which returns parser. Manual page will
+be automatically checked during generation if its "Options" section contains all
+options from this parser (and only those).
+
+
+Module contents
+---------------
+
+.. automodule:: qubes.tools
+ :members:
+ :show-inheritance:
+
+
+All CLI tools
+-------------
+
+.. toctree::
+ :maxdepth: 1
+ :glob:
+
+ *
+
+.. vim: ts=3 sw=3 et tw=80
diff --git a/doc/qubes-tools/qmemmand.rst b/doc/qubes-tools/qmemmand.rst
new file mode 100644
index 00000000..277044f9
--- /dev/null
+++ b/doc/qubes-tools/qmemmand.rst
@@ -0,0 +1,8 @@
+:py:mod:`qubes.tools.qmemmand` -- qmemman daemon
+================================================
+
+.. automodule:: qubes.tools.qmemmand
+ :members:
+ :show-inheritance:
+
+.. vim: ts=3 sw=3 et
diff --git a/doc/qubes-tools/qubes-prefs.rst b/doc/qubes-tools/qubes-prefs.rst
deleted file mode 100644
index 459c907f..00000000
--- a/doc/qubes-tools/qubes-prefs.rst
+++ /dev/null
@@ -1,24 +0,0 @@
-===========
-qubes-prefs
-===========
-
-NAME
-====
-qubes-prefs - display system-wide Qubes settings, such as:
-
-- clock VM
-- update VM
-- default template
-- default firewallVM
-- default kernel
-- default netVM
-
-SYNOPSIS
-========
-| qubes-prefs
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qubes-tools/qubes_guid.rst b/doc/qubes-tools/qubes_guid.rst
deleted file mode 100644
index 10bcd93e..00000000
--- a/doc/qubes-tools/qubes_guid.rst
+++ /dev/null
@@ -1,30 +0,0 @@
-==========
-qubes_guid
-==========
-
-NAME
-====
-qubes_guid
-
-SYNOPSIS
-========
-| qubes_guid -d domain_id [-c color] [-l label_index] [-i icon name, no suffix] [-v] [-q]
-
-OPTIONS
-=======
--v
- Increase log verbosity
--q
- Decrease log verbosity
-
-Log levels:
- 0. only errors
- 1. some basic messages (default)
- 2. debug
-
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qubes-vm/adminvm.rst b/doc/qubes-vm/adminvm.rst
new file mode 100644
index 00000000..7f5a7b77
--- /dev/null
+++ b/doc/qubes-vm/adminvm.rst
@@ -0,0 +1,8 @@
+:py:mod:`qubes.vm.adminvm` -- Dom0
+==================================
+
+.. automodule:: qubes.vm.adminvm
+ :members:
+ :show-inheritance:
+
+.. vim: ts=3 sw=3 et
diff --git a/doc/qubes-vm/appvm.rst b/doc/qubes-vm/appvm.rst
new file mode 100644
index 00000000..5ef55f31
--- /dev/null
+++ b/doc/qubes-vm/appvm.rst
@@ -0,0 +1,8 @@
+:py:mod:`qubes.vm.appvm` -- Application VM
+==========================================
+
+.. automodule:: qubes.vm.appvm
+ :members:
+ :show-inheritance:
+
+.. vim: ts=3 sw=3 et
diff --git a/doc/qubes-vm/dispvm.rst b/doc/qubes-vm/dispvm.rst
new file mode 100644
index 00000000..f8ab796e
--- /dev/null
+++ b/doc/qubes-vm/dispvm.rst
@@ -0,0 +1,8 @@
+:py:mod:`qubes.vm.dispvm` -- Disposable VM
+==========================================
+
+.. automodule:: qubes.vm.dispvm
+ :members:
+ :show-inheritance:
+
+.. vim: ts=3 sw=3 et
diff --git a/doc/qubes-vm/index.rst b/doc/qubes-vm/index.rst
new file mode 100644
index 00000000..ecc27f86
--- /dev/null
+++ b/doc/qubes-vm/index.rst
@@ -0,0 +1,84 @@
+: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.Features
+ :members:
+ :show-inheritance:
+
+Particular VM classes
+^^^^^^^^^^^^^^^^^^^^^
+
+Main types:
+
+.. toctree::
+ :maxdepth: 1
+
+ qubesvm
+ appvm
+ templatevm
+
+Special VM types:
+
+.. toctree::
+ :maxdepth: 1
+
+ dispvm
+ adminvm
+
+.. standalonevm
+
+.. vim: ts=3 sw=3 et
diff --git a/doc/qubes-vm/qubesvm.rst b/doc/qubes-vm/qubesvm.rst
new file mode 100644
index 00000000..90f1b71a
--- /dev/null
+++ b/doc/qubes-vm/qubesvm.rst
@@ -0,0 +1,8 @@
+:py:mod:`qubes.vm.qubesvm` -- Shared functionality
+==================================================
+
+.. automodule:: qubes.vm.qubesvm
+ :members:
+ :show-inheritance:
+
+.. vim: ts=3 sw=3 et
diff --git a/doc/qubes-vm/templatevm.rst b/doc/qubes-vm/templatevm.rst
new file mode 100644
index 00000000..a7ffa66b
--- /dev/null
+++ b/doc/qubes-vm/templatevm.rst
@@ -0,0 +1,8 @@
+:py:mod:`qubes.vm.templatevm` -- Template for AppVM
+===================================================
+
+.. automodule:: qubes.vm.templatevm
+ :members:
+ :show-inheritance:
+
+.. vim: ts=3 sw=3 et
diff --git a/doc/qubes.rst b/doc/qubes.rst
new file mode 100644
index 00000000..955870a8
--- /dev/null
+++ b/doc/qubes.rst
@@ -0,0 +1,220 @@
+: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:
+
+.. vim: ts=3 sw=3 et
diff --git a/doc/qvm-tools/qvm-add-appvm.rst b/doc/qvm-tools/qvm-add-appvm.rst
deleted file mode 100644
index 4e0d0af7..00000000
--- a/doc/qvm-tools/qvm-add-appvm.rst
+++ /dev/null
@@ -1,30 +0,0 @@
-=============
-qvm-add-appvm
-=============
-
-NAME
-====
-qvm-add-appvm - add an already installed appvm to the Qubes DB
-
-WARNING: Normally you should not need this command, and you should use qvm-create instead!
-
-SYNOPSIS
-========
-| qvm-add-appvm [options]
-
-OPTIONS
-=======
--h, --help
- Show this help message and exit
--p DIR_PATH, --path=DIR_PATH
- Specify path to the template directory
--c CONF_FILE, --conf=CONF_FILE
- Specify the Xen VM .conf file to use(relative to the template dir path)
---force-root
- Force to run, even with root privileges
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qvm-tools/qvm-add-template.rst b/doc/qvm-tools/qvm-add-template.rst
deleted file mode 100644
index 6a4fee31..00000000
--- a/doc/qvm-tools/qvm-add-template.rst
+++ /dev/null
@@ -1,28 +0,0 @@
-================
-qvm-add-template
-================
-
-NAME
-====
-qvm-add-template - adds an already installed template to the Qubes DB
-
-SYNOPSIS
-========
-| qvm-add-template [options]
-
-OPTIONS
-=======
--h, --help
- Show this help message and exit
--p DIR_PATH, --path=DIR_PATH
- Specify path to the template directory
--c CONF_FILE, --conf=CONF_FILE
- Specify the Xen VM .conf file to use(relative to the template dir path)
---rpm
- Template files have been installed by RPM
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qvm-tools/qvm-backup-restore.rst b/doc/qvm-tools/qvm-backup-restore.rst
deleted file mode 100644
index 9ee96a4a..00000000
--- a/doc/qvm-tools/qvm-backup-restore.rst
+++ /dev/null
@@ -1,50 +0,0 @@
-==================
-qvm-backup-restore
-==================
-
-NAME
-====
-qvm-backup-restore - restores Qubes VMs from backup
-
-SYNOPSIS
-========
-| qvm-backup-restore [options]
-
-OPTIONS
-=======
--h, --help
- Show this help message and exit
---verify-only
- Do not restore the data, only verify backup integrity
---skip-broken
- Do not restore VMs that have missing templates or netvms
---ignore-missing
- Ignore missing templates and netvms, and restore the VMs anyway
---skip-conflicting
- Do not restore VMs that are already present on the host
---force-root
- Force to run with root privileges
---replace-template=REPLACE_TEMPLATE
- Restore VMs using another template, syntax: old-template-name:new-template-name (can be repeated)
--x EXCLUDE, --exclude=EXCLUDE
- Skip restore of specified VM (can be repeated)
---skip-dom0-home
- Do not restore dom0's user home directory
---ignore-username-mismatch
- Ignore dom0 username mismatch when restoring dom0's user home directory
--d APPVM, --dest-vm=APPVM
- Restore from a backup located in a specific AppVM
--e, --encrypted
- The backup is encrypted
--p, --passphrase-file
- Read passphrase from file, or use '-' to read from stdin
--z, --compressed
- The backup is compressed
---debug
- Enable (a lot of) debug output
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qvm-tools/qvm-backup.rst b/doc/qvm-tools/qvm-backup.rst
deleted file mode 100644
index 0b749259..00000000
--- a/doc/qvm-tools/qvm-backup.rst
+++ /dev/null
@@ -1,46 +0,0 @@
-==========
-qvm-backup
-==========
-
-NAME
-====
-qvm-backup
-
-SYNOPSIS
-========
-| qvm-backup [options] [vms-to-be-included ...]
-
-OPTIONS
-=======
--h, --help
- Show this help message and exit
--x EXCLUDE_LIST, --exclude=EXCLUDE_LIST
- Exclude the specified VM from backup (might be repeated)
---force-root
- Force to run with root privileges
--d, --dest-vm
- Specify the destination VM to which the backup will be set (implies -e)
--e, --encrypt
- Encrypt the backup
---no-encrypt
- Skip encryption even if sending the backup to a VM
--p, --passphrase-file
- Read passphrase from a file, or use '-' to read from stdin
--E, --enc-algo
- Specify a non-default encryption algorithm. For a list of supported algorithms, execute 'openssl list-cipher-algorithms' (implies -e)
--H, --hmac-algo
- Specify a non-default HMAC algorithm. For a list of supported algorithms, execute 'openssl list-message-digest-algorithms'
--z, --compress
- Compress the backup
--Z, --compress-filter
- Specify a non-default compression filter program (default: gzip)
---tmpdir
- Specify a temporary directory (if you have at least 1GB free RAM in dom0, use of /tmp is advised) (default: /var/tmp)
---debug
- Enable (a lot of) debug output
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qvm-tools/qvm-block.rst b/doc/qvm-tools/qvm-block.rst
deleted file mode 100644
index bbc9a396..00000000
--- a/doc/qvm-tools/qvm-block.rst
+++ /dev/null
@@ -1,44 +0,0 @@
-=========
-qvm-block
-=========
-
-NAME
-====
-qvm-block - list/set VM PCI devices.
-
-SYNOPSIS
-========
-| qvm-block -l [options]
-| qvm-block -a [options] :
-| qvm-block -A [options] :
-| qvm-block -d [options] :
-| qvm-block -d [options]
-
-OPTIONS
-=======
--h, --help
- Show this help message and exit
--l, --list
- List block devices
--A, --attach-file
- Attach specified file instead of physical device
--a, --attach
- Attach block device to specified VM
--d, --detach
- Detach block device
--f FRONTEND, --frontend=FRONTEND
- Specify device name at destination VM [default: xvdi]
---ro
- Force read-only mode
---no-auto-detach
- Fail when device already connected to other VM
---show-system-disks
- List also system disks
---force-root
- Force to run, even with root privileges
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qvm-tools/qvm-check.rst b/doc/qvm-tools/qvm-check.rst
deleted file mode 100644
index e763b4fb..00000000
--- a/doc/qvm-tools/qvm-check.rst
+++ /dev/null
@@ -1,30 +0,0 @@
-=========
-qvm-check
-=========
-
-NAME
-====
-qvm-check - Specify no state options to check if VM exists
-
-SYNOPSIS
-========
-| qvm-check [options]
-
-OPTIONS
-=======
--h, --help
- Show this help message and exit
--q, --quiet
- Be quiet
---running
- Determine if VM is running
---paused
- Determine if VM is paused
---template
- Determine if VM is a template
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qvm-tools/qvm-clone.rst b/doc/qvm-tools/qvm-clone.rst
deleted file mode 100644
index 60d89586..00000000
--- a/doc/qvm-tools/qvm-clone.rst
+++ /dev/null
@@ -1,31 +0,0 @@
-=========
-qvm-clone
-=========
-
-NAME
-====
-qvm-clone - clones an existing VM by copying all its disk files
-
-SYNOPSIS
-========
-| qvm-clone [options]
-
-OPTIONS
-=======
--h, --help
- Show this help message and exit
--q, --quiet
- Be quiet
--p DIR_PATH, --path=DIR_PATH
- Specify path to the template directory
---force-root
- Force to run, even with root privileges
--P, --pool
- Specify in to which storage pool to clone
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
-
diff --git a/doc/qvm-tools/qvm-create-default-dvm.rst b/doc/qvm-tools/qvm-create-default-dvm.rst
deleted file mode 100644
index aa4d2d39..00000000
--- a/doc/qvm-tools/qvm-create-default-dvm.rst
+++ /dev/null
@@ -1,35 +0,0 @@
-======================
-qvm-create-default-dvm
-======================
-
-NAME
-====
-qvm-create-default-dvm - creates a default disposable VM
-
-SYNOPSIS
-========
-| qvm-create-default-dvm templatename|--default-template|--used-template [script-name|--default-script]
-
-OPTIONS
-=======
-templatename
- Base DispVM on given template. The command will create AppVM named after
- template with "-dvm" suffix. This VM will be used to create DispVM
- savefile. If you want to customize DispVM, use this VM - take a look at
- https://wiki.qubes-os.org/wiki/UserDoc/DispVMCustomization
-
---default-template
- Use default template for the DispVM
-
---used-template
- Use the same template as earlier
-
---default-script
- Use default script for seeding DispVM home.
-
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qvm-tools/qvm-create.rst b/doc/qvm-tools/qvm-create.rst
deleted file mode 100644
index 5327c742..00000000
--- a/doc/qvm-tools/qvm-create.rst
+++ /dev/null
@@ -1,53 +0,0 @@
-==========
-qvm-create
-==========
-
-NAME
-====
-qvm-create - creates a new VM
-
-SYNOPSIS
-========
-| qvm-create [options]
-
-OPTIONS
-=======
--h, --help
- Show this help message and exit
--t TEMPLATE, --template=TEMPLATE
- Specify the TemplateVM to use
--l LABEL, --label=LABEL
- Specify the label to use for the new VM (e.g. red, yellow, green, ...)
--p, --proxy
- Create ProxyVM
--n, --net
- Create NetVM
--H, --hvm
- Create HVM (standalone, unless --template option used)
---hvm-template
- Create HVM template
--R ROOT_MOVE, --root-move-from=ROOT_MOVE
- Use provided root.img instead of default/empty one
- (file will be MOVED)
--r ROOT_COPY, --root-copy-from=ROOT_COPY
- Use provided root.img instead of default/empty one
- (file will be COPIED)
--s, --standalone
- Create standalone VM - independent of template
--m MEM, --mem=MEM
- Initial memory size (in MB)
--c VCPUS, --vcpus=VCPUS
- VCPUs count
--i, --internal
- Create VM for internal use only (hidden in qubes-manager, no appmenus)
---force-root
- Force to run, even with root privileges
--q, --quiet
- Be quiet
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
-
diff --git a/doc/qvm-tools/qvm-firewall.rst b/doc/qvm-tools/qvm-firewall.rst
deleted file mode 100644
index eeb9da50..00000000
--- a/doc/qvm-tools/qvm-firewall.rst
+++ /dev/null
@@ -1,48 +0,0 @@
-============
-qvm-firewall
-============
-
-NAME
-====
-qvm-firewall - manage VM's firewall rules
-
-SYNOPSIS
-========
-| qvm-firewall [-n] [action] [rule spec]
-
-Rule specification can be one of:
- 1. address|hostname[/netmask] tcp|udp port[-port]
- 2. address|hostname[/netmask] tcp|udp service_name
- 3. address|hostname[/netmask] any
-
-OPTIONS
-=======
--h, --help
- Show this help message and exit
--l, --list
- List firewall settings (default action)
--a, --add
- Add rule
--d, --del
- Remove rule (given by number or by rule spec)
--P SET_POLICY, --policy=SET_POLICY
- Set firewall policy (allow/deny)
--i SET_ICMP, --icmp=SET_ICMP
- Set ICMP access (allow/deny)
--D SET_DNS, --dns=SET_DNS
- Set DNS access (allow/deny)
--Y SET_YUM_PROXY, --yum-proxy=SET_YUM_PROXY
- Set access to Qubes yum proxy (allow/deny).
- *Note:* if set to "deny", access will be rejected even if policy set to "allow"
--r, --reload
- Reload firewall (implied by any change action)
--n, --numeric
- Display port numbers instead of services (makes sense only with --list)
---force-root
- Force to run, even with root privileges
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qvm-tools/qvm-grow-private.rst b/doc/qvm-tools/qvm-grow-private.rst
deleted file mode 100644
index ef1a9b67..00000000
--- a/doc/qvm-tools/qvm-grow-private.rst
+++ /dev/null
@@ -1,22 +0,0 @@
-================
-qvm-grow-private
-================
-
-NAME
-====
-qvm-grow-private - increase private storage capacity of a specified VM
-
-SYNOPSIS
-========
-| qvm-grow-private
-
-OPTIONS
-=======
--h, --help
- Show this help message and exit
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qvm-tools/qvm-grow-root.rst b/doc/qvm-tools/qvm-grow-root.rst
deleted file mode 100644
index 5f004d4b..00000000
--- a/doc/qvm-tools/qvm-grow-root.rst
+++ /dev/null
@@ -1,24 +0,0 @@
-=============
-qvm-grow-root
-=============
-
-NAME
-====
-qvm-grow-root - increase root storage capacity of a specified VM
-
-SYNOPSIS
-========
-| qvm-grow-root
-
-OPTIONS
-=======
--h, --help
- Show this help message and exit
---allow-start
- Allow VM to be started to complete the operation
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qvm-tools/qvm-kill.rst b/doc/qvm-tools/qvm-kill.rst
deleted file mode 100644
index 9314f4b7..00000000
--- a/doc/qvm-tools/qvm-kill.rst
+++ /dev/null
@@ -1,23 +0,0 @@
-========
-qvm-kill
-========
-
-NAME
-====
-qvm-kill - kills the specified VM
-
-SYNOPSIS
-========
-| qvm-kill [options]
-
-
-OPTIONS
-=======
--h, --help
- Show this help message and exit
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qvm-tools/qvm-ls.rst b/doc/qvm-tools/qvm-ls.rst
deleted file mode 100644
index a9b92160..00000000
--- a/doc/qvm-tools/qvm-ls.rst
+++ /dev/null
@@ -1,42 +0,0 @@
-======
-qvm-ls
-======
-
-NAME
-====
-qvm-ls - list VMs and various information about their state
-
-SYNOPSIS
-========
-| qvm-ls [options]
-
-OPTIONS
-=======
--h, --help
- Show help message and exit
--n, --network
- Show network addresses assigned to VMs
--c, --cpu
- Show CPU load
--m, --mem
- Show memory usage
--d, --disk
- Show VM disk utilization statistics
--i, --ids
- Show Qubes and Xen id
--k, --kernel
- Show VM kernel options
--b, --last-backup
- Show date of last VM backup
---raw-list
- List only VM names one per line
---raw-data
- Display specify data of specified VMs. Intended for bash-parsing.
---list-fields
- List field names valid for --raw-data
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qvm-tools/qvm-pci.rst b/doc/qvm-tools/qvm-pci.rst
deleted file mode 100644
index ed974eef..00000000
--- a/doc/qvm-tools/qvm-pci.rst
+++ /dev/null
@@ -1,36 +0,0 @@
-=======
-qvm-pci
-=======
-
-NAME
-====
-qvm-pci - list/set VM PCI devices
-
-SYNOPSIS
-========
-| qvm-pci -l [options]
-| qvm-pci -a [options]
-| qvm-pci -d [options]
-
-OPTIONS
-=======
--h, --help
- Show this help message and exit
--l, --list
- List VM PCI devices
--a, --add
- Add a PCI device to specified VM
--C, --add-class
- Add all devices of given class:
- net - network interfaces,
- usb - USB controllers
--d, --delete
- Remove a PCI device from specified VM
---offline-mode
- Offline mode
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qvm-tools/qvm-prefs.rst b/doc/qvm-tools/qvm-prefs.rst
deleted file mode 100644
index b13c3fdb..00000000
--- a/doc/qvm-tools/qvm-prefs.rst
+++ /dev/null
@@ -1,215 +0,0 @@
-=========
-qvm-prefs
-=========
-
-NAME
-====
-qvm-prefs - list/set various per-VM properties
-
-SYNOPSIS
-========
-| qvm-prefs -l [options]
-| qvm-prefs -g [options]
-| qvm-prefs -s [options] [...]
-
-
-OPTIONS
-=======
--h, --help
- Show this help message and exit
--l, --list
- List properties of a specified VM
--g, --get
- Get a single property of a specified VM
--s, --set
- Set properties of a specified VM
---force-root
- Force to run, even with root privileges
---offline-mode
- Offline mode
-
-PROPERTIES
-==========
-
-include_in_backups
- Accepted values: ``True``, ``False``
-
- Control whenever this VM will be included in backups by default (for now
- works only in qubes-manager). You can always manually select or
- deselect any VM for backup.
-
-pcidevs
- PCI devices assigned to the VM. Should be edited using qvm-pci tool.
-
-pci_strictreset
- Accepted values: ``True``, ``False``
-
- Control whether prevent assigning to VM a device which does not support any
- reset method. Generally such devices should not be assigned to any VM,
- because there will be no way to reset device state after VM shutdown, so
- the device could attack next VM to which it will be assigned. But in some
- cases it could make sense - for example when the VM to which it is assigned
- is trusted one, or is running all the time.
-
-pci_e820_host
- Accepted values: ``True``, ``False``
-
- Give VM with PCI devices a memory map (e820) of the host. This is
- required for some devices to properly resolve conflicts in address space.
- This option is enabled by default for VMs with PCI devices and have no
- effect for VMs without devices.
-
-label
- Accepted values: ``red``, ``orange``, ``yellow``, ``green``, ``gray``,
- ``blue``, ``purple``, ``black``
-
- Color of VM label (icon, appmenus, windows border). If VM is running,
- change will be applied at first VM restart.
-
-netvm
- Accepted values: netvm name, ``default``, ``none``
-
- To which NetVM connect. Setting to ``default`` will follow system-global
- default NetVM (managed by qubes-prefs). Setting to ``none`` will disable
- networking in this VM.
-
-dispvm_netvm
- Accepted values: netvm name, ``default``, ``none``
-
- Which NetVM should be used for Disposable VMs started by this one.
- ``default`` is to use the same NetVM as the VM itself.
-
-maxmem
- Accepted values: memory size in MB
-
- Maximum memory size available for this VM. Dynamic memory management (aka
- qmemman) will not be able to balloon over this limit. For VMs with
- qmemman disabled, this will be overridden by *memory* property (at VM
- startup).
-
-memory
- Accepted values: memory size in MB
-
- Initial memory size for VM. This should be large enough to allow VM startup
- - before qmemman starts managing memory for this VM. For VM with qmemman
- disabled, this is static memory size.
-
-kernel
- Accepted values: kernel version, ``default``, ``none``
-
- Kernel version to use (only for PV VMs). Available kernel versions will be
- listed when no value given (there are in /var/lib/qubes/vm-kernels).
- Setting to ``default`` will follow system-global default kernel (managed
- via qubes-prefs). Setting to ``none`` will use "kernels" subdir in
- VM directory - this allows having VM-specific kernel; also this the only
- case when /lib/modules is writable from within VM.
-
-template
- Accepted values: TemplateVM name
-
- TemplateVM on which VM base. It can be changed only when VM isn't running.
-
-vcpus
- Accepted values: no of CPUs
-
- Number of CPU (cores) available to VM. Some VM types (eg DispVM) will not
- work properly with more than one CPU.
-
-kernelopts
- Accepted values: string, ``default``
-
- VM kernel parameters (available only for PV VMs). This can be used to
- workaround some hardware specific problems (eg for NetVM). Setting to
- ``default`` will use some reasonable defaults (currently different for VMs
- with PCI devices and without). For VM without PCI devices
- ``default`` option means inherit this value from the VM template (if any).
- Some helpful options (for debugging purposes): ``earlyprintk=xen``,
- ``init=/bin/bash``
-
-name
- Accepted values: alphanumerical name
-
- Name of the VM. Can be only changed when VM isn't running.
-
-drive
- Accepted values: [hd:\|cdrom:][backend-vm:]path
-
- Additional drive for the VM (available only for HVMs). This can be used to
- attach installation image. ``path`` can be file or physical device (eg.
- /dev/sr0). The same syntax can be used in qvm-start --drive - to
- attach drive only temporarily.
-
-mac
- Accepted values: MAC address, ``auto``
-
- Can be used to force specific of virtual ethernet card in the VM. Setting
- to ``auto`` will use automatic-generated MAC - based on VM id. Especially
- useful when licensing requires a static MAC address.
- For template-based HVM ``auto`` mode means to clone template MAC.
-
-default_user
- Accepted values: username
-
- Default user used by qvm-run. Note that it make sense only on non-standard
- template, as the standard one always have "user" account.
-
-debug
- Accepted values: ``on``, ``off``
-
- Enables debug mode for VM. This can be used to turn on/off verbose logging
- in many Qubes components at once (gui virtualization, VM kernel, some other
- services).
- For template-based HVM, enabling debug mode also disables automatic reset
- root.img (actually volatile.img) before each VM startup, so changes made to
- root filesystem stays intact. To force reset root.img when debug mode
- enabled, either change something in the template (simple start+stop will
- do, even touch its root.img is enough), or remove VM's volatile.img
- (check the path with qvm-prefs).
-
-qrexec_installed
- Accepted values: ``True``, ``False``
-
- This HVM have qrexec agent installed. When VM have qrexec agent installed,
- one can use qvm-run to start VM process, VM will benefit from Qubes RPC
- services (like file copy, or inter-vm clipboard). This option will be
- automatically turned on during Qubes Windows Tools installation, but if you
- install qrexec agent in some other OS, you need to turn this option on
- manually.
-
-guiagent_installed
- Accepted values: ``True``, ``False``
-
- This HVM have gui agent installed. This option disables full screen GUI
- virtualization and enables per-window seemless GUI mode. This option will
- be automatically turned on during Qubes Windows Tools installation, but if
- you install Qubes gui agent in some other OS, you need to turn this option
- on manually. You can turn this option off to troubleshoot some early HVM OS
- boot problems (enter safe mode etc), but the option will be automatically
- enabled at first VM normal startup (and will take effect from the next
- startup).
-
- *Notice:* when Windows GUI agent is installed in the VM, SVGA device (used
- to full screen video) is disabled, so even if you disable this
- option, you will not get functional full desktop access (on normal VM
- startup). Use some other means for that (VNC, RDP or so).
-
-autostart
- Accepted values: ``True``, ``False``
-
- Start the VM during system startup. The default netvm is autostarted
- regardless of this setting.
-
-timezone
- Accepted values: ``localtime``, time offset in seconds
-
- Set emulated HVM clock timezone. Use ``localtime`` (the default) to use the
- same time as dom0 have. Note that HVM will get only clock value, not the
- timezone itself, so if you use ``localtime`` setting, OS inside of HVM
- should also be configured to treat hardware clock as local time (and have
- proper timezone set).
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qvm-tools/qvm-remove.rst b/doc/qvm-tools/qvm-remove.rst
deleted file mode 100644
index b9309ee8..00000000
--- a/doc/qvm-tools/qvm-remove.rst
+++ /dev/null
@@ -1,28 +0,0 @@
-==========
-qvm-remove
-==========
-
-NAME
-====
-qvm-remove - remove a VM
-
-SYNOPSIS
-========
-| qvm-remove [options]
-
-OPTIONS
-=======
--h, --help
- Show this help message and exit
--q, --quiet
- Be quiet
---just-db
- Remove only from qubes.xml; do not remove any files
---force-root
- Force to run, even with root privileges
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qvm-tools/qvm-revert-template-changes.rst b/doc/qvm-tools/qvm-revert-template-changes.rst
deleted file mode 100644
index 519ccafa..00000000
--- a/doc/qvm-tools/qvm-revert-template-changes.rst
+++ /dev/null
@@ -1,24 +0,0 @@
-===========================
-qvm-revert-template-changes
-===========================
-
-NAME
-====
-qvm-revert-template-changes
-
-SYNOPSIS
-========
-| qvm-revert-template-changes [options]
-
-OPTIONS
-=======
--h, --help
- Show this help message and exit
---force
- Do not prompt for confirmation
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qvm-tools/qvm-run.rst b/doc/qvm-tools/qvm-run.rst
deleted file mode 100644
index 5e714e45..00000000
--- a/doc/qvm-tools/qvm-run.rst
+++ /dev/null
@@ -1,62 +0,0 @@
-=======
-qvm-run
-=======
-
-NAME
-====
-qvm-run - run a command on a specified VM
-
-SYNOPSIS
-========
-| qvm-run [options] [] []
-
-OPTIONS
-=======
--h, --help
- Show this help message and exit
--q, --quiet
- Be quiet
--a, --auto
- Auto start the VM if not running
--u USER, --user=USER
- Run command in a VM as a specified user
---tray
- Use tray notifications instead of stdout
---all
- Run command on all currently running VMs (or all paused, in case of --unpause)
---exclude=EXCLUDE_LIST
- When --all is used: exclude this VM name (might be repeated)
---wait
- Wait for the VM(s) to shutdown
---shutdown
- (deprecated) Do 'xl shutdown' for the VM(s) (can be combined this with --all and --wait)
---pause
- Do 'xl pause' for the VM(s) (can be combined this with --all and --wait)
---unpause
- Do 'xl unpause' for the VM(s) (can be combined this with --all and --wait)
--p, --pass-io
- Pass stdin/stdout/stderr from remote program
---localcmd=LOCALCMD
- With --pass-io, pass stdin/stdout/stderr to the given program
---nogui
- Run command without gui
---filter-escape-chars
- Filter terminal escape sequences (default if output is terminal)
---no-filter-escape-chars
- Do not filter terminal escape sequences - overrides --filter-escape-chars, DANGEROUS when output is terminal
---no-color-output
- Disable marking VM output with red color
---no-color-stderr
- Disable marking VM stderr with red color
---color-output
- Force marking VM output with given ANSI style (use 31 for red)
---color-stderr
- Force marking VM stderr with given ANSI style (use 31 for red)
---force
- Force operation, even if may damage other VMs (eg. shutdown of NetVM)
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qvm-tools/qvm-service.rst b/doc/qvm-tools/qvm-service.rst
deleted file mode 100644
index 720f8bc4..00000000
--- a/doc/qvm-tools/qvm-service.rst
+++ /dev/null
@@ -1,133 +0,0 @@
-===========
-qvm-service
-===========
-
-NAME
-====
-qvm-service - manage (Qubes-specific) services started in VM
-
-SYNOPSIS
-========
-| qvm-service [-l]
-| qvm-service [-e|-d|-D]
-
-OPTIONS
-=======
--h, --help
- Show this help message and exit
--l, --list
- List services (default action)
--e, --enable
- Enable service
--d, --disable
- Disable service
--D, --default
- Reset service to its default state (remove from the list). Default state
- means "lets VM choose" and can depend on VM type (NetVM, AppVM etc).
-
-SUPPORTED SERVICES
-==================
-
-This list can be incomplete as VM can implement any additional service without knowledge of qubes-core code.
-
-meminfo-writer
- Default: enabled everywhere excluding NetVM
-
- This service reports VM memory usage to dom0, which effectively enables dynamic memory management for the VM.
-
- *Note:* this service is enforced to be set by dom0 code. If you try to
- remove it (reset to default state), will be recreated with the rule: enabled
- if VM have no PCI devices assigned, otherwise disabled.
-
-qubes-dvm
- Default: disabled
-
- Used internally when creating DispVM savefile.
-
-qubes-firewall
- Default: enabled only in ProxyVM
-
- Dynamic firewall manager, based on settings in dom0 (qvm-firewall, firewall tab in qubes-manager).
- This service is not supported in netvms.
-
-qubes-network
- Default: enabled only in NetVM and ProxyVM
-
- Expose network for other VMs. This includes enabling network forwarding, MASQUERADE, DNS redirection and basic firewall.
-
-qubes-netwatcher
- Default: enabled only in ProxyVM
-
- Monitor IP change notification from NetVM. When received, reload qubes-firewall service (to force DNS resolution).
- This service makes sense only with qubes-firewall enabled.
-
-qubes-update-check
- Default: enabled
-
- Notify dom0 about updates available for this VM. This is shown in qubes-manager as 'update-pending' flag.
-
-cups
- Default: enabled only in AppVM
-
- Enable CUPS service. The user can disable cups in VM which do not need printing to speed up booting.
-
-crond
- Default: disabled
-
- Enable CRON service.
-
-network-manager
- Default: enabled in NetVM
-
- Enable NetworkManager. Only VM with direct access to network device needs
- this service, but can be useful in ProxyVM to ease VPN setup.
-
-ntpd
- Default: disabled
-
- Enable NTPD service. By default Qubes calls ntpdate every 6 minutes in
- selected VM (aka ClockVM), then propagate the result using qrexec calls.
- Enabling ntpd *do not* disable this behaviour.
-
-qubes-yum-proxy
- Deprecated name for qubes-updates-proxy.
-
-qubes-updates-proxy
- Default: enabled in NetVM
-
- Provide proxy service, which allow access only to yum repos. Filtering is
- done based on URLs, so it shouldn't be used as leak control (pretty easy to
- bypass), but is enough to prevent some erroneous user actions.
-
-yum-proxy-setup
- Deprecated name for updates-proxy-setup.
-
-updates-proxy-setup
- Default: enabled in AppVM (also in templates)
-
- Setup yum at startup to use qubes-yum-proxy service.
-
- *Note:* this service is automatically enabled when you allow VM to access
- yum proxy (in firewall settings) and disabled when you deny access to yum
- proxy.
-
-disable-default-route
- Default: disabled
-
- Disables the default route for networking. Enabling this service
- will prevent the creation of the default route, but the VM will
- still be able to reach it's direct neighbors. The functionality
- is implemented in /usr/lib/qubes/setup-ip.
-
-disable-dns-server
- Default: disabled
-
- Enabling this service will result in an empty /etc/resolv.conf.
- The functionality is implemented in /usr/lib/qubes/setup-ip.
-
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qvm-tools/qvm-shutdown.rst b/doc/qvm-tools/qvm-shutdown.rst
deleted file mode 100644
index ff7d81d3..00000000
--- a/doc/qvm-tools/qvm-shutdown.rst
+++ /dev/null
@@ -1,36 +0,0 @@
-============
-qvm-shutdown
-============
-
-NAME
-====
-qvm-shutdown
-
-:Date: 2012-04-11
-
-SYNOPSIS
-========
-| qvm-shutdown [options] [vm-name ...]
-
-OPTIONS
-=======
--h, --help
- Show this help message and exit
--q, --quiet
- Be quiet
---force
- Force operation, even if may damage other VMs (eg. shutdown of NetVM)
---wait
- Wait for the VM(s) to shutdown
---wait-time
- Timeout after which VM will be killed when --wait is used
---all
- Shutdown all running VMs
---exclude=EXCLUDE_LIST
- When --all is used: exclude this VM name (might be repeated)
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qvm-tools/qvm-start.rst b/doc/qvm-tools/qvm-start.rst
deleted file mode 100644
index f722d118..00000000
--- a/doc/qvm-tools/qvm-start.rst
+++ /dev/null
@@ -1,44 +0,0 @@
-=========
-qvm-start
-=========
-
-NAME
-====
-qvm-start - start a specified VM
-
-SYNOPSIS
-========
-| qvm-start [options]
-
-OPTIONS
-=======
--h, --help
- Show this help message and exit
--q, --quiet
- Be quiet
---tray
- Use tray notifications instead of stdout
---no-guid
- Do not start the GUId (ignored)
---drive
- Temporarily attach specified drive as CD/DVD or hard disk (can be specified with prefix 'hd' or 'cdrom:', default is cdrom)
---hddisk
- Temporarily attach specified drive as hard disk
---cdrom
- Temporarily attach specified drive as CD/DVD
---install-windows-tools
- Attach Windows tools CDROM to the VM
---dvm
- Do actions necessary when preparing DVM image
---custom-config=CUSTOM_CONFIG
- Use custom Xen config instead of Qubes-generated one
---skip-if-running
- Do no fail if the VM is already running
---debug
- Enable debug mode for this VM (until its shutdown)
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qvm-tools/qvm-template-commit.rst b/doc/qvm-tools/qvm-template-commit.rst
deleted file mode 100644
index f8957a78..00000000
--- a/doc/qvm-tools/qvm-template-commit.rst
+++ /dev/null
@@ -1,24 +0,0 @@
-===================
-qvm-template-commit
-===================
-
-NAME
-====
-qvm-template-commit
-
-SYNOPSIS
-========
-| qvm-template-commit [options]
-
-OPTIONS
-=======
--h, --help
- Show this help message and exit
---offline-mode
- Offline mode
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/qvm-tools/qvm-usb.rst b/doc/qvm-tools/qvm-usb.rst
deleted file mode 100644
index 7d2a90b3..00000000
--- a/doc/qvm-tools/qvm-usb.rst
+++ /dev/null
@@ -1,34 +0,0 @@
-=======
-qvm-usb
-=======
-
-NAME
-====
-qvm-usb - List/set VM USB devices
-
-SYNOPSIS
-========
-| qvm-usb -l [options]
-| qvm-usb -a [options] :
-| qvm-usb -d [options] :
-
-OPTIONS
-=======
--h, --help
- Show this help message and exit
--l, -list
- List devices
--a, --attach
- Attach specified device to specified VM
--d, --detach
- Detach specified device
---no-auto-detach
- Fail when device already connected to other VM
---force-root
- Force to run, even with root privileges
-
-AUTHORS
-=======
-| Joanna Rutkowska
-| Rafal Wojtczuk
-| Marek Marczykowski
diff --git a/doc/requirements.txt b/doc/requirements.txt
new file mode 100644
index 00000000..3dd3d0df
--- /dev/null
+++ b/doc/requirements.txt
@@ -0,0 +1,3 @@
+# WARNING: those requirements are used only for readthedocs.org
+# they SHOULD NOT be used under normal conditions; use system package manager
+lxml
diff --git a/doc/skel-manpage.py b/doc/skel-manpage.py
new file mode 100755
index 00000000..c650ebba
--- /dev/null
+++ b/doc/skel-manpage.py
@@ -0,0 +1,20 @@
+#!/usr/bin/python3
+
+import os
+import sys
+sys.path.insert(0, os.path.abspath('../'))
+
+import argparse
+import qubes.dochelpers
+
+parser = argparse.ArgumentParser(description='prepare new manpage for command')
+parser.add_argument('command', metavar='COMMAND',
+ help='program\'s command name; this should translate to '
+ 'qubes.tools.')
+
+def main():
+ args = parser.parse_args()
+ sys.stdout.write(qubes.dochelpers.prepare_manpage(args.command))
+
+if __name__ == '__main__':
+ main()
diff --git a/qmemman/qmemman.conf b/etc/qmemman.conf
similarity index 100%
rename from qmemman/qmemman.conf
rename to etc/qmemman.conf
diff --git a/linux/aux-tools/Makefile b/linux/aux-tools/Makefile
index 123845da..106e7e74 100644
--- a/linux/aux-tools/Makefile
+++ b/linux/aux-tools/Makefile
@@ -3,12 +3,7 @@ all:
install:
mkdir -p $(DESTDIR)/usr/lib/qubes
- cp unbind-pci-device.sh $(DESTDIR)/usr/lib/qubes
cp cleanup-dispvms $(DESTDIR)/usr/lib/qubes
cp startup-misc.sh $(DESTDIR)/usr/lib/qubes
- cp prepare-volatile-img.sh $(DESTDIR)/usr/lib/qubes
- cp vusb-ctl.py $(DESTDIR)/usr/lib/qubes/
- cp xl-qvm-usb-attach.py $(DESTDIR)/usr/lib/qubes/
- cp xl-qvm-usb-detach.py $(DESTDIR)/usr/lib/qubes/
cp block-cleaner-daemon.py $(DESTDIR)/usr/lib/qubes/
cp fix-dir-perms.sh $(DESTDIR)/usr/lib/qubes/
diff --git a/linux/aux-tools/prepare-volatile-img.sh b/linux/aux-tools/prepare-volatile-img.sh
deleted file mode 100755
index 822affa9..00000000
--- a/linux/aux-tools/prepare-volatile-img.sh
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/bin/sh
-
-set -e
-
-if ! echo $PATH | grep -q sbin; then
- PATH=$PATH:/sbin:/usr/sbin
-fi
-
-FILENAME=$1
-ROOT_SIZE=$2
-SWAP_SIZE=$[ 1024 ]
-
-if [ -z "$ROOT_SIZE" -o -z "$FILENAME" ]; then
- echo "Usage: $0 "
- exit 1
-fi
-
-if [ -e "$FILENAME" ]; then
- echo "$FILENAME already exists, not overriding"
- exit 1
-fi
-
-umask 002
-TOTAL_SIZE=$[ $ROOT_SIZE + $SWAP_SIZE + 512 ]
-truncate -s ${TOTAL_SIZE}M "$FILENAME"
diff --git a/linux/aux-tools/startup-misc.sh b/linux/aux-tools/startup-misc.sh
index 8b5f8081..7f2ab414 100755
--- a/linux/aux-tools/startup-misc.sh
+++ b/linux/aux-tools/startup-misc.sh
@@ -3,8 +3,6 @@
# Misc dom0 startup setup
/usr/lib/qubes/fix-dir-perms.sh
-xenstore-write /local/domain/0/name dom0
-xenstore-write domid 0
DOM0_MAXMEM=`/usr/sbin/xl info | grep total_memory | awk '{ print $3 }'`
xenstore-write /local/domain/0/memory/static-max $[ $DOM0_MAXMEM * 1024 ]
diff --git a/linux/aux-tools/unbind-pci-device.sh b/linux/aux-tools/unbind-pci-device.sh
deleted file mode 100755
index f3839949..00000000
--- a/linux/aux-tools/unbind-pci-device.sh
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/bin/sh
-#
-# The Qubes OS Project, http://www.qubes-os.org
-#
-# Copyright (C) 2010 Joanna Rutkowska
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-#
-
-BDF=$1
-if [ x$BDF = x ] ; then
- echo "usage: $0 "
- exit 0
-fi
-BDF=0000:$BDF
-#echo -n "Binding device $BDF to xen-pciback..."
-if [ -e /sys/bus/pci/drivers/pciback/$BDF ]; then
- # Already bound to pciback
- # Check if device not assigned to any RUNNING VM
- XS_PATH=/local/domain/0/backend/pci
- GREP_RE="^$XS_PATH/[0-9]*/[0-9]*/dev-[0-9]* = \"$BDF\""
- if xenstore-ls -f $XS_PATH 2> /dev/null | grep -q "$GREP_RE"; then
- DOMID=`xenstore-ls -f $XS_PATH | grep "$GREP_RE"|cut -d/ -f7`
- echo "ERROR: Device already attached to the running VM '`xl domname $DOMID`'" >&2
- exit 1
- fi
- exit 0
-fi
-
-if [ -e /sys/bus/pci/devices/$BDF/driver/unbind ] ; then
- echo -n $BDF > /sys/bus/pci/devices/$BDF/driver/unbind || exit 1
-fi
-echo -n $BDF > /sys/bus/pci/drivers/pciback/new_slot || exit 1
-echo -n $BDF > /sys/bus/pci/drivers/pciback/bind || exit 1
-#echo ok
diff --git a/linux/aux-tools/vusb-ctl.py b/linux/aux-tools/vusb-ctl.py
deleted file mode 100755
index eae621c8..00000000
--- a/linux/aux-tools/vusb-ctl.py
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/usr/bin/python
-
-##
-## Python script wrapper around xen.util.vusb_util bind_usb_device() and unbind_usb_device() methods
-## Run as root in usbvm
-##
-
-from xen.util import vusb_util
-import sys
-import os
-
-if len(sys.argv)!=3:
- print 'usage: vusb-ctl device'
- sys.exit(1)
-
-device=sys.argv[2]
-if sys.argv[1] == 'bind':
- vusb_util.bind_usb_device(device)
-elif sys.argv[1] == 'unbind':
- vusb_util.unbind_usb_device(device)
-else:
- print "Invalid command, must be 'bind' or 'unbind'"
- sys.exit(1)
-
diff --git a/linux/aux-tools/xl-qvm-usb-attach.py b/linux/aux-tools/xl-qvm-usb-attach.py
deleted file mode 100755
index 58ac2a8c..00000000
--- a/linux/aux-tools/xl-qvm-usb-attach.py
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/usr/bin/python
-
-##
-## This script is for dom0
-## The syntax is modelled after "xl block-attach"
-##
-
-import sys
-import os
-import xen.lowlevel.xl
-
-
-# parse command line
-if (len(sys.argv)<4) or (len(sys.argv)>5):
- print 'usage: xl-qvm-usb-attach.py []'
- sys.exit(1)
-
-frontendvm_xid=sys.argv[1]
-backendvm_device=sys.argv[2]
-
-frontend=sys.argv[3].split('-')
-if len(frontend)!=2:
- print 'Error: frontendvm-device must be in - format'
- sys.exit(1)
-(controller, port)=frontend
-
-if len(sys.argv)>4:
- backendvm_xid=int(sys.argv[4])
- backendvm_name=xen.lowlevel.xl.ctx().domid_to_name(backendvm_xid)
-else:
- backendvm_xid=0
-
-# FIXME: command injection
-os.system("xenstore-write /local/domain/%s/backend/vusb/%s/%s/port/%s '%s'"
- % (backendvm_xid, frontendvm_xid, controller, port, backendvm_device))
-
-cmd = "/usr/lib/qubes/vusb-ctl.py bind '%s'" % backendvm_device
-if backendvm_xid == 0:
- os.system("sudo %s" % cmd)
-else:
- from qubes.qubes import QubesVmCollection
- qvm_collection = QubesVmCollection()
- qvm_collection.lock_db_for_reading()
- qvm_collection.load()
- qvm_collection.unlock_db()
-
- # launch
- qvm_collection.get_vm_by_name(backendvm_name).run(cmd, user="root")
diff --git a/linux/aux-tools/xl-qvm-usb-detach.py b/linux/aux-tools/xl-qvm-usb-detach.py
deleted file mode 100755
index e32fe479..00000000
--- a/linux/aux-tools/xl-qvm-usb-detach.py
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/usr/bin/python
-
-##
-## This script is for dom0
-## The syntax is modelled after "xl block-attach"
-## FIXME: should be modelled after block-detach instead
-##
-
-import sys
-import os
-import xen.lowlevel.xl
-
-# parse command line
-if (len(sys.argv)<4) or (len(sys.argv)>5):
- print 'usage: xl-qvm-usb-detach.py []'
- sys.exit(1)
-
-frontendvm_xid=sys.argv[1]
-backendvm_device=sys.argv[2]
-
-frontend=sys.argv[3].split('-')
-if len(frontend)!=2:
- print 'Error: frontendvm-device must be in - format'
- sys.exit(1)
-(controller, port)=frontend
-
-if len(sys.argv)>4:
- backendvm_xid=int(sys.argv[4])
- backendvm_name=xen.lowlevel.xl.ctx().domid_to_name(backendvm_xid)
-else:
- backendvm_xid=0
-
-cmd = "/usr/lib/qubes/vusb-ctl.py unbind '%s'" % backendvm_device
-if backendvm_xid == 0:
- os.system("sudo %s" % cmd)
-else:
- from qubes.qubes import QubesVmCollection
- qvm_collection = QubesVmCollection()
- qvm_collection.lock_db_for_reading()
- qvm_collection.load()
- qvm_collection.unlock_db()
-
- # launch
- qvm_collection.get_vm_by_name(backendvm_name).run(cmd, user="root")
-
-# FIXME: command injection
-os.system("xenstore-write /local/domain/%s/backend/vusb/%s/%s/port/%s ''"
- % (backendvm_xid, frontendvm_xid, controller, port))
-
diff --git a/linux/system-config/Makefile b/linux/system-config/Makefile
index 51536153..cdcf4b48 100644
--- a/linux/system-config/Makefile
+++ b/linux/system-config/Makefile
@@ -7,5 +7,8 @@ install:
cp block-snapshot $(DESTDIR)/etc/xen/scripts
ln -s block-snapshot $(DESTDIR)/etc/xen/scripts/block-origin
install -d $(DESTDIR)/etc/xdg/autostart
- install -m 0644 qubes-guid.desktop $(DESTDIR)/etc/xdg/autostart/
+ install -m 0644 qrexec-policy-agent.desktop $(DESTDIR)/etc/xdg/autostart/
install -m 0644 -D tmpfiles-qubes.conf $(DESTDIR)/usr/lib/tmpfiles.d/qubes.conf
+ install -d $(DESTDIR)/etc/dbus-1/system.d
+ install -m 0644 dbus-org.qubesos.PolicyAgent.conf \
+ $(DESTDIR)/etc/dbus-1/system.d/org.qubesos.PolicyAgent.conf
diff --git a/linux/system-config/block-snapshot b/linux/system-config/block-snapshot
index 4f0b80c2..a922989e 100755
--- a/linux/system-config/block-snapshot
+++ b/linux/system-config/block-snapshot
@@ -53,14 +53,21 @@ get_dev() {
}
get_dm_snapshot_name() {
+ local base cow cow2
+
base=$1
cow=$2
+ cow2=$3
- echo snapshot-$(stat -c '%D:%i' "$base")-$(stat -c '%D:%i' "$cow")
+ name="snapshot-$(stat -c '%D:%i' "$base")-$(stat -c '%D:%i' "$cow")"
+ if [ -n "$cow2" ]; then
+ name="$name-$(stat -c '%D:%i' "$cow2")"
+ fi
+ echo "$name"
}
create_dm_snapshot() {
- local base_dev cow_dev base_sz
+ local base_dev cow_dev base_sz base cow dm_devname
dm_devname=$1
base=$2
@@ -77,7 +84,7 @@ create_dm_snapshot() {
}
create_dm_snapshot_origin() {
- local base_dev base_sz
+ local base_dev base_sz dm_devname base
dm_devname=$1
base=$2
@@ -103,8 +110,14 @@ case "$command" in
fi
echo $p > "$HOTPLUG_STORE-params"
echo $t > "$HOTPLUG_STORE-type"
- base=${p/:*/}
- cow=${p/*:/}
+ base=${p%%:*}
+ cow=${p#*:}
+ cow2=${p##*:}
+ if [ "$cow" != "$cow2" ]; then
+ cow=${cow%:*}
+ else
+ cow2=""
+ fi
if [ -L "$base" ]; then
base=$(readlink -f "$base") || fatal "$base link does not exist."
@@ -114,6 +127,10 @@ case "$command" in
cow=$(readlink -f "$cow") || fatal "$cow link does not exist."
fi
+ if [ -L "$cow2" ]; then
+ cow2=$(readlink -f "$cow2") || fatal "$cow2 link does not exist."
+ fi
+
# first ensure that snapshot device exists (to write somewhere changes from snapshot-origin)
dm_devname=$(get_dm_snapshot_name "$base" "$cow")
@@ -122,6 +139,12 @@ case "$command" in
# prepare snapshot device
create_dm_snapshot $dm_devname "$base" "$cow"
+ if [ -n "$cow2" ]; then
+ dm_devname_full=$(get_dm_snapshot_name "$base" "$cow" "$cow2")
+ create_dm_snapshot "$dm_devname_full" "/dev/mapper/$dm_devname" "$cow2"
+ dm_devname="$dm_devname_full"
+ fi
+
if [ "$t" == "snapshot" ]; then
#that's all for snapshot, store name of prepared device
xenstore_write "$XENBUS_PATH/node" "/dev/mapper/$dm_devname"
@@ -152,8 +175,14 @@ case "$command" in
case $t in
snapshot|origin)
p=$3
- base=${p/:*/}
- cow=${p/*:/}
+ base=${p%%:*}
+ cow=${p#*:}
+ cow2=${p##*:}
+ if [ "$cow" != "$cow2" ]; then
+ cow=${cow%:*}
+ else
+ cow2=""
+ fi
if [ -L "$base" ]; then
base=$(readlink -f "$base") || fatal "$base link does not exist."
@@ -163,6 +192,10 @@ case "$command" in
cow=$(readlink -f "$cow") || fatal "$cow link does not exist."
fi
+ if [ -L "$cow2" ]; then
+ cow2=$(readlink -f "$cow2") || fatal "$cow2 link does not exist."
+ fi
+
# first ensure that snapshot device exists (to write somewhere changes from snapshot-origin)
dm_devname=$(get_dm_snapshot_name "$base" "$cow")
@@ -171,6 +204,12 @@ case "$command" in
# prepare snapshot device
create_dm_snapshot $dm_devname "$base" "$cow"
+ if [ -n "$cow2" ]; then
+ dm_devname_full=$(get_dm_snapshot_name "$base" "$cow" "$cow2")
+ create_dm_snapshot "$dm_devname_full" "/dev/mapper/$dm_devname" "$cow2"
+ dm_devname="$dm_devname_full"
+ fi
+
if [ "$t" == "snapshot" ]; then
#that's all for snapshot, store name of prepared device
echo "/dev/mapper/$dm_devname"
@@ -232,7 +271,7 @@ case "$command" in
fi
# get list of used (loop) devices
- deps="$(dmsetup deps $node | cut -d: -f2 | sed -e 's#(7, \([0-9]\+\))#/dev/loop\1#g')"
+ deps="$(dmsetup deps $node -o blkdevname | cut -d: -f2 | sed -e 's#(\([a-z0-9-]\+\))#/dev/\1#g')"
# if this is origin
if [ "${node/origin/}" != "$node" ]; then
@@ -241,7 +280,8 @@ case "$command" in
use_count=$(dmsetup info $snap|grep Open|awk '{print $3}')
if [ "$use_count" -eq 0 ]; then
# unused snapshot - remove it
- deps="$deps $(dmsetup deps $snap | cut -d: -f2 | sed -e 's#(7, \([0-9]\+\))#/dev/loop\1#g')"
+ deps="$deps $(dmsetup deps $snap -o blkdevname | cut -d: -f2 |\
+ sed -e 's#(\([a-z0-9-]\+\))#/dev/\1#g')"
log debug "Removing $snap"
dmsetup remove $snap
fi
@@ -250,6 +290,10 @@ case "$command" in
# Commit template changes
domain=$(cat "$HOTPLUG_STORE-domain")
if [ "$domain" ]; then
+ if [ -r /var/lib/qubes/qubes-test.xml -a \
+ "${domain#test-}" != "$domain" ]; then
+ export QUBES_XML_PATH=/var/lib/qubes/qubes-test.xml
+ fi
# Dont stop on errors
/usr/bin/qvm-template-commit --offline-mode "$domain" || true
fi
@@ -261,11 +305,18 @@ case "$command" in
dmsetup remove $node
fi
- # try to free loop devices
+ # try to free unused devices
for dev in $deps; do
if [ -b "$dev" ]; then
log debug "Removing $dev"
- losetup -d $dev 2> /dev/null || true
+ case $dev in
+ /dev/loop*)
+ losetup -d $dev 2> /dev/null || true
+ ;;
+ /dev/dm-*)
+ dmsetup remove $dev 2> /dev/null || true
+ ;;
+ esac
fi
done
diff --git a/linux/system-config/dbus-org.qubesos.PolicyAgent.conf b/linux/system-config/dbus-org.qubesos.PolicyAgent.conf
new file mode 100644
index 00000000..e1dd2b9d
--- /dev/null
+++ b/linux/system-config/dbus-org.qubesos.PolicyAgent.conf
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/linux/system-config/qrexec-policy-agent.desktop b/linux/system-config/qrexec-policy-agent.desktop
new file mode 100644
index 00000000..0e1cd8d3
--- /dev/null
+++ b/linux/system-config/qrexec-policy-agent.desktop
@@ -0,0 +1,7 @@
+[Desktop Entry]
+Name=Qubes Qrexec Policy agent
+Comment=Agent for handling policy confirmation prompts
+Icon=qubes
+Exec=qrexec-policy-agent
+Terminal=false
+Type=Application
diff --git a/linux/system-config/qubes-guid.desktop b/linux/system-config/qubes-guid.desktop
deleted file mode 100644
index 0bf5e3d1..00000000
--- a/linux/system-config/qubes-guid.desktop
+++ /dev/null
@@ -1,7 +0,0 @@
-[Desktop Entry]
-Name=Qubes Guid
-Comment=Starts Dom0 GUI daemon for Qubes VMs
-Icon=qubes
-Exec=qvm-run --all true
-Terminal=false
-Type=Application
diff --git a/linux/systemd/Makefile b/linux/systemd/Makefile
index 8f035146..7cebae40 100644
--- a/linux/systemd/Makefile
+++ b/linux/systemd/Makefile
@@ -11,3 +11,5 @@ install:
cp qubes-vm@.service $(DESTDIR)$(UNITDIR)
cp qubes-reload-firewall@.service $(DESTDIR)$(UNITDIR)
cp qubes-reload-firewall@.timer $(DESTDIR)$(UNITDIR)
+ cp qubes-qmemman.service $(DESTDIR)$(UNITDIR)
+ cp qubesd.service $(DESTDIR)$(UNITDIR)
diff --git a/linux/systemd/qubes-netvm.service b/linux/systemd/qubes-netvm.service
index bf556f3c..4c201cee 100644
--- a/linux/systemd/qubes-netvm.service
+++ b/linux/systemd/qubes-netvm.service
@@ -9,7 +9,7 @@ Group=qubes
Environment=DISPLAY=:0
RemainAfterExit=yes
KillMode=none
-ExecStart=/bin/sh -c 'NETVM=`qubes-prefs --get default-netvm`; [ -n "$NETVM" ] && qvm-start -q --no-guid $NETVM'
+ExecStart=/bin/sh -c 'NETVM=`qubes-prefs --force-root default-netvm`; [ -n "$NETVM" ] && qvm-start -q --no-guid $NETVM'
[Install]
WantedBy=multi-user.target
diff --git a/qmemman/qubes-qmemman.service b/linux/systemd/qubes-qmemman.service
similarity index 78%
rename from qmemman/qubes-qmemman.service
rename to linux/systemd/qubes-qmemman.service
index 1b8ae566..e8832d03 100644
--- a/qmemman/qubes-qmemman.service
+++ b/linux/systemd/qubes-qmemman.service
@@ -4,7 +4,7 @@ After=qubes-core.service
[Service]
Type=notify
-ExecStart=/usr/lib/qubes/qmemman_daemon.py
+ExecStart=/usr/bin/qmemmand
StandardOutput=syslog
[Install]
diff --git a/linux/systemd/qubesd.service b/linux/systemd/qubesd.service
new file mode 100644
index 00000000..0e8d54cc
--- /dev/null
+++ b/linux/systemd/qubesd.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=Qubes OS daemon
+
+[Service]
+Type=notify
+ExecStart=/usr/bin/qubesd
+StandardOutput=syslog
+
+[Install]
+WantedBy=multi-user.target
diff --git a/qmemman/Makefile b/qmemman/Makefile
deleted file mode 100644
index 6c04a458..00000000
--- a/qmemman/Makefile
+++ /dev/null
@@ -1,24 +0,0 @@
-PYTHON_QUBESPATH = $(PYTHON_SITEPATH)/qubes
-SYSCONFDIR ?= /etc
-UNITDIR ?= /usr/lib/systemd/system
-all:
- python -m compileall .
- python -O -m compileall .
-
-clean:
- rm -f *.pyo
-
-install:
-ifndef PYTHON_SITEPATH
- $(error PYTHON_SITEPATH not defined)
-endif
- mkdir -p $(DESTDIR)$(PYTHON_QUBESPATH)
- cp qmemman*py $(DESTDIR)$(PYTHON_QUBESPATH)
- cp qmemman*py[co] $(DESTDIR)$(PYTHON_QUBESPATH)
- mkdir -p $(DESTDIR)$(SYSCONFDIR)/qubes
- cp qmemman.conf $(DESTDIR)$(SYSCONFDIR)/qubes/
- mkdir -p $(DESTDIR)/usr/lib/qubes
- cp server.py $(DESTDIR)/usr/lib/qubes/qmemman_daemon.py
- mkdir -p $(DESTDIR)$(UNITDIR)
- cp qubes-qmemman.service $(DESTDIR)$(UNITDIR)
-
diff --git a/qmemman/server.py b/qmemman/server.py
deleted file mode 100755
index a5447425..00000000
--- a/qmemman/server.py
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/usr/bin/python2
-# -*- coding: utf-8 -*-
-#
-# The Qubes OS Project, http://www.qubes-os.org
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-#
-from qubes.qmemman_server import QMemmanServer
-
-QMemmanServer.main()
diff --git a/qubes-rpc-policy/qubes.FeaturesRequest.policy b/qubes-rpc-policy/qubes.FeaturesRequest.policy
new file mode 100644
index 00000000..0f00b0b6
--- /dev/null
+++ b/qubes-rpc-policy/qubes.FeaturesRequest.policy
@@ -0,0 +1,6 @@
+## Note that policy parsing stops at the first match,
+## so adding anything below "$anyvm $anyvm action" line will have no effect
+
+## Please use a single # to start your custom comments
+
+$anyvm dom0 allow
diff --git a/qubes-rpc/qubes.FeaturesRequest b/qubes-rpc/qubes.FeaturesRequest
new file mode 100755
index 00000000..b0ec5c19
--- /dev/null
+++ b/qubes-rpc/qubes.FeaturesRequest
@@ -0,0 +1,13 @@
+#!/usr/bin/env python2
+
+import os
+import qubes
+
+PREFIX = '/features-request/'
+
+app = qubes.Qubes()
+vm = app.domains[os.environ['QREXEC_REMOTE_DOMAIN']]
+vm.fire_event('features-request',
+ untrusted_features={key[len(PREFIX):]: vm.qdb.read(key)
+ for key in vm.qdb.list(PREFIX)})
+app.save()
diff --git a/qubes/__init__.py b/qubes/__init__.py
new file mode 100644
index 00000000..41e059e9
--- /dev/null
+++ b/qubes/__init__.py
@@ -0,0 +1,695 @@
+#
+# The Qubes OS Project, https://www.qubes-os.org/
+#
+# Copyright (C) 2010-2015 Joanna Rutkowska
+# Copyright (C) 2011-2015 Marek Marczykowski-Górecki
+#
+# Copyright (C) 2014-2015 Wojtek Porczyk
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+
+'''
+Qubes OS
+
+:copyright: © 2010-2015 Invisible Things Lab
+'''
+
+import builtins
+import collections
+import os
+import os.path
+import string
+
+import lxml.etree
+import qubes.config
+import qubes.events
+import qubes.exc
+
+__author__ = 'Invisible Things Lab'
+__license__ = 'GPLv2 or later'
+__version__ = 'R3'
+
+
+class Label(object):
+ '''Label definition for virtual machines
+
+ Label specifies colour of the padlock displayed next to VM's name.
+ When this is a :py:class:`qubes.vm.dispvm.DispVM`, padlock is overlayed
+ with recycling pictogram.
+
+ :param int index: numeric identificator of label
+ :param str color: colour specification as in HTML (``#abcdef``)
+ :param str name: label's name like "red" or "green"
+ '''
+
+ def __init__(self, index, color, name):
+ #: numeric identificator of label
+ self.index = index
+
+ #: colour specification as in HTML (``#abcdef``)
+ self.color = color
+
+ #: label's name like "red" or "green"
+ self.name = name
+
+ #: freedesktop icon name, suitable for use in
+ #: :py:meth:`PyQt4.QtGui.QIcon.fromTheme`
+ self.icon = 'appvm-' + name
+
+ #: freedesktop icon name, suitable for use in
+ #: :py:meth:`PyQt4.QtGui.QIcon.fromTheme` on DispVMs
+ self.icon_dispvm = 'dispvm-' + name
+
+
+ @classmethod
+ def fromxml(cls, xml):
+ '''Create label definition from XML node
+
+ :param lxml.etree._Element xml: XML node reference
+ :rtype: :py:class:`qubes.Label`
+ '''
+
+ index = int(xml.get('id').split('-', 1)[1])
+ color = xml.get('color')
+ name = xml.text
+
+ return cls(index, color, name)
+
+
+ def __xml__(self):
+ element = lxml.etree.Element(
+ 'label', id='label-{}'.format(self.index), color=self.color)
+ element.text = self.name
+ return element
+
+ def __str__(self):
+ return self.name
+
+ def __repr__(self):
+ return '{}({!r}, {!r}, {!r})'.format(
+ self.__class__.__name__,
+ self.index,
+ self.color,
+ self.name)
+
+ def __eq__(self, other):
+ if isinstance(other, Label):
+ return self.name == other.name
+ return NotImplemented
+
+ def __hash__(self):
+ return hash(self.name)
+
+ @builtins.property
+ def icon_path(self):
+ '''Icon path
+
+ .. deprecated:: 2.0
+ use :py:meth:`PyQt4.QtGui.QIcon.fromTheme` and :py:attr:`icon`
+ '''
+ return os.path.join(qubes.config.system_path['qubes_icon_dir'],
+ self.icon) + ".png"
+
+
+ @builtins.property
+ def icon_path_dispvm(self):
+ '''Icon path
+
+ .. deprecated:: 2.0
+ use :py:meth:`PyQt4.QtGui.QIcon.fromTheme` and :py:attr:`icon_dispvm`
+ '''
+ return os.path.join(qubes.config.system_path['qubes_icon_dir'],
+ self.icon_dispvm) + ".png"
+
+
+class property(object): # pylint: disable=redefined-builtin,invalid-name
+ '''Qubes property.
+
+ This class holds one property that can be saved to and loaded from
+ :file:`qubes.xml`. It is used for both global and per-VM properties.
+
+ Property can be unset by ordinary ``del`` statement or assigning
+ :py:attr:`DEFAULT` special value to it. After deletion (or before first
+ assignment/load) attempting to read a property will get its default value
+ or, when no default, py:class:`exceptions.AttributeError`.
+
+ :param str name: name of the property
+ :param collections.Callable setter: if not :py:obj:`None`, this is used to \
+ initialise value; first parameter to the function is holder instance \
+ and the second is value; this is called before ``type``
+ :param collections.Callable saver: function to coerce value to something \
+ readable by setter
+ :param type type: if not :py:obj:`None`, value is coerced to this type
+ :param object default: default value; if callable, will be called with \
+ holder as first argument
+ :param int load_stage: stage when property should be loaded (see \
+ :py:class:`Qubes` for description of stages)
+ :param int order: order of evaluation (bigger order values are later)
+ :param bool clone: :py:meth:`PropertyHolder.clone_properties` will not \
+ include this property by default if :py:obj:`False`
+ :param str doc: docstring; this should be one paragraph of plain RST, no \
+ sphinx-specific features
+
+ Setters and savers have following signatures:
+
+ .. :py:function:: setter(self, prop, value)
+ :noindex:
+
+ :param self: instance of object that is holding property
+ :param prop: property object
+ :param value: value being assigned
+
+ .. :py:function:: saver(self, prop, value)
+ :noindex:
+
+ :param self: instance of object that is holding property
+ :param prop: property object
+ :param value: value being saved
+ :rtype: str
+ :raises property.DontSave: when property should not be saved at all
+
+ '''
+
+ #: Assigning this value to property means setting it to its default value.
+ #: If property has no default value, this will unset it.
+ DEFAULT = object()
+
+ # internal use only
+ _NO_DEFAULT = object()
+
+ def __init__(self, name, setter=None, saver=None, type=None,
+ default=_NO_DEFAULT, write_once=False, load_stage=2, order=0,
+ save_via_ref=False, clone=True,
+ doc=None):
+ # pylint: disable=redefined-builtin
+ self.__name__ = name
+ self._setter = setter
+ self._saver = saver if saver is not None else (
+ lambda self, prop, value: str(value))
+ self.type = type
+ self._default = default
+ self._write_once = write_once
+ self.order = order
+ self.load_stage = load_stage
+ self.save_via_ref = save_via_ref
+ self.clone = clone
+ self.__doc__ = doc
+ self._attr_name = '_qubesprop_' + name
+
+ def __get__(self, instance, owner):
+ if instance is None:
+ return self
+
+ # XXX this violates duck typing, shall we keep it?
+ if not isinstance(instance, PropertyHolder):
+ raise AttributeError('qubes.property should be used on '
+ 'qubes.PropertyHolder instances only')
+
+ try:
+ return getattr(instance, self._attr_name)
+
+ except AttributeError:
+ if self._default is self._NO_DEFAULT:
+ raise AttributeError(
+ 'property {!r} not set'.format(self.__name__))
+ elif isinstance(self._default, collections.Callable):
+ return self._default(instance)
+ else:
+ return self._default
+
+
+ def __set__(self, instance, value):
+ self._enforce_write_once(instance)
+
+ if value is self.__class__.DEFAULT:
+ self.__delete__(instance)
+ return
+
+ try:
+ oldvalue = getattr(instance, self.__name__)
+ has_oldvalue = True
+ except AttributeError:
+ has_oldvalue = False
+
+ if self._setter is not None:
+ value = self._setter(instance, self, value)
+ if self.type not in (None, type(value)):
+ value = self.type(value)
+
+ if has_oldvalue:
+ instance.fire_event_pre('property-pre-set:' + self.__name__,
+ name=self.__name__, newvalue=value, oldvalue=oldvalue)
+ else:
+ instance.fire_event_pre('property-pre-set:' + self.__name__,
+ name=self.__name__, newvalue=value)
+
+ instance._property_init(self, value) # pylint: disable=protected-access
+
+ if has_oldvalue:
+ instance.fire_event('property-set:' + self.__name__,
+ name=self.__name__, newvalue=value, oldvalue=oldvalue)
+ else:
+ instance.fire_event('property-set:' + self.__name__,
+ name=self.__name__, newvalue=value)
+
+
+ def __delete__(self, instance):
+ self._enforce_write_once(instance)
+
+ try:
+ oldvalue = getattr(instance, self._attr_name)
+ has_oldvalue = True
+ except AttributeError:
+ has_oldvalue = False
+
+ if has_oldvalue:
+ instance.fire_event_pre('property-pre-del:' + self.__name__,
+ name=self.__name__, oldvalue=oldvalue)
+ delattr(instance, self._attr_name)
+ instance.fire_event('property-del:' + self.__name__,
+ name=self.__name__, oldvalue=oldvalue)
+
+ else:
+ instance.fire_event_pre('property-pre-del:' + self.__name__,
+ name=self.__name__)
+ instance.fire_event('property-del:' + self.__name__,
+ name=self.__name__)
+
+
+ def __repr__(self):
+ default = ' default={!r}'.format(self._default) \
+ if self._default is not self._NO_DEFAULT \
+ else ''
+ return '<{} object at {:#x} name={!r}{}>'.format(
+ self.__class__.__name__, id(self), self.__name__, default) \
+
+
+ def __hash__(self):
+ return hash(self.__name__)
+
+ def __lt__(self, other):
+ if isinstance(other, property):
+ return (self.load_stage, self.order, self.__name__) <\
+ (other.load_stage, other.order, other.__name__)
+ return NotImplemented
+
+ def __eq__(self, other):
+ if isinstance(other, str):
+ return self.__name__ == other
+ return isinstance(other, property) and self.__name__ == other.__name__
+
+
+ def _enforce_write_once(self, instance):
+ if self._write_once and not instance.property_is_default(self):
+ raise AttributeError(
+ 'property {!r} is write-once and already set'.format(
+ self.__name__))
+
+ def sanitize(self, *, untrusted_newvalue):
+ '''Coarse sanitization of value to be set, before sending it to a
+ setter. Can raise QubesValueError if the value is invalid.
+
+ :param untrusted_newvalue: value to be validated
+ :return sanitized value
+ :raises qubes.exc.QubesValueError
+ '''
+ # do not treat type='str' as sufficient validation
+ if self.type is not None and self.type is not str:
+ # assume specific type will preform enough validation
+ if self.type is bool:
+ try:
+ untrusted_newvalue = untrusted_newvalue.decode('ascii')
+ except UnicodeDecodeError:
+ raise qubes.exc.QubesValueError
+ return self.bool(None, None, untrusted_newvalue)
+ else:
+ try:
+ return self.type(untrusted_newvalue)
+ except ValueError:
+ raise qubes.exc.QubesValueError
+ else:
+ # 'str' or not specified type
+ try:
+ untrusted_newvalue = untrusted_newvalue.decode('ascii',
+ errors='strict')
+ except UnicodeDecodeError:
+ raise qubes.exc.QubesValueError
+ allowed_set = string.printable
+ if not all(x in allowed_set for x in untrusted_newvalue):
+ raise qubes.exc.QubesValueError(
+ 'Invalid characters in property value')
+ return untrusted_newvalue
+
+
+ #
+ # exceptions
+ #
+
+ class DontSave(Exception):
+ '''This exception may be raised from saver to sign that property should
+ not be saved.
+ '''
+ pass
+
+ @staticmethod
+ def dontsave(self, prop, value):
+ '''Dummy saver that never saves anything.'''
+ # pylint: disable=bad-staticmethod-argument,unused-argument
+ raise property.DontSave()
+
+ #
+ # some setters provided
+ #
+
+ @staticmethod
+ def forbidden(self, prop, value):
+ '''Property setter that forbids loading a property.
+
+ This is used to effectively disable property in classes which inherit
+ unwanted property. When someone attempts to load such a property, it
+
+ :throws AttributeError: always
+ ''' # pylint: disable=bad-staticmethod-argument,unused-argument
+
+ raise AttributeError(
+ 'setting {} property on {} instance is forbidden'.format(
+ prop.__name__, self.__class__.__name__))
+
+
+ @staticmethod
+ def bool(self, prop, value):
+ '''Property setter for boolean properties.
+
+ It accepts (case-insensitive) ``'0'``, ``'no'`` and ``false`` as
+ :py:obj:`False` and ``'1'``, ``'yes'`` and ``'true'`` as
+ :py:obj:`True`.
+ ''' # pylint: disable=bad-staticmethod-argument,unused-argument
+
+ if isinstance(value, str):
+ lcvalue = value.lower()
+ if lcvalue in ('0', 'no', 'false', 'off'):
+ return False
+ if lcvalue in ('1', 'yes', 'true', 'on'):
+ return True
+ raise qubes.exc.QubesValueError(
+ 'Invalid literal for boolean property: {!r}'.format(value))
+
+ return bool(value)
+
+
+def stateless_property(func):
+ '''Decorator similar to :py:class:`builtins.property`, but for properties
+ exposed through management API (including qvm-prefs etc)'''
+ return property(func.__name__,
+ setter=property.forbidden,
+ saver=property.DontSave,
+ default=func,
+ doc=func.__doc__)
+
+
+class PropertyHolder(qubes.events.Emitter):
+ '''Abstract class for holding :py:class:`qubes.property`
+
+ Events fired by instances of this class:
+
+ .. event:: property-load (subject, event)
+
+ Fired once after all properties are loaded from XML. Individual
+ ``property-set`` events are not fired.
+
+ .. event:: property-set: \
+ (subject, event, name, newvalue[, oldvalue])
+
+ Fired when property changes state. Signature is variable,
+ *oldvalue* is present only if there was an old value.
+
+ :param name: Property name
+ :param newvalue: New value of the property
+ :param oldvalue: Old value of the property
+
+ .. event:: property-pre-set: \
+ (subject, event, name, newvalue[, oldvalue])
+
+ Fired before property changes state. Signature is variable,
+ *oldvalue* is present only if there was an old value.
+
+ :param name: Property name
+ :param newvalue: New value of the property
+ :param oldvalue: Old value of the property
+
+ .. event:: property-del: \
+ (subject, event, name[, oldvalue])
+
+ Fired when property gets deleted (is set to default). Signature is
+ variable, *oldvalue* is present only if there was an old value.
+
+ :param name: Property name
+ :param oldvalue: Old value of the property
+
+ .. event:: property-pre-del: \
+ (subject, event, name[, oldvalue])
+
+ Fired before property gets deleted (is set to default). Signature
+ is variable, *oldvalue* is present only if there was an old value.
+
+ :param name: Property name
+ :param oldvalue: Old value of the property
+
+ .. event:: clone-properties (subject, event, src, proplist)
+
+ :param src: object, from which we are cloning
+ :param proplist: list of properties
+
+ Members:
+ '''
+
+ def __init__(self, xml, **kwargs):
+ self.xml = xml
+
+ propvalues = {}
+
+ all_names = set(prop.__name__ for prop in self.property_list())
+ for key in list(kwargs):
+ if not key in all_names:
+ continue
+ propvalues[key] = kwargs.pop(key)
+
+ super(PropertyHolder, self).__init__(**kwargs)
+
+ for key, value in propvalues.items():
+ setattr(self, key, value)
+
+ if self.xml is not None:
+ # check if properties are appropriate
+ all_names = set(prop.__name__ for prop in self.property_list())
+
+ for node in self.xml.xpath('./properties/property'):
+ name = node.get('name')
+ if name not in all_names:
+ raise TypeError(
+ 'property {!r} not applicable to {!r}'.format(
+ name, self.__class__.__name__))
+
+ @classmethod
+ def property_list(cls, load_stage=None):
+ '''List all properties attached to this VM's class
+
+ :param load_stage: Filter by load stage
+ :type load_stage: :py:func:`int` or :py:obj:`None`
+ '''
+
+ props = set()
+ for class_ in cls.__mro__:
+ props.update(prop for prop in class_.__dict__.values()
+ if isinstance(prop, property))
+ if load_stage is not None:
+ props = set(prop for prop in props
+ if prop.load_stage == load_stage)
+ return sorted(props)
+
+ def _property_init(self, prop, value):
+ '''Initialise property to a given value, without side effects.
+
+ :param qubes.property prop: property object of particular interest
+ :param value: value
+ '''
+
+ # pylint: disable=protected-access
+ setattr(self, self.property_get_def(prop)._attr_name, value)
+
+
+ def property_is_default(self, prop):
+ '''Check whether property is in it's default value.
+
+ Properties when unset may return some default value, so
+ ``hasattr(vm, prop.__name__)`` is wrong in some circumstances. This
+ method allows for checking if the value returned is in fact it's
+ default value.
+
+ :param qubes.property prop: property object of particular interest
+ :rtype: bool
+ ''' # pylint: disable=protected-access
+
+ # both property_get_def() and ._attr_name may throw AttributeError,
+ # which we don't want to catch
+ attrname = self.property_get_def(prop)._attr_name
+ return not hasattr(self, attrname)
+
+
+ @classmethod
+ def property_get_def(cls, prop):
+ '''Return property definition object.
+
+ If prop is already :py:class:`qubes.property` instance, return the same
+ object.
+
+ :param prop: property object or name
+ :type prop: qubes.property or str
+ :rtype: qubes.property
+ '''
+
+ if isinstance(prop, qubes.property):
+ return prop
+
+ for p in cls.property_list():
+ if p.__name__ == prop:
+ return p
+
+ raise AttributeError('No property {!r} found in {!r}'.format(
+ prop, cls))
+
+
+ def load_properties(self, load_stage=None):
+ '''Load properties from immediate children of XML node.
+
+ ``property-set`` events are not fired for each individual property.
+
+ :param int load_stage: Stage of loading.
+ '''
+
+ if self.xml is None:
+ return
+ all_names = set(
+ prop.__name__ for prop in self.property_list(load_stage))
+ for node in self.xml.xpath('./properties/property'):
+ name = node.get('name')
+ value = node.get('ref') or node.text
+
+ if not name in all_names:
+ continue
+
+ setattr(self, name, value)
+
+
+ def xml_properties(self, with_defaults=False):
+ '''Iterator that yields XML nodes representing set properties.
+
+ :param bool with_defaults: If :py:obj:`True`, then it also includes \
+ properties which were not set explicite, but have default values \
+ filled.
+ '''
+
+
+ properties = lxml.etree.Element('properties')
+
+ for prop in self.property_list():
+ # pylint: disable=protected-access
+ try:
+ value = getattr(
+ self, (prop.__name__ if with_defaults else prop._attr_name))
+ except AttributeError:
+ continue
+
+ try:
+ value = prop._saver(self, prop, value)
+ except property.DontSave:
+ continue
+
+ element = lxml.etree.Element('property', name=prop.__name__)
+ if prop.save_via_ref:
+ element.set('ref', value)
+ else:
+ element.text = value
+ properties.append(element)
+
+ return properties
+
+
+ # this was clone_attrs
+ def clone_properties(self, src, proplist=None):
+ '''Clone properties from other object.
+
+ :param PropertyHolder src: source object
+ :param list proplist: list of properties \
+ (:py:obj:`None` or omit for all properties except those with \
+ :py:attr:`property.clone` set to :py:obj:`False`)
+ '''
+
+ if proplist is None:
+ proplist = [prop for prop in self.property_list()
+ if prop.clone]
+ else:
+ proplist = [prop for prop in self.property_list()
+ if prop.__name__ in proplist or prop in proplist]
+
+ for prop in proplist:
+ try:
+ # pylint: disable=protected-access
+ self._property_init(prop, getattr(src, prop._attr_name))
+ except AttributeError:
+ continue
+
+ self.fire_event('clone-properties', src=src, proplist=proplist)
+
+
+ def property_require(self, prop, allow_none=False, hard=False):
+ '''Complain badly when property is not set.
+
+ :param prop: property name or object
+ :type prop: qubes.property or str
+ :param bool allow_none: if :py:obj:`True`, don't complain if \
+ :py:obj:`None` is found
+ :param bool hard: if :py:obj:`True`, raise :py:class:`AssertionError`; \
+ if :py:obj:`False`, log warning instead
+ '''
+
+ if isinstance(prop, qubes.property):
+ prop = prop.__name__
+
+ try:
+ value = getattr(self, prop)
+ if value is None and not allow_none:
+ raise AttributeError()
+ except AttributeError:
+ # pylint: disable=no-member
+ msg = 'Required property {!r} not set on {!r}'.format(prop, self)
+ if hard:
+ raise AssertionError(msg)
+ else:
+ # pylint: disable=no-member
+ self.log.fatal(msg)
+
+# pylint: disable=wrong-import-position
+from qubes.vm import VMProperty
+from qubes.app import Qubes
+
+__all__ = [
+ 'Label',
+ 'PropertyHolder',
+ 'Qubes',
+ 'VMProperty',
+ 'property',
+]
diff --git a/qubes/api/__init__.py b/qubes/api/__init__.py
new file mode 100644
index 00000000..22abcbbb
--- /dev/null
+++ b/qubes/api/__init__.py
@@ -0,0 +1,177 @@
+# -*- encoding: utf8 -*-
+#
+# The Qubes OS Project, http://www.qubes-os.org
+#
+# Copyright (C) 2017 Wojtek Porczyk
+# Copyright (C) 2017 Marek Marczykowski-Górecki
+#
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, see .
+import asyncio
+import functools
+
+
+class ProtocolError(AssertionError):
+ '''Raised when something is wrong with data received'''
+ pass
+
+
+class PermissionDenied(Exception):
+ '''Raised deliberately by handlers when we decide not to cooperate'''
+ pass
+
+
+def method(name, *, no_payload=False, endpoints=None):
+ '''Decorator factory for methods intended to appear in API.
+
+ The decorated method can be called from public API using a child of
+ :py:class:`AbstractQubesMgmt` class. The method becomes "public", and can be
+ called using remote management interface.
+
+ :param str name: qrexec rpc method name
+ :param bool no_payload: if :py:obj:`True`, will barf on non-empty payload; \
+ also will not pass payload at all to the method
+
+ The expected function method should have one argument (other than usual
+ *self*), ``untrusted_payload``, which will contain the payload.
+
+ .. warning::
+ This argument has to be named such, to remind the programmer that the
+ content of this variable is indeed untrusted.
+
+ If *no_payload* is true, then the method is called with no arguments.
+ '''
+
+ def decorator(func):
+ if no_payload:
+ # the following assignment is needed for how closures work in Python
+ _func = func
+ @functools.wraps(_func)
+ def wrapper(self, untrusted_payload, **kwargs):
+ if untrusted_payload != b'':
+ raise ProtocolError('unexpected payload')
+ return _func(self, **kwargs)
+ func = wrapper
+
+ # pylint: disable=protected-access
+ if endpoints is None:
+ func._rpcname = ((name, None),)
+ else:
+ func._rpcname = tuple(
+ (name.format(endpoint=endpoint), endpoint)
+ for endpoint in endpoints)
+ return func
+
+ return decorator
+
+
+def apply_filters(iterable, filters):
+ '''Apply filters returned by mgmt-permission:... event'''
+ for selector in filters:
+ iterable = filter(selector, iterable)
+ return iterable
+
+
+class AbstractQubesAPI(object):
+ '''Common code for Qubes Management Protocol handling
+
+ Different interfaces can expose different API call sets, however they share
+ common protocol and common implementation framework. This class is the
+ latter.
+
+ To implement a new interface, inherit from this class and write at least one
+ method and decorate it with :py:func:`api` decorator. It will have access to
+ pre-defined attributes: :py:attr:`app`, :py:attr:`src`, :py:attr:`dest`,
+ :py:attr:`arg` and :py:attr:`method`.
+
+ There are also two helper functions for firing events associated with API
+ calls.
+ '''
+ def __init__(self, app, src, method_name, dest, arg, send_event=None):
+ #: :py:class:`qubes.Qubes` object
+ self.app = app
+
+ #: source qube
+ self.src = self.app.domains[src.decode('ascii')]
+
+ #: destination qube
+ self.dest = self.app.domains[dest.decode('ascii')]
+
+ #: argument
+ self.arg = arg.decode('ascii')
+
+ #: name of the method
+ self.method = method_name.decode('ascii')
+
+ #: callback for sending events if applicable
+ self.send_event = send_event
+
+ #: is this operation cancellable?
+ self.cancellable = False
+
+ untrusted_candidates = []
+ for attr in dir(self):
+ func = getattr(self, attr)
+
+ if not callable(func):
+ continue
+
+ try:
+ # pylint: disable=protected-access
+ for mname, endpoint in func._rpcname:
+ if mname != self.method:
+ continue
+ untrusted_candidates.append((func, endpoint))
+ except AttributeError:
+ continue
+
+ if not untrusted_candidates:
+ raise ProtocolError('no such method: {!r}'.format(self.method))
+
+ assert len(untrusted_candidates) == 1, \
+ 'multiple candidates for method {!r}'.format(self.method)
+
+ #: the method to execute
+ self._handler = untrusted_candidates[0]
+ self._running_handler = None
+ del untrusted_candidates
+
+ def execute(self, *, untrusted_payload):
+ '''Execute management operation.
+
+ This method is a coroutine.
+ '''
+ handler, endpoint = self._handler
+ kwargs = {}
+ if endpoint is not None:
+ kwargs['endpoint'] = endpoint
+ self._running_handler = asyncio.ensure_future(handler(
+ untrusted_payload=untrusted_payload, **kwargs))
+ return self._running_handler
+
+ def cancel(self):
+ '''If operation is cancellable, interrupt it'''
+ if self.cancellable and self._running_handler is not None:
+ self._running_handler.cancel()
+
+
+ def fire_event_for_permission(self, **kwargs):
+ '''Fire an event on the source qube to check for permission'''
+ return self.src.fire_event_pre('mgmt-permission:{}'.format(self.method),
+ dest=self.dest, arg=self.arg, **kwargs)
+
+ def fire_event_for_filter(self, iterable, **kwargs):
+ '''Fire an event on the source qube to filter for permission'''
+ return apply_filters(iterable,
+ self.fire_event_for_permission(**kwargs))
diff --git a/qubes/api/admin.py b/qubes/api/admin.py
new file mode 100644
index 00000000..516eaa73
--- /dev/null
+++ b/qubes/api/admin.py
@@ -0,0 +1,684 @@
+#
+# The Qubes OS Project, https://www.qubes-os.org/
+#
+# Copyright (C) 2017 Wojtek Porczyk
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+
+'''
+Qubes OS Management API
+'''
+
+import asyncio
+import string
+
+import pkg_resources
+
+import qubes.api
+import qubes.storage
+import qubes.utils
+import qubes.vm
+import qubes.vm.qubesvm
+
+
+class QubesMgmtEventsDispatcher(object):
+ def __init__(self, filters, send_event):
+ self.filters = filters
+ self.send_event = send_event
+
+ def vm_handler(self, subject, event, **kwargs):
+ if event.startswith('mgmt-permission:'):
+ return
+ if not list(qubes.api.apply_filters([(subject, event, kwargs)],
+ self.filters)):
+ return
+ self.send_event(subject, event, **kwargs)
+
+ def app_handler(self, subject, event, **kwargs):
+ if not list(qubes.api.apply_filters([(subject, event, kwargs)],
+ self.filters)):
+ return
+ self.send_event(subject, event, **kwargs)
+
+ def on_domain_add(self, subject, event, vm):
+ # pylint: disable=unused-argument
+ vm.add_handler('*', self.vm_handler)
+
+ def on_domain_delete(self, subject, event, vm):
+ # pylint: disable=unused-argument
+ vm.remove_handler('*', self.vm_handler)
+
+
+class QubesAdminAPI(qubes.api.AbstractQubesAPI):
+ '''Implementation of Qubes Management API calls
+
+ This class contains all the methods available in the main API.
+
+ .. seealso::
+ https://www.qubes-os.org/doc/mgmt1/
+ '''
+
+ @qubes.api.method('admin.vmclass.List', no_payload=True)
+ @asyncio.coroutine
+ def vmclass_list(self):
+ '''List all VM classes'''
+ assert not self.arg
+ assert self.dest.name == 'dom0'
+
+ entrypoints = self.fire_event_for_filter(
+ pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT))
+
+ return ''.join('{}\n'.format(ep.name)
+ for ep in entrypoints)
+
+ @qubes.api.method('admin.vm.List', no_payload=True)
+ @asyncio.coroutine
+ def vm_list(self):
+ '''List all the domains'''
+ assert not self.arg
+
+ if self.dest.name == 'dom0':
+ domains = self.fire_event_for_filter(self.app.domains)
+ else:
+ domains = self.fire_event_for_filter([self.dest])
+
+ return ''.join('{} class={} state={}\n'.format(
+ vm.name,
+ vm.__class__.__name__,
+ vm.get_power_state())
+ for vm in sorted(domains))
+
+ @qubes.api.method('admin.vm.property.List', no_payload=True)
+ @asyncio.coroutine
+ def vm_property_list(self):
+ '''List all properties on a qube'''
+ assert not self.arg
+
+ properties = self.fire_event_for_filter(self.dest.property_list())
+
+ return ''.join('{}\n'.format(prop.__name__) for prop in properties)
+
+ @qubes.api.method('admin.vm.property.Get', no_payload=True)
+ @asyncio.coroutine
+ def vm_property_get(self):
+ '''Get a value of one property'''
+ assert self.arg in self.dest.property_list()
+
+ self.fire_event_for_permission()
+
+ property_def = self.dest.property_get_def(self.arg)
+ # explicit list to be sure that it matches protocol spec
+ if isinstance(property_def, qubes.vm.VMProperty):
+ property_type = 'vm'
+ elif property_def.type is int:
+ property_type = 'int'
+ elif property_def.type is bool:
+ property_type = 'bool'
+ elif self.arg == 'label':
+ property_type = 'label'
+ else:
+ property_type = 'str'
+
+ try:
+ value = getattr(self.dest, self.arg)
+ except AttributeError:
+ return 'default=True type={} '.format(property_type)
+ else:
+ return 'default={} type={} {}'.format(
+ str(self.dest.property_is_default(self.arg)),
+ property_type,
+ str(value) if value is not None else '')
+
+ @qubes.api.method('admin.vm.property.Set')
+ @asyncio.coroutine
+ def vm_property_set(self, untrusted_payload):
+ assert self.arg in self.dest.property_list()
+
+ property_def = self.dest.property_get_def(self.arg)
+ newvalue = property_def.sanitize(untrusted_newvalue=untrusted_payload)
+
+ self.fire_event_for_permission(newvalue=newvalue)
+
+ setattr(self.dest, self.arg, newvalue)
+ self.app.save()
+
+ @qubes.api.method('admin.vm.property.Help', no_payload=True)
+ @asyncio.coroutine
+ def vm_property_help(self):
+ '''Get help for one property'''
+ assert self.arg in self.dest.property_list()
+
+ self.fire_event_for_permission()
+
+ try:
+ doc = self.dest.property_get_def(self.arg).__doc__
+ except AttributeError:
+ return ''
+
+ return qubes.utils.format_doc(doc)
+
+ @qubes.api.method('admin.vm.property.Reset', no_payload=True)
+ @asyncio.coroutine
+ def vm_property_reset(self):
+ '''Reset a property to a default value'''
+ assert self.arg in self.dest.property_list()
+
+ self.fire_event_for_permission()
+
+ delattr(self.dest, self.arg)
+ self.app.save()
+
+ @qubes.api.method('admin.vm.volume.List', no_payload=True)
+ @asyncio.coroutine
+ def vm_volume_list(self):
+ assert not self.arg
+
+ volume_names = self.fire_event_for_filter(self.dest.volumes.keys())
+ return ''.join('{}\n'.format(name) for name in volume_names)
+
+ @qubes.api.method('admin.vm.volume.Info', no_payload=True)
+ @asyncio.coroutine
+ def vm_volume_info(self):
+ assert self.arg in self.dest.volumes.keys()
+
+ self.fire_event_for_permission()
+
+ volume = self.dest.volumes[self.arg]
+ # properties defined in API
+ volume_properties = [
+ 'pool', 'vid', 'size', 'usage', 'rw', 'internal', 'source',
+ 'save_on_stop', 'snap_on_start']
+ return ''.join('{}={}\n'.format(key, getattr(volume, key)) for key in
+ volume_properties)
+
+ @qubes.api.method('admin.vm.volume.ListSnapshots', no_payload=True)
+ @asyncio.coroutine
+ def vm_volume_listsnapshots(self):
+ assert self.arg in self.dest.volumes.keys()
+
+ volume = self.dest.volumes[self.arg]
+ revisions = [revision for revision in volume.revisions]
+ revisions = self.fire_event_for_filter(revisions)
+
+ return ''.join('{}\n'.format(revision) for revision in revisions)
+
+ @qubes.api.method('admin.vm.volume.Revert')
+ @asyncio.coroutine
+ def vm_volume_revert(self, untrusted_payload):
+ assert self.arg in self.dest.volumes.keys()
+ untrusted_revision = untrusted_payload.decode('ascii').strip()
+ del untrusted_payload
+
+ volume = self.dest.volumes[self.arg]
+ snapshots = volume.revisions
+ assert untrusted_revision in snapshots
+ revision = untrusted_revision
+
+ self.fire_event_for_permission(revision=revision)
+
+ self.dest.storage.get_pool(volume).revert(revision)
+ self.app.save()
+
+ @qubes.api.method('admin.vm.volume.Resize')
+ @asyncio.coroutine
+ def vm_volume_resize(self, untrusted_payload):
+ assert self.arg in self.dest.volumes.keys()
+ untrusted_size = untrusted_payload.decode('ascii').strip()
+ del untrusted_payload
+ assert untrusted_size.isdigit() # only digits, forbid '-' too
+ assert len(untrusted_size) <= 20 # limit to about 2^64
+
+ size = int(untrusted_size)
+
+ self.fire_event_for_permission(size=size)
+
+ self.dest.storage.resize(self.arg, size)
+ self.app.save()
+
+ @qubes.api.method('admin.pool.List', no_payload=True)
+ @asyncio.coroutine
+ def pool_list(self):
+ assert not self.arg
+ assert self.dest.name == 'dom0'
+
+ pools = self.fire_event_for_filter(self.app.pools)
+
+ return ''.join('{}\n'.format(pool) for pool in pools)
+
+ @qubes.api.method('admin.pool.ListDrivers', no_payload=True)
+ @asyncio.coroutine
+ def pool_listdrivers(self):
+ assert self.dest.name == 'dom0'
+ assert not self.arg
+
+ drivers = self.fire_event_for_filter(qubes.storage.pool_drivers())
+
+ return ''.join('{} {}\n'.format(
+ driver,
+ ' '.join(qubes.storage.driver_parameters(driver)))
+ for driver in drivers)
+
+ @qubes.api.method('admin.pool.Info', no_payload=True)
+ @asyncio.coroutine
+ def pool_info(self):
+ assert self.dest.name == 'dom0'
+ assert self.arg in self.app.pools.keys()
+
+ pool = self.app.pools[self.arg]
+
+ self.fire_event_for_permission(pool=pool)
+
+ return ''.join('{}={}\n'.format(prop, val)
+ for prop, val in sorted(pool.config.items()))
+
+ @qubes.api.method('admin.pool.Add')
+ @asyncio.coroutine
+ def pool_add(self, untrusted_payload):
+ assert self.dest.name == 'dom0'
+ drivers = qubes.storage.pool_drivers()
+ assert self.arg in drivers
+ untrusted_pool_config = untrusted_payload.decode('ascii').splitlines()
+ del untrusted_payload
+ assert all(('=' in line) for line in untrusted_pool_config)
+ # pairs of (option, value)
+ untrusted_pool_config = [line.split('=', 1)
+ for line in untrusted_pool_config]
+ # reject duplicated options
+ assert len(set(x[0] for x in untrusted_pool_config)) == \
+ len([x[0] for x in untrusted_pool_config])
+ # and convert to dict
+ untrusted_pool_config = dict(untrusted_pool_config)
+
+ assert 'name' in untrusted_pool_config
+ untrusted_pool_name = untrusted_pool_config.pop('name')
+ allowed_chars = string.ascii_letters + string.digits + '-_.'
+ assert all(c in allowed_chars for c in untrusted_pool_name)
+ pool_name = untrusted_pool_name
+ assert pool_name not in self.app.pools
+
+ driver_parameters = qubes.storage.driver_parameters(self.arg)
+ assert all(key in driver_parameters for key in untrusted_pool_config)
+ pool_config = untrusted_pool_config
+
+ self.fire_event_for_permission(name=pool_name,
+ pool_config=pool_config)
+
+ self.app.add_pool(name=pool_name, driver=self.arg, **pool_config)
+ self.app.save()
+
+ @qubes.api.method('admin.pool.Remove', no_payload=True)
+ @asyncio.coroutine
+ def pool_remove(self):
+ assert self.dest.name == 'dom0'
+ assert self.arg in self.app.pools.keys()
+
+ self.fire_event_for_permission()
+
+ self.app.remove_pool(self.arg)
+ self.app.save()
+
+ @qubes.api.method('admin.label.List', no_payload=True)
+ @asyncio.coroutine
+ def label_list(self):
+ assert self.dest.name == 'dom0'
+ assert not self.arg
+
+ labels = self.fire_event_for_filter(self.app.labels.values())
+
+ return ''.join('{}\n'.format(label.name) for label in labels)
+
+ @qubes.api.method('admin.label.Get', no_payload=True)
+ @asyncio.coroutine
+ def label_get(self):
+ assert self.dest.name == 'dom0'
+
+ try:
+ label = self.app.get_label(self.arg)
+ except KeyError:
+ raise qubes.exc.QubesValueError
+
+ self.fire_event_for_permission(label=label)
+
+ return label.color
+
+ @qubes.api.method('admin.label.Index', no_payload=True)
+ @asyncio.coroutine
+ def label_index(self):
+ assert self.dest.name == 'dom0'
+
+ try:
+ label = self.app.get_label(self.arg)
+ except KeyError:
+ raise qubes.exc.QubesValueError
+
+ self.fire_event_for_permission(label=label)
+
+ return str(label.index)
+
+ @qubes.api.method('admin.label.Create')
+ @asyncio.coroutine
+ def label_create(self, untrusted_payload):
+ assert self.dest.name == 'dom0'
+
+ # don't confuse label name with label index
+ assert not self.arg.isdigit()
+ allowed_chars = string.ascii_letters + string.digits + '-_.'
+ assert all(c in allowed_chars for c in self.arg)
+ try:
+ self.app.get_label(self.arg)
+ except KeyError:
+ # ok, no such label yet
+ pass
+ else:
+ raise qubes.exc.QubesValueError('label already exists')
+
+ untrusted_payload = untrusted_payload.decode('ascii').strip()
+ assert len(untrusted_payload) == 8
+ assert untrusted_payload.startswith('0x')
+ # besides prefix, only hex digits are allowed
+ assert all(x in string.hexdigits for x in untrusted_payload[2:])
+
+ # SEE: #2732
+ color = untrusted_payload
+
+ self.fire_event_for_permission(color=color)
+
+ # allocate new index, but make sure it's outside of default labels set
+ new_index = max(
+ qubes.config.max_default_label, *self.app.labels.keys()) + 1
+
+ label = qubes.Label(new_index, color, self.arg)
+ self.app.labels[new_index] = label
+ self.app.save()
+
+ @qubes.api.method('admin.label.Remove', no_payload=True)
+ @asyncio.coroutine
+ def label_remove(self):
+ assert self.dest.name == 'dom0'
+
+ try:
+ label = self.app.get_label(self.arg)
+ except KeyError:
+ raise qubes.exc.QubesValueError
+ # don't allow removing default labels
+ assert label.index > qubes.config.max_default_label
+
+ # FIXME: this should be in app.add_label()
+ for vm in self.app.domains:
+ if vm.label == label:
+ raise qubes.exc.QubesException('label still in use')
+
+ self.fire_event_for_permission(label=label)
+
+ del self.app.labels[label.index]
+ self.app.save()
+
+ @qubes.api.method('admin.vm.Start', no_payload=True)
+ @asyncio.coroutine
+ def vm_start(self):
+ assert not self.arg
+ self.fire_event_for_permission()
+ yield from self.dest.start()
+
+ @qubes.api.method('admin.vm.Shutdown', no_payload=True)
+ @asyncio.coroutine
+ def vm_shutdown(self):
+ assert not self.arg
+ self.fire_event_for_permission()
+ yield from self.dest.shutdown()
+
+ @qubes.api.method('admin.vm.Pause', no_payload=True)
+ @asyncio.coroutine
+ def vm_pause(self):
+ assert not self.arg
+ self.fire_event_for_permission()
+ yield from self.dest.pause()
+
+ @qubes.api.method('admin.vm.Unpause', no_payload=True)
+ @asyncio.coroutine
+ def vm_unpause(self):
+ assert not self.arg
+ self.fire_event_for_permission()
+ yield from self.dest.unpause()
+
+ @qubes.api.method('admin.vm.Kill', no_payload=True)
+ @asyncio.coroutine
+ def vm_kill(self):
+ assert not self.arg
+ self.fire_event_for_permission()
+ yield from self.dest.kill()
+
+ @qubes.api.method('admin.Events', no_payload=True)
+ @asyncio.coroutine
+ def events(self):
+ assert not self.arg
+
+ # run until client connection is terminated
+ self.cancellable = True
+ wait_for_cancel = asyncio.get_event_loop().create_future()
+
+ # cache event filters, to not call an event each time an event arrives
+ event_filters = self.fire_event_for_permission()
+
+ dispatcher = QubesMgmtEventsDispatcher(event_filters, self.send_event)
+ if self.dest.name == 'dom0':
+ self.app.add_handler('*', dispatcher.app_handler)
+ self.app.add_handler('domain-add', dispatcher.on_domain_add)
+ self.app.add_handler('domain-delete', dispatcher.on_domain_delete)
+ for vm in self.app.domains:
+ vm.add_handler('*', dispatcher.vm_handler)
+ else:
+ self.dest.add_handler('*', dispatcher.vm_handler)
+
+ # send artificial event as a confirmation that connection is established
+ self.send_event(self.app, 'connection-established')
+
+ try:
+ yield from wait_for_cancel
+ except asyncio.CancelledError:
+ # the above waiting was already interrupted, this is all we need
+ pass
+
+ if self.dest.name == 'dom0':
+ self.app.remove_handler('*', dispatcher.app_handler)
+ self.app.remove_handler('domain-add', dispatcher.on_domain_add)
+ self.app.remove_handler('domain-delete',
+ dispatcher.on_domain_delete)
+ for vm in self.app.domains:
+ vm.remove_handler('*', dispatcher.vm_handler)
+ else:
+ self.dest.remove_handler('*', dispatcher.vm_handler)
+
+ @qubes.api.method('admin.vm.feature.List', no_payload=True)
+ @asyncio.coroutine
+ def vm_feature_list(self):
+ assert not self.arg
+ features = self.fire_event_for_filter(self.dest.features.keys())
+ return ''.join('{}\n'.format(feature) for feature in features)
+
+ @qubes.api.method('admin.vm.feature.Get', no_payload=True)
+ @asyncio.coroutine
+ def vm_feature_get(self):
+ # validation of self.arg done by qrexec-policy is enough
+
+ self.fire_event_for_permission()
+ try:
+ value = self.dest.features[self.arg]
+ except KeyError:
+ raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
+ return value
+
+ @qubes.api.method('admin.vm.feature.CheckWithTemplate', no_payload=True)
+ @asyncio.coroutine
+ def vm_feature_checkwithtemplate(self):
+ # validation of self.arg done by qrexec-policy is enough
+
+ self.fire_event_for_permission()
+ try:
+ value = self.dest.features.check_with_template(self.arg)
+ except KeyError:
+ raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
+ return value
+
+ @qubes.api.method('admin.vm.feature.Remove', no_payload=True)
+ @asyncio.coroutine
+ def vm_feature_remove(self):
+ # validation of self.arg done by qrexec-policy is enough
+
+ self.fire_event_for_permission()
+ try:
+ del self.dest.features[self.arg]
+ except KeyError:
+ raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
+ self.app.save()
+
+ @qubes.api.method('admin.vm.feature.Set')
+ @asyncio.coroutine
+ def vm_feature_set(self, untrusted_payload):
+ # validation of self.arg done by qrexec-policy is enough
+ value = untrusted_payload.decode('ascii', errors='strict')
+ del untrusted_payload
+
+ self.fire_event_for_permission(value=value)
+ self.dest.features[self.arg] = value
+ self.app.save()
+
+ @qubes.api.method('admin.vm.Create.{endpoint}', endpoints=(ep.name
+ for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)))
+ @asyncio.coroutine
+ def vm_create(self, endpoint, untrusted_payload=None):
+ return self._vm_create(endpoint, allow_pool=False,
+ untrusted_payload=untrusted_payload)
+
+ @qubes.api.method('admin.vm.CreateInPool.{endpoint}', endpoints=(ep.name
+ for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)))
+ @asyncio.coroutine
+ def vm_create_in_pool(self, endpoint, untrusted_payload=None):
+ return self._vm_create(endpoint, allow_pool=True,
+ untrusted_payload=untrusted_payload)
+
+ def _vm_create(self, vm_type, allow_pool=False, untrusted_payload=None):
+ assert self.dest.name == 'dom0'
+
+ kwargs = {}
+ pool = None
+ pools = {}
+
+ # this will raise exception if none is found
+ vm_class = qubes.utils.get_entry_point_one(qubes.vm.VM_ENTRY_POINT,
+ vm_type)
+
+ # if argument is given, it needs to be a valid template, and only
+ # when given VM class do need a template
+ if hasattr(vm_class, 'template'):
+ assert self.arg in self.app.domains
+ kwargs['template'] = self.app.domains[self.arg]
+ else:
+ assert not self.arg
+
+ for untrusted_param in untrusted_payload.decode('ascii',
+ errors='strict').split(' '):
+ untrusted_key, untrusted_value = untrusted_param.split('=', 1)
+ if untrusted_key in kwargs:
+ raise qubes.api.ProtocolError('duplicated parameters')
+
+ if untrusted_key == 'name':
+ qubes.vm.validate_name(None, None, untrusted_value)
+ kwargs['name'] = untrusted_value
+
+ elif untrusted_key == 'label':
+ # don't confuse label name with label index
+ assert not untrusted_value.isdigit()
+ allowed_chars = string.ascii_letters + string.digits + '-_.'
+ assert all(c in allowed_chars for c in untrusted_value)
+ try:
+ kwargs['label'] = self.app.get_label(untrusted_value)
+ except KeyError:
+ raise qubes.exc.QubesValueError
+
+ elif untrusted_key == 'pool' and allow_pool:
+ if pool is not None:
+ raise qubes.api.ProtocolError('duplicated pool parameter')
+ pool = self.app.get_pool(untrusted_value)
+ elif untrusted_key.startswith('pool:') and allow_pool:
+ untrusted_volume = untrusted_key.split(':', 1)[1]
+ # kind of ugly, but actual list of volumes is available only
+ # after creating a VM
+ assert untrusted_volume in ['root', 'private', 'volatile',
+ 'kernel']
+ volume = untrusted_volume
+ if volume in pools:
+ raise qubes.api.ProtocolError(
+ 'duplicated pool:{} parameter'.format(volume))
+ pools[volume] = self.app.get_pool(untrusted_value)
+
+ else:
+ raise qubes.api.ProtocolError('Invalid param name')
+ del untrusted_payload
+
+ if 'name' not in kwargs or 'label' not in kwargs:
+ raise qubes.api.ProtocolError('Missing name or label')
+
+ if pool and pools:
+ raise qubes.api.ProtocolError(
+ 'Only one of \'pool=\' and \'pool:volume=\' can be used')
+
+ if kwargs['name'] in self.app.domains:
+ raise qubes.exc.QubesValueError(
+ 'VM {} already exists'.format(kwargs['name']))
+
+ self.fire_event_for_permission(pool=pool, pools=pools, **kwargs)
+
+ vm = self.app.add_new_vm(vm_class, **kwargs)
+
+ try:
+ yield from vm.create_on_disk(pool=pool, pools=pools)
+ except:
+ del self.app.domains[vm]
+ raise
+ self.app.save()
+
+ @qubes.api.method('admin.vm.Clone')
+ @asyncio.coroutine
+ def vm_clone(self, untrusted_payload):
+ assert not self.arg
+
+ assert untrusted_payload.startswith(b'name=')
+ untrusted_name = untrusted_payload[5:].decode('ascii')
+ qubes.vm.validate_name(None, None, untrusted_name)
+ new_name = untrusted_name
+
+ del untrusted_payload
+
+ if new_name in self.app.domains:
+ raise qubes.exc.QubesValueError('Already exists')
+
+ self.fire_event_for_permission(new_name=new_name)
+
+ src_vm = self.dest
+
+ dst_vm = self.app.add_new_vm(src_vm.__class__, name=new_name)
+ try:
+ dst_vm.clone_properties(src_vm)
+ # TODO: tags
+ # TODO: features
+ # TODO: firewall
+ # TODO: persistent devices
+ yield from dst_vm.clone_disk_files(src_vm)
+ except:
+ del self.app.domains[dst_vm]
+ raise
+ self.app.save()
diff --git a/qubes/api/internal.py b/qubes/api/internal.py
new file mode 100644
index 00000000..f400cdbe
--- /dev/null
+++ b/qubes/api/internal.py
@@ -0,0 +1,83 @@
+# -*- encoding: utf8 -*-
+#
+# The Qubes OS Project, http://www.qubes-os.org
+#
+# Copyright (C) 2017 Marek Marczykowski-Górecki
+#
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, see .
+
+''' Internal interface for dom0 components to communicate with qubesd. '''
+
+import asyncio
+import json
+
+import qubes.api
+import qubes.api.admin
+import qubes.vm.dispvm
+
+
+class QubesInternalAPI(qubes.api.AbstractQubesAPI):
+ ''' Communication interface for dom0 components,
+ by design the input here is trusted.'''
+ #
+ # PRIVATE METHODS, not to be called via RPC
+ #
+
+ #
+ # ACTUAL RPC CALLS
+ #
+
+ @qubes.api.method('internal.GetSystemInfo', no_payload=True)
+ @asyncio.coroutine
+ def getsysteminfo(self):
+ assert self.dest.name == 'dom0'
+ assert not self.arg
+
+ system_info = {'domains': {
+ domain.name: {
+ 'tags': list(domain.tags),
+ 'type': domain.__class__.__name__,
+ 'dispvm_allowed': getattr(domain, 'dispvm_allowed', False),
+ 'default_dispvm': (str(domain.default_dispvm) if
+ domain.default_dispvm else None),
+ 'icon': str(domain.label.icon),
+ } for domain in self.app.domains
+ }}
+
+ return json.dumps(system_info)
+
+ @qubes.api.method('internal.vm.Start', no_payload=True)
+ @asyncio.coroutine
+ def start(self):
+ assert not self.arg
+
+ yield from self.dest.start()
+
+ @qubes.api.method('internal.vm.Create.DispVM', no_payload=True)
+ @asyncio.coroutine
+ def create_dispvm(self):
+ assert not self.arg
+
+ # TODO convert to coroutine
+ dispvm = qubes.vm.dispvm.DispVM.from_appvm(self.dest)
+ return dispvm.name
+
+ @qubes.api.method('internal.vm.CleanupDispVM', no_payload=True)
+ @asyncio.coroutine
+ def cleanup_dispvm(self):
+ assert not self.arg
+
+ # TODO convert to coroutine
+ self.dest.cleanup()
diff --git a/qubes/app.py b/qubes/app.py
new file mode 100644
index 00000000..65ad12fa
--- /dev/null
+++ b/qubes/app.py
@@ -0,0 +1,1129 @@
+#
+# The Qubes OS Project, https://www.qubes-os.org/
+#
+# Copyright (C) 2010-2015 Joanna Rutkowska
+# Copyright (C) 2011-2015 Marek Marczykowski-Górecki
+#
+# Copyright (C) 2014-2015 Wojtek Porczyk
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+
+import collections
+import errno
+import functools
+import grp
+import logging
+import os
+import random
+import sys
+import tempfile
+import time
+import uuid
+
+import lxml.etree
+
+import jinja2
+import libvirt
+
+try:
+ import xen.lowlevel.xs # pylint: disable=wrong-import-order
+ import xen.lowlevel.xc # pylint: disable=wrong-import-order
+except ImportError:
+ pass
+
+if os.name == 'posix':
+ # pylint: disable=wrong-import-order
+ import fcntl
+elif os.name == 'nt':
+ # pylint: disable=import-error
+ import win32con
+ import win32file
+ import pywintypes
+else:
+ raise RuntimeError("Qubes works only on POSIX or WinNT systems")
+
+# pylint: disable=wrong-import-position
+import qubes
+import qubes.ext
+import qubes.utils
+import qubes.vm
+import qubes.vm.adminvm
+import qubes.vm.qubesvm
+import qubes.vm.templatevm
+# pylint: enable=wrong-import-position
+
+class VirDomainWrapper(object):
+ # pylint: disable=too-few-public-methods
+
+ def __init__(self, connection, vm):
+ self._connection = connection
+ self._vm = vm
+
+ def _reconnect_if_dead(self):
+ is_dead = not self._vm.connect().isAlive()
+ if is_dead:
+ # pylint: disable=protected-access
+ self._connection._reconnect_if_dead()
+ self._vm = self._connection._conn.lookupByUUID(self._vm.UUID())
+ return is_dead
+
+ def __getattr__(self, attrname):
+ attr = getattr(self._vm, attrname)
+ if not isinstance(attr, collections.Callable):
+ return attr
+
+ @functools.wraps(attr)
+ def wrapper(*args, **kwargs):
+ try:
+ return attr(*args, **kwargs)
+ except libvirt.libvirtError:
+ if self._reconnect_if_dead():
+ return getattr(self._vm, attrname)(*args, **kwargs)
+ raise
+ return wrapper
+
+
+class VirConnectWrapper(object):
+ # pylint: disable=too-few-public-methods
+
+ def __init__(self, uri):
+ self._conn = libvirt.open(uri)
+
+ def _reconnect_if_dead(self):
+ is_dead = not self._conn.isAlive()
+ if is_dead:
+ self._conn = libvirt.open(self._conn.getURI())
+ # TODO: re-register event handlers
+ return is_dead
+
+ def _wrap_domain(self, ret):
+ if isinstance(ret, libvirt.virDomain):
+ ret = VirDomainWrapper(self, ret)
+ return ret
+
+ def __getattr__(self, attrname):
+ attr = getattr(self._conn, attrname)
+ if not isinstance(attr, collections.Callable):
+ return attr
+
+ @functools.wraps(attr)
+ def wrapper(*args, **kwargs):
+ try:
+ return self._wrap_domain(attr(*args, **kwargs))
+ except libvirt.libvirtError:
+ if self._reconnect_if_dead():
+ return self._wrap_domain(
+ getattr(self._conn, attrname)(*args, **kwargs))
+ raise
+ return wrapper
+
+
+class VMMConnection(object):
+ '''Connection to Virtual Machine Manager (libvirt)'''
+
+ def __init__(self, offline_mode=None):
+ '''
+
+ :param offline_mode: enable/disable offline mode; default is to
+ enable when running in chroot as root, otherwise disable
+ '''
+ self._libvirt_conn = None
+ self._xs = None
+ self._xc = None
+ if offline_mode is None:
+ offline_mode = bool(os.getuid() == 0 and
+ os.stat('/') != os.stat('/proc/1/root/.'))
+ self._offline_mode = offline_mode
+
+ @property
+ def offline_mode(self):
+ '''Check or enable offline mode (do not actually connect to vmm)'''
+ return self._offline_mode
+
+ def _libvirt_error_handler(self, ctx, error):
+ pass
+
+ def init_vmm_connection(self):
+ '''Initialise connection
+
+ This method is automatically called when getting'''
+ if self._libvirt_conn is not None:
+ # Already initialized
+ return
+ if self._offline_mode:
+ # Do not initialize in offline mode
+ raise qubes.exc.QubesException(
+ 'VMM operations disabled in offline mode')
+
+ if 'xen.lowlevel.xs' in sys.modules:
+ self._xs = xen.lowlevel.xs.xs()
+ if 'xen.lowlevel.cs' in sys.modules:
+ self._xc = xen.lowlevel.xc.xc()
+ self._libvirt_conn = VirConnectWrapper(
+ qubes.config.defaults['libvirt_uri'])
+ libvirt.registerErrorHandler(self._libvirt_error_handler, None)
+
+ @property
+ def libvirt_conn(self):
+ '''Connection to libvirt'''
+ self.init_vmm_connection()
+ return self._libvirt_conn
+
+ @property
+ def xs(self):
+ '''Connection to Xen Store
+
+ This property in available only when running on Xen.
+ '''
+
+ # XXX what about the case when we run under KVM,
+ # but xen modules are importable?
+ if 'xen.lowlevel.xs' not in sys.modules:
+ raise AttributeError(
+ 'xs object is available under Xen hypervisor only')
+
+ self.init_vmm_connection()
+ return self._xs
+
+ @property
+ def xc(self):
+ '''Connection to Xen
+
+ This property in available only when running on Xen.
+ '''
+
+ # XXX what about the case when we run under KVM,
+ # but xen modules are importable?
+ if 'xen.lowlevel.xc' not in sys.modules:
+ raise AttributeError(
+ 'xc object is available under Xen hypervisor only')
+
+ self.init_vmm_connection()
+ return self._xs
+
+ def register_event_handlers(self, app):
+ '''Register libvirt event handlers, which will translate libvirt
+ events into qubes.events. This function should be called only in
+ 'qubesd' process and only when mainloop has been already set.
+ '''
+ self._libvirt_conn.domainEventRegisterAny(
+ None, # any domain
+ libvirt.VIR_DOMAIN_EVENT_ID_LIFECYCLE,
+ self._domain_event_callback,
+ app
+ )
+
+ @staticmethod
+ def _domain_event_callback(_conn, domain, event, _detail, opaque):
+ '''Generic libvirt event handler (virConnectDomainEventCallback),
+ translate libvirt event into qubes.events.
+ '''
+ app = opaque
+ try:
+ vm = app.domains[domain.name()]
+ except KeyError:
+ # ignore events for unknown domains
+ return
+
+ if event == libvirt.VIR_DOMAIN_EVENT_STOPPED:
+ vm.fire_event('domain-shutdown')
+
+ def __del__(self):
+ if self._libvirt_conn:
+ self._libvirt_conn.close()
+
+
+class QubesHost(object):
+ '''Basic information about host machine
+
+ :param qubes.Qubes app: Qubes application context (must have \
+ :py:attr:`Qubes.vmm` attribute defined)
+ '''
+
+ def __init__(self, app):
+ self.app = app
+ self._no_cpus = None
+ self._total_mem = None
+ self._physinfo = None
+
+
+ def _fetch(self):
+ if self._no_cpus is not None:
+ return
+
+ # pylint: disable=unused-variable
+ (model, memory, cpus, mhz, nodes, socket, cores, threads) = \
+ self.app.vmm.libvirt_conn.getInfo()
+ self._total_mem = int(memory) * 1024
+ self._no_cpus = cpus
+
+ self.app.log.debug('QubesHost: no_cpus={} memory_total={}'.format(
+ self.no_cpus, self.memory_total))
+ try:
+ self.app.log.debug('QubesHost: xen_free_memory={}'.format(
+ self.get_free_xen_memory()))
+ except NotImplementedError:
+ pass
+
+
+ @property
+ def memory_total(self):
+ '''Total memory, in kbytes'''
+
+ if self.app.vmm.offline_mode:
+ return 2**64-1
+ self._fetch()
+ return self._total_mem
+
+
+ @property
+ def no_cpus(self):
+ '''Number of CPUs'''
+
+ if self.app.vmm.offline_mode:
+ return 42
+
+ self._fetch()
+ return self._no_cpus
+
+
+ def get_free_xen_memory(self):
+ '''Get free memory from Xen's physinfo.
+
+ :raises NotImplementedError: when not under Xen
+ '''
+ try:
+ self._physinfo = self.app.xc.physinfo()
+ except AttributeError:
+ raise NotImplementedError('This function requires Xen hypervisor')
+ return int(self._physinfo['free_memory'])
+
+
+ def measure_cpu_usage(self, previous_time=None, previous=None,
+ wait_time=1):
+ '''Measure cpu usage for all domains at once.
+
+ This function requires Xen hypervisor.
+
+ .. versionchanged:: 3.0
+ argument order to match return tuple
+
+ :raises NotImplementedError: when not under Xen
+ '''
+
+ if previous is None:
+ previous_time = time.time()
+ previous = {}
+ try:
+ info = self.app.vmm.xc.domain_getinfo(0, qubes.config.max_qid)
+ except AttributeError:
+ raise NotImplementedError(
+ 'This function requires Xen hypervisor')
+
+ for vm in info:
+ previous[vm['domid']] = {}
+ previous[vm['domid']]['cpu_time'] = (
+ vm['cpu_time'] / max(vm['online_vcpus'], 1))
+ previous[vm['domid']]['cpu_usage'] = 0
+ time.sleep(wait_time)
+
+ current_time = time.time()
+ current = {}
+ try:
+ info = self.app.vmm.xc.domain_getinfo(0, qubes.config.max_qid)
+ except AttributeError:
+ raise NotImplementedError(
+ 'This function requires Xen hypervisor')
+ for vm in info:
+ current[vm['domid']] = {}
+ current[vm['domid']]['cpu_time'] = (
+ vm['cpu_time'] / max(vm['online_vcpus'], 1))
+ if vm['domid'] in previous.keys():
+ current[vm['domid']]['cpu_usage'] = (
+ float(current[vm['domid']]['cpu_time'] -
+ previous[vm['domid']]['cpu_time']) /
+ 1000 ** 3 / (current_time - previous_time) * 100)
+ if current[vm['domid']]['cpu_usage'] < 0:
+ # VM has been rebooted
+ current[vm['domid']]['cpu_usage'] = 0
+ else:
+ current[vm['domid']]['cpu_usage'] = 0
+
+ return (current_time, current)
+
+
+class VMCollection(object):
+ '''A collection of Qubes VMs
+
+ VMCollection supports ``in`` operator. You may test for ``qid``, ``name``
+ and whole VM object's presence.
+
+ Iterating over VMCollection will yield machine objects.
+ '''
+
+ def __init__(self, app):
+ self.app = app
+ self._dict = dict()
+
+
+ def __repr__(self):
+ return '<{} {!r}>'.format(
+ self.__class__.__name__, list(sorted(self.keys())))
+
+
+ def items(self):
+ '''Iterate over ``(qid, vm)`` pairs'''
+ for qid in self.qids():
+ yield (qid, self[qid])
+
+
+ def qids(self):
+ '''Iterate over all qids
+
+ qids are sorted by numerical order.
+ '''
+
+ return iter(sorted(self._dict.keys()))
+
+ keys = qids
+
+
+ def names(self):
+ '''Iterate over all names
+
+ names are sorted by lexical order.
+ '''
+
+ return iter(sorted(vm.name for vm in self._dict.values()))
+
+
+ def vms(self):
+ '''Iterate over all machines
+
+ vms are sorted by qid.
+ '''
+
+ return iter(sorted(self._dict.values()))
+
+ __iter__ = vms
+ values = vms
+
+ def add(self, value, _enable_events=True):
+ '''Add VM to collection
+
+ :param qubes.vm.BaseVM value: VM to add
+ :raises TypeError: when value is of wrong type
+ :raises ValueError: when there is already VM which has equal ``qid``
+ '''
+
+ # this violates duck typing, but is needed
+ # for VMProperty to function correctly
+ if not isinstance(value, qubes.vm.BaseVM):
+ raise TypeError('{} holds only BaseVM instances'.format(
+ self.__class__.__name__))
+
+ if value.qid in self:
+ raise ValueError('This collection already holds VM that has '
+ 'qid={!r} ({!r})'.format(value.qid, self[value.qid]))
+ if value.name in self:
+
+ raise ValueError('A VM named {!s} already exists'
+ .format(value.name))
+
+ self._dict[value.qid] = value
+ if _enable_events:
+ value.events_enabled = True
+ self.app.fire_event('domain-add', vm=value)
+
+ return value
+
+ def __getitem__(self, key):
+ if isinstance(key, int):
+ return self._dict[key]
+
+ if isinstance(key, str):
+ for vm in self:
+ if vm.name == key:
+ return vm
+ raise KeyError(key)
+
+ if isinstance(key, qubes.vm.BaseVM):
+ key = key.uuid
+
+ if isinstance(key, uuid.UUID):
+ for vm in self:
+ if vm.uuid == key:
+ return vm
+ raise KeyError(key)
+
+ raise KeyError(key)
+
+ def __delitem__(self, key):
+ vm = self[key]
+ if not vm.is_halted():
+ raise qubes.exc.QubesVMNotHaltedError(vm)
+ self.app.fire_event_pre('domain-pre-delete', vm=vm)
+ try:
+ vm.libvirt_domain.undefine()
+ except libvirt.libvirtError as e:
+ if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
+ # already undefined
+ pass
+ del self._dict[vm.qid]
+ self.app.fire_event('domain-delete', vm=vm)
+
+ def __contains__(self, key):
+ return any((key == vm or key == vm.qid or key == vm.name)
+ for vm in self)
+
+
+ def __len__(self):
+ return len(self._dict)
+
+
+ def get_vms_based_on(self, template):
+ template = self[template]
+ return set(vm for vm in self
+ if hasattr(vm, 'template') and vm.template == template)
+
+
+ def get_vms_connected_to(self, netvm):
+ new_vms = set([self[netvm]])
+ dependent_vms = set()
+
+ # Dependency resolving only makes sense on NetVM (or derivative)
+# if not self[netvm_qid].is_netvm():
+# return set([])
+
+ while new_vms:
+ cur_vm = new_vms.pop()
+ for vm in cur_vm.connected_vms:
+ if vm in dependent_vms:
+ continue
+ dependent_vms.add(vm)
+# if vm.is_netvm():
+ new_vms.add(vm)
+
+ return dependent_vms
+
+
+ # XXX with Qubes Admin Api this will probably lead to race condition
+ # whole process of creating and adding should be synchronised
+ def get_new_unused_qid(self):
+ used_ids = set(self.qids())
+ for i in range(1, qubes.config.max_qid):
+ if i not in used_ids:
+ return i
+ raise LookupError("Cannot find unused qid!")
+
+
+ def get_new_unused_netid(self):
+ used_ids = set([vm.netid for vm in self]) # if vm.is_netvm()])
+ for i in range(1, qubes.config.max_netid):
+ if i not in used_ids:
+ return i
+ raise LookupError("Cannot find unused netid!")
+
+
+ def get_new_unused_dispid(self):
+ for _ in range(int(qubes.config.max_dispid ** 0.5)):
+ dispid = random.SystemRandom().randrange(qubes.config.max_dispid)
+ if not any(getattr(vm, 'dispid', None) == dispid for vm in self):
+ return dispid
+ raise LookupError((
+ 'https://xkcd.com/221/',
+ 'http://dilbert.com/strip/2001-10-25')[random.randint(0, 1)])
+
+
+class Qubes(qubes.PropertyHolder):
+ '''Main Qubes application
+
+ :param str store: path to ``qubes.xml``
+
+ The store is loaded in stages:
+
+ 1. In the first stage there are loaded some basic features from store
+ (currently labels).
+
+ 2. In the second stage stubs for all VMs are loaded. They are filled
+ with their basic properties, like ``qid`` and ``name``.
+
+ 3. In the third stage all global properties are loaded. They often
+ reference VMs, like default netvm, so they should be filled after
+ loading VMs.
+
+ 4. In the fourth stage all remaining VM properties are loaded. They
+ also need all VMs loaded, because they represent dependencies
+ between VMs like aforementioned netvm.
+
+ 5. In the fifth stage there are some fixups to ensure sane system
+ operation.
+
+ This class emits following events:
+
+ .. event:: domain-add (subject, event, vm)
+
+ When domain is added.
+
+ :param subject: Event emitter
+ :param event: Event name (``'domain-add'``)
+ :param vm: Domain object
+
+ .. event:: domain-pre-delete (subject, event, vm)
+
+ When domain is deleted. VM still has reference to ``app`` object,
+ and is contained within VMCollection. You may prevent removal by
+ raising an exception.
+
+ :param subject: Event emitter
+ :param event: Event name (``'domain-pre-delete'``)
+ :param vm: Domain object
+
+ .. event:: domain-delete (subject, event, vm)
+
+ When domain is deleted. VM still has reference to ``app`` object,
+ but is not contained within VMCollection.
+
+ :param subject: Event emitter
+ :param event: Event name (``'domain-delete'``)
+ :param vm: Domain object
+
+ Methods and attributes:
+ '''
+
+ default_netvm = qubes.VMProperty('default_netvm', load_stage=3,
+ default=None, allow_none=True,
+ doc='''Default NetVM for AppVMs. Initial state is `None`, which means
+ that AppVMs are not connected to the Internet.''')
+ default_fw_netvm = qubes.VMProperty('default_fw_netvm', load_stage=3,
+ default=None, allow_none=True,
+ doc='''Default NetVM for ProxyVMs. Initial state is `None`, which means
+ that ProxyVMs (including FirewallVM) are not connected to the
+ Internet.''')
+ default_template = qubes.VMProperty('default_template', load_stage=3,
+ vmclass=qubes.vm.templatevm.TemplateVM,
+ doc='Default template for new AppVMs')
+ updatevm = qubes.VMProperty('updatevm', load_stage=3,
+ allow_none=True,
+ doc='''Which VM to use as `yum` proxy for updating AdminVM and
+ TemplateVMs''')
+ clockvm = qubes.VMProperty('clockvm', load_stage=3,
+ allow_none=True,
+ doc='Which VM to use as NTP proxy for updating AdminVM')
+ default_kernel = qubes.property('default_kernel', load_stage=3,
+ doc='Which kernel to use when not overriden in VM')
+ default_dispvm = qubes.VMProperty('default_dispvm', load_stage=3,
+ doc='Default DispVM base for service calls')
+
+ # TODO #1637 #892
+ check_updates_vm = qubes.property('check_updates_vm',
+ type=bool, setter=qubes.property.bool,
+ default=True,
+ doc='check for updates inside qubes')
+
+ def __init__(self, store=None, load=True, offline_mode=None, lock=False,
+ **kwargs):
+ #: logger instance for logging global messages
+ self.log = logging.getLogger('app')
+
+ self._extensions = qubes.ext.get_extensions()
+
+ #: collection of all VMs managed by this Qubes instance
+ self.domains = VMCollection(self)
+
+ #: collection of all available labels for VMs
+ self.labels = {}
+
+ #: collection of all pools
+ self.pools = {}
+
+ #: Connection to VMM
+ self.vmm = VMMConnection(offline_mode=offline_mode)
+
+ #: Information about host system
+ self.host = QubesHost(self)
+
+ if store is not None:
+ self._store = store
+ else:
+ self._store = os.environ.get('QUBES_XML_PATH',
+ os.path.join(
+ qubes.config.system_path['qubes_base_dir'],
+ qubes.config.system_path['qubes_store_filename']))
+
+ super(Qubes, self).__init__(xml=None, **kwargs)
+
+ self.__load_timestamp = None
+ self.__locked_fh = None
+
+ #: jinja2 environment for libvirt XML templates
+ self.env = jinja2.Environment(
+ loader=jinja2.FileSystemLoader([
+ '/etc/qubes/templates',
+ '/usr/share/qubes/templates',
+ ]),
+ undefined=jinja2.StrictUndefined)
+
+ if load:
+ self.load(lock=lock)
+
+ self.events_enabled = True
+
+ @property
+ def store(self):
+ return self._store
+
+ def load(self, lock=False):
+ '''Open qubes.xml
+
+ :throws EnvironmentError: failure on parsing store
+ :throws xml.parsers.expat.ExpatError: failure on parsing store
+ :raises lxml.etree.XMLSyntaxError: on syntax error in qubes.xml
+ '''
+
+ fh = self._acquire_lock()
+ self.xml = lxml.etree.parse(fh)
+
+ # stage 1: load labels and pools
+ for node in self.xml.xpath('./labels/label'):
+ label = qubes.Label.fromxml(node)
+ self.labels[label.index] = label
+
+ for node in self.xml.xpath('./pools/pool'):
+ name = node.get('name')
+ assert name, "Pool name '%s' is invalid " % name
+ try:
+ self.pools[name] = self._get_pool(**node.attrib)
+ except qubes.exc.QubesException as e:
+ self.log.error(str(e))
+
+ # stage 2: load VMs
+ for node in self.xml.xpath('./domains/domain'):
+ # pylint: disable=no-member
+ cls = self.get_vm_class(node.get('class'))
+ vm = cls(self, node)
+ vm.load_properties(load_stage=2)
+ vm.init_log()
+ self.domains.add(vm, _enable_events=False)
+
+ if 0 not in self.domains:
+ self.domains.add(
+ qubes.vm.adminvm.AdminVM(self, None, qid=0, name='dom0'),
+ _enable_events=False)
+
+ # stage 3: load global properties
+ self.load_properties(load_stage=3)
+
+ # stage 4: fill all remaining VM properties
+ for vm in self.domains:
+ vm.load_properties(load_stage=4)
+ vm.load_extras()
+
+ # stage 5: misc fixups
+
+ self.property_require('default_fw_netvm', allow_none=True)
+ self.property_require('default_netvm', allow_none=True)
+ self.property_require('default_template')
+ self.property_require('clockvm', allow_none=True)
+ self.property_require('updatevm', allow_none=True)
+
+ # Disable ntpd in ClockVM - to not conflict with ntpdate (both are
+ # using 123/udp port)
+ if hasattr(self, 'clockvm') and self.clockvm is not None:
+ if self.clockvm.features.get('service/ntpd', False):
+ self.log.warning(
+ 'VM set as clockvm (%r) has enabled \'ntpd\' service! '
+ 'Expect failure when syncing time in dom0.',
+ self.clockvm)
+ else:
+ self.clockvm.features['service/ntpd'] = ''
+
+ for vm in self.domains:
+ vm.events_enabled = True
+ vm.fire_event('domain-load')
+
+ # get a file timestamp (before closing it - still holding the lock!),
+ # to detect whether anyone else have modified it in the meantime
+ self.__load_timestamp = os.path.getmtime(self._store)
+
+ if not lock:
+ self._release_lock()
+
+
+ def __xml__(self):
+ element = lxml.etree.Element('qubes')
+
+ element.append(self.xml_labels())
+
+ pools_xml = lxml.etree.Element('pools')
+ for pool in self.pools.values():
+ xml = pool.__xml__()
+ if xml is not None:
+ pools_xml.append(xml)
+
+ element.append(pools_xml)
+
+ element.append(self.xml_properties())
+
+ domains = lxml.etree.Element('domains')
+ for vm in self.domains:
+ domains.append(vm.__xml__())
+ element.append(domains)
+
+ return element
+
+
+ def save(self, lock=True):
+ '''Save all data to qubes.xml
+
+ There are several problems with saving :file:`qubes.xml` which must be
+ mitigated:
+
+ - Running out of disk space. No space left should not result in empty
+ file. This is done by writing to temporary file and then renaming.
+ - Attempts to write two or more files concurrently. This is done by
+ sophisticated locking.
+
+ :param bool lock: keep file locked after saving
+ :throws EnvironmentError: failure on saving
+ '''
+
+ if not self.__locked_fh:
+ self._acquire_lock(for_save=True)
+
+ fh_new = tempfile.NamedTemporaryFile(
+ prefix=self._store, delete=False)
+ lxml.etree.ElementTree(self.__xml__()).write(
+ fh_new, encoding='utf-8', pretty_print=True)
+ fh_new.flush()
+ try:
+ os.chown(fh_new.name, -1, grp.getgrnam('qubes').gr_gid)
+ os.chmod(fh_new.name, 0o660)
+ except KeyError: # group 'qubes' not found
+ # don't change mode if no 'qubes' group in the system
+ pass
+ os.rename(fh_new.name, self._store)
+
+ # update stored mtime, in case of multiple save() calls without
+ # loading qubes.xml again
+ self.__load_timestamp = os.path.getmtime(self._store)
+
+ # this releases lock for all other processes,
+ # but they should instantly block on the new descriptor
+ self.__locked_fh.close()
+ self.__locked_fh = fh_new
+
+ if not lock:
+ self._release_lock()
+
+
+ def _acquire_lock(self, for_save=False):
+ assert self.__locked_fh is None, 'double lock'
+
+ while True:
+ try:
+ fd = os.open(self._store,
+ os.O_RDWR | (os.O_CREAT * int(for_save)))
+ except OSError as e:
+ if not for_save and e.errno == errno.ENOENT:
+ raise qubes.exc.QubesException(
+ 'Qubes XML store {!r} is missing; '
+ 'use qubes-create tool'.format(self._store))
+ raise
+
+ # While we were waiting for lock, someone could have unlink()ed
+ # (or rename()d) our file out of the filesystem. We have to
+ # ensure we got lock on something linked to filesystem.
+ # If not, try again.
+ if os.fstat(fd) != os.stat(self._store):
+ os.close(fd)
+ continue
+
+ if self.__load_timestamp and \
+ os.path.getmtime(self._store) != self.__load_timestamp:
+ os.close(fd)
+ raise qubes.exc.QubesException(
+ 'Someone else modified qubes.xml in the meantime')
+
+ break
+
+ if os.name == 'posix':
+ fcntl.lockf(fd, fcntl.LOCK_EX)
+ elif os.name == 'nt':
+ # pylint: disable=protected-access
+ overlapped = pywintypes.OVERLAPPED()
+ win32file.LockFileEx(
+ win32file._get_osfhandle(fd),
+ win32con.LOCKFILE_EXCLUSIVE_LOCK, 0, -0x10000, overlapped)
+
+ self.__locked_fh = os.fdopen(fd, 'r+b')
+ return self.__locked_fh
+
+
+ def _release_lock(self):
+ assert self.__locked_fh is not None, 'double release'
+
+ # intentionally do not call explicit unlock to not unlock the file
+ # before all buffers are flushed
+ self.__locked_fh.close()
+ self.__locked_fh = None
+
+
+ def load_initial_values(self):
+ self.labels = {
+ 1: qubes.Label(1, '0xcc0000', 'red'),
+ 2: qubes.Label(2, '0xf57900', 'orange'),
+ 3: qubes.Label(3, '0xedd400', 'yellow'),
+ 4: qubes.Label(4, '0x73d216', 'green'),
+ 5: qubes.Label(5, '0x555753', 'gray'),
+ 6: qubes.Label(6, '0x3465a4', 'blue'),
+ 7: qubes.Label(7, '0x75507b', 'purple'),
+ 8: qubes.Label(8, '0x000000', 'black'),
+ }
+ assert max(self.labels.keys()) == qubes.config.max_default_label
+
+ # check if the default LVM Thin pool qubes_dom0/pool00 exists
+ if os.path.exists('/dev/mapper/qubes_dom0-pool00-tpool'):
+ self.add_pool(volume_group='qubes_dom0', thin_pool='pool00',
+ name='default', driver='lvm_thin')
+ else:
+ self.pools['default'] = self._get_pool(
+ dir_path=qubes.config.qubes_base_dir,
+ name='default', driver='file')
+ for name, config in qubes.config.defaults['pool_configs'].items():
+ self.pools[name] = self._get_pool(**config)
+
+ self.domains.add(
+ qubes.vm.adminvm.AdminVM(self, None, qid=0, name='dom0',
+ label='black'))
+
+ @classmethod
+ def create_empty_store(cls, *args, **kwargs):
+ self = cls(*args, load=False, **kwargs)
+ if os.path.exists(self.store):
+ raise qubes.exc.QubesException(
+ '{} already exists, aborting'.format(self.store))
+ self.load_initial_values()
+ # TODO py3 get lock= as keyword-only arg
+ self.save(kwargs.get('lock'))
+
+ return self
+
+
+ def xml_labels(self):
+ '''Serialise labels
+
+ :rtype: lxml.etree._Element
+ '''
+
+ labels = lxml.etree.Element('labels')
+ for label in sorted(self.labels.values(), key=lambda labl: labl.index):
+ labels.append(label.__xml__())
+ return labels
+
+ @staticmethod
+ def get_vm_class(clsname):
+ '''Find the class for a domain.
+
+ Classes are registered as setuptools' entry points in ``qubes.vm``
+ group. Any package may supply their own classes.
+
+ :param str clsname: name of the class
+ :return type: class
+ '''
+
+ try:
+ return qubes.utils.get_entry_point_one(
+ qubes.vm.VM_ENTRY_POINT, clsname)
+ except KeyError:
+ raise qubes.exc.QubesException(
+ 'no such VM class: {!r}'.format(clsname))
+ # don't catch TypeError
+
+ def add_new_vm(self, cls, qid=None, **kwargs):
+ '''Add new Virtual Machine to collection
+
+ '''
+
+ if qid is None:
+ qid = self.domains.get_new_unused_qid()
+
+ if isinstance(cls, str):
+ cls = self.get_vm_class(cls)
+ # handle default template; specifically allow template=None (do not
+ # override it with default template)
+ if 'template' not in kwargs and hasattr(cls, 'template'):
+ kwargs['template'] = self.default_template
+ elif 'template' in kwargs and isinstance(kwargs['template'], str):
+ kwargs['template'] = self.domains[kwargs['template']]
+
+ return self.domains.add(cls(self, None, qid=qid, **kwargs))
+
+ def get_label(self, label):
+ '''Get label as identified by index or name
+
+ :throws KeyError: when label is not found
+ '''
+
+ # first search for index, verbatim
+ try:
+ return self.labels[label]
+ except KeyError:
+ pass
+
+ # then search for name
+ for i in self.labels.values():
+ if i.name == label:
+ return i
+
+ # last call, if label is a number represented as str, search in indices
+ try:
+ return self.labels[int(label)]
+ except (KeyError, ValueError):
+ pass
+
+ raise KeyError(label)
+
+ def add_pool(self, name, **kwargs):
+ """ Add a storage pool to config."""
+
+ if name in self.pools.keys():
+ raise qubes.exc.QubesException('pool named %s already exists \n' %
+ name)
+
+ kwargs['name'] = name
+ pool = self._get_pool(**kwargs)
+ pool.setup()
+ self.pools[name] = pool
+ return pool
+
+ def remove_pool(self, name):
+ """ Remove a storage pool from config file. """
+ try:
+ pool = self.pools[name]
+ del self.pools[name]
+ pool.destroy()
+ except KeyError:
+ return
+
+
+ def get_pool(self, name):
+ ''' Returns a :py:class:`qubes.storage.Pool` instance '''
+ try:
+ return self.pools[name]
+ except KeyError:
+ raise qubes.exc.QubesException('Unknown storage pool ' + name)
+
+ @staticmethod
+ def _get_pool(**kwargs):
+ try:
+ name = kwargs['name']
+ assert name, 'Name needs to be an non empty string'
+ except KeyError:
+ raise qubes.exc.QubesException('No pool name for pool')
+
+ try:
+ driver = kwargs['driver']
+ except KeyError:
+ raise qubes.exc.QubesException('No driver specified for pool ' +
+ name)
+ try:
+ klass = qubes.utils.get_entry_point_one(
+ qubes.storage.STORAGE_ENTRY_POINT, driver)
+ del kwargs['driver']
+ return klass(**kwargs)
+ except KeyError:
+ raise qubes.exc.QubesException('No driver %s for pool %s' %
+ (driver, name))
+
+ @qubes.events.handler('domain-pre-delete')
+ def on_domain_pre_deleted(self, event, vm):
+ # pylint: disable=unused-argument
+ if isinstance(vm, qubes.vm.templatevm.TemplateVM):
+ appvms = self.domains.get_vms_based_on(vm)
+ if appvms:
+ raise qubes.exc.QubesException(
+ 'Cannot remove template that has dependent AppVMs. '
+ 'Affected are: {}'.format(', '.join(
+ vm.name for name in sorted(appvms))))
+
+
+ @qubes.events.handler('domain-delete')
+ def on_domain_deleted(self, event, vm):
+ # pylint: disable=unused-argument
+ for propname in (
+ 'default_netvm',
+ 'default_fw_netvm',
+ 'clockvm',
+ 'updatevm',
+ 'default_template',
+ ):
+ try:
+ if getattr(self, propname) == vm:
+ delattr(self, propname)
+ except AttributeError:
+ pass
+
+
+ @qubes.events.handler('property-pre-set:clockvm')
+ def on_property_pre_set_clockvm(self, event, name, newvalue, oldvalue=None):
+ # pylint: disable=unused-argument,no-self-use
+ if newvalue is None:
+ return
+ if newvalue.features.get('service/ntpd', False):
+ raise qubes.exc.QubesVMError(newvalue,
+ 'Cannot set {!r} as {!r} since it has ntpd enabled.'.format(
+ newvalue.name, name))
+ else:
+ newvalue.features['service/ntpd'] = ''
+
+
+ @qubes.events.handler(
+ 'property-pre-set:default_netvm',
+ 'property-pre-set:default_fw_netvm')
+ def on_property_pre_set_default_netvm(self, event, name, newvalue,
+ oldvalue=None):
+ # pylint: disable=unused-argument,invalid-name
+ if newvalue is not None and oldvalue is not None \
+ and oldvalue.is_running() and not newvalue.is_running() \
+ and self.domains.get_vms_connected_to(oldvalue):
+ raise qubes.exc.QubesVMNotRunningError(newvalue,
+ 'Cannot change {!r} to domain that '
+ 'is not running ({!r}).'.format(name, newvalue.name))
+
+
+ @qubes.events.handler('property-set:default_fw_netvm')
+ def on_property_set_default_fw_netvm(self, event, name, newvalue,
+ oldvalue=None):
+ # pylint: disable=unused-argument,invalid-name
+ for vm in self.domains:
+ if not vm.provides_network and vm.property_is_default('netvm'):
+ # fire property-del:netvm as it is responsible for resetting
+ # netvm to it's default value
+ vm.fire_event('property-del:netvm',
+ name='netvm', newvalue=newvalue, oldvalue=oldvalue)
+
+
+ @qubes.events.handler('property-set:default_netvm')
+ def on_property_set_default_netvm(self, event, name, newvalue,
+ oldvalue=None):
+ # pylint: disable=unused-argument
+ for vm in self.domains:
+ if vm.property_is_default('netvm'):
+ # fire property-del:netvm as it is responsible for resetting
+ # netvm to it's default value
+ vm.fire_event('property-del:netvm',
+ name='netvm', oldvalue=oldvalue)
diff --git a/qubes/backup.py b/qubes/backup.py
new file mode 100644
index 00000000..1a26221a
--- /dev/null
+++ b/qubes/backup.py
@@ -0,0 +1,2623 @@
+#
+# The Qubes OS Project, http://www.qubes-os.org
+#
+# Copyright (C) 2013-2015 Marek Marczykowski-Górecki
+#
+# Copyright (C) 2013 Olivier Médoc
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
+#
+#
+from __future__ import unicode_literals
+import itertools
+import logging
+import functools
+import termios
+
+from qubes.utils import size_to_human
+import sys
+import stat
+import os
+import fcntl
+import subprocess
+import re
+import shutil
+import tempfile
+import time
+import grp
+import pwd
+import errno
+import datetime
+from multiprocessing import Queue, Process
+import qubes
+import qubes.core2migration
+import qubes.storage
+import qubes.storage.file
+import qubes.vm.templatevm
+
+QUEUE_ERROR = "ERROR"
+
+QUEUE_FINISHED = "FINISHED"
+
+HEADER_FILENAME = 'backup-header'
+DEFAULT_CRYPTO_ALGORITHM = 'aes-256-cbc'
+# 'scrypt' is not exactly HMAC algorithm, but a tool we use to
+# integrity-protect the data
+DEFAULT_HMAC_ALGORITHM = 'scrypt'
+DEFAULT_COMPRESSION_FILTER = 'gzip'
+CURRENT_BACKUP_FORMAT_VERSION = '4'
+# Maximum size of error message get from process stderr (including VM process)
+MAX_STDERR_BYTES = 1024
+# header + qubes.xml max size
+HEADER_QUBES_XML_MAX_SIZE = 1024 * 1024
+# hmac file max size - regardless of backup format version!
+HMAC_MAX_SIZE = 4096
+
+BLKSIZE = 512
+
+_re_alphanum = re.compile(r'^[A-Za-z0-9-]*$')
+
+
+class BackupCanceledError(qubes.exc.QubesException):
+ def __init__(self, msg, tmpdir=None):
+ super(BackupCanceledError, self).__init__(msg)
+ self.tmpdir = tmpdir
+
+
+class BackupHeader(object):
+ '''Structure describing backup-header file included as the first file in
+ backup archive
+ '''
+ header_keys = {
+ 'version': 'version',
+ 'encrypted': 'encrypted',
+ 'compressed': 'compressed',
+ 'compression-filter': 'compression_filter',
+ 'crypto-algorithm': 'crypto_algorithm',
+ 'hmac-algorithm': 'hmac_algorithm',
+ 'backup-id': 'backup_id'
+ }
+ bool_options = ['encrypted', 'compressed']
+ int_options = ['version']
+
+ def __init__(self,
+ header_data=None,
+ version=None,
+ encrypted=None,
+ compressed=None,
+ compression_filter=None,
+ hmac_algorithm=None,
+ crypto_algorithm=None,
+ backup_id=None):
+ # repeat the list to help code completion...
+ self.version = version
+ self.encrypted = encrypted
+ self.compressed = compressed
+ # Options introduced in backup format 3+, which always have a header,
+ # so no need for fallback in function parameter
+ self.compression_filter = compression_filter
+ self.hmac_algorithm = hmac_algorithm
+ self.crypto_algorithm = crypto_algorithm
+ self.backup_id = backup_id
+
+ if header_data is not None:
+ self.load(header_data)
+
+ def load(self, untrusted_header_text):
+ """Parse backup header file.
+
+ :param untrusted_header_text: header content
+ :type untrusted_header_text: basestring
+ .. warning::
+ This function may be exposed to not yet verified header,
+ so is security critical.
+ """
+ try:
+ untrusted_header_text = untrusted_header_text.decode('ascii')
+ except UnicodeDecodeError:
+ raise qubes.exc.QubesException(
+ "Non-ASCII characters in backup header")
+ for untrusted_line in untrusted_header_text.splitlines():
+ if untrusted_line.count('=') != 1:
+ raise qubes.exc.QubesException("Invalid backup header")
+ key, value = untrusted_line.strip().split('=', 1)
+ if not _re_alphanum.match(key):
+ raise qubes.exc.QubesException("Invalid backup header (key)")
+ if key not in self.header_keys.keys():
+ # Ignoring unknown option
+ continue
+ if not _re_alphanum.match(value):
+ raise qubes.exc.QubesException("Invalid backup header (value)")
+ if getattr(self, self.header_keys[key]) is not None:
+ raise qubes.exc.QubesException(
+ "Duplicated header line: {}".format(key))
+ if key in self.bool_options:
+ value = value.lower() in ["1", "true", "yes"]
+ elif key in self.int_options:
+ value = int(value)
+ setattr(self, self.header_keys[key], value)
+
+ self.validate()
+
+ def validate(self):
+ if self.version == 1:
+ # header not really present
+ pass
+ elif self.version in [2, 3, 4]:
+ expected_attrs = ['version', 'encrypted', 'compressed',
+ 'hmac_algorithm']
+ if self.encrypted:
+ expected_attrs += ['crypto_algorithm']
+ if self.version >= 3 and self.compressed:
+ expected_attrs += ['compression_filter']
+ if self.version >= 4:
+ expected_attrs += ['backup_id']
+ for key in expected_attrs:
+ if getattr(self, key) is None:
+ raise qubes.exc.QubesException(
+ "Backup header lack '{}' info".format(key))
+ else:
+ raise qubes.exc.QubesException(
+ "Unsupported backup version {}".format(self.version))
+
+ def save(self, filename):
+ with open(filename, "w") as f:
+ # make sure 'version' is the first key
+ f.write('version={}\n'.format(self.version))
+ for key, attr in self.header_keys.items():
+ if key == 'version':
+ continue
+ if getattr(self, attr) is None:
+ continue
+ f.write("{!s}={!s}\n".format(key, getattr(self, attr)))
+
+
+class SendWorker(Process):
+ def __init__(self, queue, base_dir, backup_stdout):
+ super(SendWorker, self).__init__()
+ self.queue = queue
+ self.base_dir = base_dir
+ self.backup_stdout = backup_stdout
+ self.log = logging.getLogger('qubes.backup')
+
+ def run(self):
+ self.log.debug("Started sending thread")
+
+ self.log.debug("Moving to temporary dir".format(self.base_dir))
+ os.chdir(self.base_dir)
+
+ for filename in iter(self.queue.get, None):
+ if filename in (QUEUE_FINISHED, QUEUE_ERROR):
+ break
+
+ self.log.debug("Sending file {}".format(filename))
+ # This tar used for sending data out need to be as simple, as
+ # simple, as featureless as possible. It will not be
+ # verified before untaring.
+ tar_final_cmd = ["tar", "-cO", "--posix",
+ "-C", self.base_dir, filename]
+ final_proc = subprocess.Popen(tar_final_cmd,
+ stdin=subprocess.PIPE,
+ stdout=self.backup_stdout)
+ if final_proc.wait() >= 2:
+ if self.queue.full():
+ # if queue is already full, remove some entry to wake up
+ # main thread, so it will be able to notice error
+ self.queue.get()
+ # handle only exit code 2 (tar fatal error) or
+ # greater (call failed?)
+ raise qubes.exc.QubesException(
+ "ERROR: Failed to write the backup, out of disk space? "
+ "Check console output or ~/.xsession-errors for details.")
+
+ # Delete the file as we don't need it anymore
+ self.log.debug("Removing file {}".format(filename))
+ os.remove(filename)
+
+ self.log.debug("Finished sending thread")
+
+
+def launch_proc_with_pty(args, stdin=None, stdout=None, stderr=None, echo=True):
+ """Similar to pty.fork, but handle stdin/stdout according to parameters
+ instead of connecting to the pty
+
+ :return tuple (subprocess.Popen, pty_master)
+ """
+
+ def set_ctty(ctty_fd, master_fd):
+ os.setsid()
+ os.close(master_fd)
+ fcntl.ioctl(ctty_fd, termios.TIOCSCTTY, 0)
+ if not echo:
+ termios_p = termios.tcgetattr(ctty_fd)
+ # termios_p.c_lflags
+ termios_p[3] &= ~termios.ECHO
+ termios.tcsetattr(ctty_fd, termios.TCSANOW, termios_p)
+ (pty_master, pty_slave) = os.openpty()
+ p = subprocess.Popen(args, stdin=stdin, stdout=stdout, stderr=stderr,
+ preexec_fn=lambda: set_ctty(pty_slave, pty_master))
+ os.close(pty_slave)
+ return p, os.fdopen(pty_master, 'wb+', buffering=0)
+
+
+def launch_scrypt(action, input_name, output_name, passphrase):
+ '''
+ Launch 'scrypt' process, pass passphrase to it and return
+ subprocess.Popen object.
+
+ :param action: 'enc' or 'dec'
+ :param input_name: input path or '-' for stdin
+ :param output_name: output path or '-' for stdout
+ :param passphrase: passphrase
+ :return: subprocess.Popen object
+ '''
+ command_line = ['scrypt', action, input_name, output_name]
+ (p, pty) = launch_proc_with_pty(command_line,
+ stdin=subprocess.PIPE if input_name == '-' else None,
+ stdout=subprocess.PIPE if output_name == '-' else None,
+ stderr=subprocess.PIPE,
+ echo=False)
+ if action == 'enc':
+ prompts = (b'Please enter passphrase: ', b'Please confirm passphrase: ')
+ else:
+ prompts = (b'Please enter passphrase: ',)
+ for prompt in prompts:
+ actual_prompt = p.stderr.read(len(prompt))
+ if actual_prompt != prompt:
+ raise qubes.exc.QubesException(
+ 'Unexpected prompt from scrypt: {}'.format(actual_prompt))
+ pty.write(passphrase.encode('utf-8') + b'\n')
+ pty.flush()
+ # save it here, so garbage collector would not close it (which would kill
+ # the child)
+ p.pty = pty
+ return p
+
+
+class Backup(object):
+ '''Backup operation manager. Usage:
+
+ >>> app = qubes.Qubes()
+ >>> # optional - you can use 'None' to use default list (based on
+ >>> # vm.include_in_backups property)
+ >>> vms = [app.domains[name] for name in ['my-vm1', 'my-vm2', 'my-vm3']]
+ >>> exclude_vms = []
+ >>> options = {
+ >>> 'encrypted': True,
+ >>> 'compressed': True,
+ >>> 'passphrase': 'This is very weak backup passphrase',
+ >>> 'target_vm': app.domains['sys-usb'],
+ >>> 'target_dir': '/media/disk',
+ >>> }
+ >>> backup_op = Backup(app, vms, exclude_vms, **options)
+ >>> print(backup_op.get_backup_summary())
+ >>> backup_op.backup_do()
+
+ See attributes of this object for all available options.
+
+ '''
+ class FileToBackup(object):
+ def __init__(self, file_path, subdir=None, name=None):
+ sz = qubes.storage.file.get_disk_usage(file_path)
+
+ if subdir is None:
+ abs_file_path = os.path.abspath(file_path)
+ abs_base_dir = os.path.abspath(
+ qubes.config.system_path["qubes_base_dir"]) + '/'
+ abs_file_dir = os.path.dirname(abs_file_path) + '/'
+ (nothing, directory, subdir) = abs_file_dir.partition(abs_base_dir)
+ assert nothing == ""
+ assert directory == abs_base_dir
+ else:
+ if len(subdir) > 0 and not subdir.endswith('/'):
+ subdir += '/'
+
+ #: real path to the file
+ self.path = file_path
+ #: size of the file
+ self.size = sz
+ #: directory in backup archive where file should be placed
+ self.subdir = subdir
+ #: use this name in the archive (aka rename)
+ self.name = os.path.basename(file_path)
+ if name is not None:
+ self.name = name
+
+ class VMToBackup(object):
+ def __init__(self, vm, files, subdir):
+ self.vm = vm
+ self.files = files
+ self.subdir = subdir
+
+ @property
+ def size(self):
+ return functools.reduce(lambda x, y: x + y.size, self.files, 0)
+
+ def __init__(self, app, vms_list=None, exclude_list=None, **kwargs):
+ """
+ If vms = None, include all (sensible) VMs;
+ exclude_list is always applied
+ """
+ super(Backup, self).__init__()
+
+ #: progress of the backup - bytes handled of the current VM
+ self.chunk_size = 100 * 1024 * 1024
+ self._current_vm_bytes = 0
+ #: progress of the backup - bytes handled of finished VMs
+ self._done_vms_bytes = 0
+ #: total backup size (set by :py:meth:`get_files_to_backup`)
+ self.total_backup_bytes = 0
+ #: application object
+ self.app = app
+ #: directory for temporary files - set after creating the directory
+ self.tmpdir = None
+
+ # Backup settings - defaults
+ #: should the backup be encrypted?
+ self.encrypted = True
+ #: should the backup be compressed?
+ self.compressed = True
+ #: what passphrase should be used to intergrity protect (and encrypt)
+ #: the backup; required
+ self.passphrase = None
+ #: custom hmac algorithm
+ self.hmac_algorithm = DEFAULT_HMAC_ALGORITHM
+ #: custom encryption algorithm
+ self.crypto_algorithm = DEFAULT_CRYPTO_ALGORITHM
+ #: custom compression filter; a program which process stdin to stdout
+ self.compression_filter = DEFAULT_COMPRESSION_FILTER
+ #: VM to which backup should be sent (if any)
+ self.target_vm = None
+ #: directory to save backup in (either in dom0 or target VM,
+ #: depending on :py:attr:`target_vm`
+ self.target_dir = None
+ #: callback for progress reporting. Will be called with one argument
+ #: - progress in percents
+ self.progress_callback = None
+ #: backup ID, needs to be unique (for a given user),
+ #: not necessary unpredictable; automatically generated
+ self.backup_id = datetime.datetime.now().strftime(
+ '%Y%m%dT%H%M%S-' + str(os.getpid()))
+
+ for key, value in kwargs.items():
+ if hasattr(self, key):
+ setattr(self, key, value)
+ else:
+ raise AttributeError(key)
+
+ #: whether backup was canceled
+ self.canceled = False
+ #: list of PIDs to kill on backup cancel
+ self.processes_to_kill_on_cancel = []
+
+ self.log = logging.getLogger('qubes.backup')
+
+ if not self.encrypted:
+ self.log.warning('\'encrypted\' option is ignored, backup is '
+ 'always encrypted')
+
+ if exclude_list is None:
+ exclude_list = []
+
+ if vms_list is None:
+ vms_list = [vm for vm in app.domains if vm.include_in_backups]
+
+ # Apply exclude list
+ self.vms_for_backup = [vm for vm in vms_list
+ if vm.name not in exclude_list]
+
+ self._files_to_backup = self.get_files_to_backup()
+
+ def __del__(self):
+ if self.tmpdir and os.path.exists(self.tmpdir):
+ shutil.rmtree(self.tmpdir)
+
+ def cancel(self):
+ """Cancel running backup operation. Can be called from another thread.
+ """
+ self.canceled = True
+ for proc in self.processes_to_kill_on_cancel:
+ try:
+ proc.terminate()
+ except OSError:
+ pass
+
+
+ def get_files_to_backup(self):
+ files_to_backup = {}
+ for vm in self.vms_for_backup:
+ if vm.qid == 0:
+ # handle dom0 later
+ continue
+
+ if self.encrypted:
+ subdir = 'vm%d/' % vm.qid
+ else:
+ subdir = None
+
+ vm_files = []
+ if vm.volumes['private'] is not None:
+ path_to_private_img = vm.storage.export('private')
+ vm_files.append(self.FileToBackup(path_to_private_img, subdir,
+ 'private.img'))
+
+ vm_files.append(self.FileToBackup(vm.icon_path, subdir))
+ vm_files.extend(self.FileToBackup(i, subdir)
+ for i in vm.fire_event('backup-get-files'))
+
+ # TODO: drop after merging firewall.xml into qubes.xml
+ firewall_conf = os.path.join(vm.dir_path, vm.firewall_conf)
+ if os.path.exists(firewall_conf):
+ vm_files.append(self.FileToBackup(firewall_conf, subdir))
+
+ if vm.updateable:
+ path_to_root_img = vm.storage.export('root')
+ vm_files.append(self.FileToBackup(path_to_root_img, subdir,
+ 'root.img'))
+ files_to_backup[vm.qid] = self.VMToBackup(vm, vm_files, subdir)
+
+ # Dom0 user home
+ if 0 in [vm.qid for vm in self.vms_for_backup]:
+ local_user = grp.getgrnam('qubes').gr_mem[0]
+ home_dir = pwd.getpwnam(local_user).pw_dir
+ # Home dir should have only user-owned files, so fix it now
+ # to prevent permissions problems - some root-owned files can
+ # left after 'sudo bash' and similar commands
+ subprocess.check_call(['sudo', 'chown', '-R', local_user, home_dir])
+
+ home_to_backup = [
+ self.FileToBackup(home_dir, 'dom0-home/')]
+ vm_files = home_to_backup
+
+ files_to_backup[0] = self.VMToBackup(self.app.domains[0],
+ vm_files,
+ os.path.join('dom0-home', os.path.basename(home_dir)))
+
+ self.total_backup_bytes = functools.reduce(
+ lambda x, y: x + y.size, files_to_backup.values(), 0)
+ return files_to_backup
+
+
+ def get_backup_summary(self):
+ summary = ""
+
+ fields_to_display = [
+ {"name": "VM", "width": 16},
+ {"name": "type", "width": 12},
+ {"name": "size", "width": 12}
+ ]
+
+ # Display the header
+ for f in fields_to_display:
+ fmt = "{{0:-^{0}}}-+".format(f["width"] + 1)
+ summary += fmt.format('-')
+ summary += "\n"
+ for f in fields_to_display:
+ fmt = "{{0:>{0}}} |".format(f["width"] + 1)
+ summary += fmt.format(f["name"])
+ summary += "\n"
+ for f in fields_to_display:
+ fmt = "{{0:-^{0}}}-+".format(f["width"] + 1)
+ summary += fmt.format('-')
+ summary += "\n"
+
+ files_to_backup = self._files_to_backup
+
+ for qid, vm_info in files_to_backup.items():
+ s = ""
+ fmt = "{{0:>{0}}} |".format(fields_to_display[0]["width"] + 1)
+ s += fmt.format(vm_info['vm'].name)
+
+ fmt = "{{0:>{0}}} |".format(fields_to_display[1]["width"] + 1)
+ if qid == 0:
+ s += fmt.format("User home")
+ elif isinstance(vm_info['vm'], qubes.vm.templatevm.TemplateVM):
+ s += fmt.format("Template VM")
+ else:
+ s += fmt.format("VM" + (" + Sys" if vm_info['vm'].updateable
+ else ""))
+
+ vm_size = vm_info['size']
+
+ fmt = "{{0:>{0}}} |".format(fields_to_display[2]["width"] + 1)
+ s += fmt.format(size_to_human(vm_size))
+
+ if qid != 0 and vm_info['vm'].is_running():
+ s += " <-- The VM is running, please shut it down before proceeding " \
+ "with the backup!"
+
+ summary += s + "\n"
+
+ for f in fields_to_display:
+ fmt = "{{0:-^{0}}}-+".format(f["width"] + 1)
+ summary += fmt.format('-')
+ summary += "\n"
+
+ fmt = "{{0:>{0}}} |".format(fields_to_display[0]["width"] + 1)
+ summary += fmt.format("Total size:")
+ fmt = "{{0:>{0}}} |".format(
+ fields_to_display[1]["width"] + 1 + 2 + fields_to_display[2][
+ "width"] + 1)
+ summary += fmt.format(size_to_human(self.total_backup_bytes))
+ summary += "\n"
+
+ for f in fields_to_display:
+ fmt = "{{0:-^{0}}}-+".format(f["width"] + 1)
+ summary += fmt.format('-')
+ summary += "\n"
+
+ vms_not_for_backup = [vm.name for vm in self.app.domains
+ if vm not in self.vms_for_backup]
+ summary += "VMs not selected for backup:\n - " + "\n - ".join(
+ sorted(vms_not_for_backup))
+
+ return summary
+
+ def prepare_backup_header(self):
+ header_file_path = os.path.join(self.tmpdir, HEADER_FILENAME)
+ backup_header = BackupHeader(
+ version=CURRENT_BACKUP_FORMAT_VERSION,
+ hmac_algorithm=self.hmac_algorithm,
+ crypto_algorithm=self.crypto_algorithm,
+ encrypted=self.encrypted,
+ compressed=self.compressed,
+ compression_filter=self.compression_filter,
+ backup_id=self.backup_id,
+ )
+ backup_header.save(header_file_path)
+ # Start encrypt, scrypt will also handle integrity
+ # protection
+ scrypt_passphrase = u'{filename}!{passphrase}'.format(
+ filename=HEADER_FILENAME, passphrase=self.passphrase)
+ scrypt = launch_scrypt(
+ 'enc', header_file_path, header_file_path + '.hmac',
+ scrypt_passphrase)
+
+ if scrypt.wait() != 0:
+ raise qubes.exc.QubesException(
+ "Failed to compute hmac of header file: "
+ + scrypt.stderr.read())
+ return HEADER_FILENAME, HEADER_FILENAME + ".hmac"
+
+
+ @staticmethod
+ def _queue_put_with_check(proc, vmproc, queue, element):
+ if queue.full():
+ if not proc.is_alive():
+ if vmproc:
+ message = ("Failed to write the backup, VM output:\n" +
+ vmproc.stderr.read())
+ else:
+ message = "Failed to write the backup. Out of disk space?"
+ raise qubes.exc.QubesException(message)
+ queue.put(element)
+
+ def _send_progress_update(self):
+ if callable(self.progress_callback):
+ progress = (
+ 100 * (self._done_vms_bytes + self._current_vm_bytes) /
+ self.total_backup_bytes)
+ self.progress_callback(progress)
+
+ def _add_vm_progress(self, bytes_done):
+ self._current_vm_bytes += bytes_done
+ self._send_progress_update()
+
+ def backup_do(self):
+ if self.passphrase is None:
+ raise qubes.exc.QubesException("No passphrase set")
+ qubes_xml = self.app.store
+ self.tmpdir = tempfile.mkdtemp()
+ shutil.copy(qubes_xml, os.path.join(self.tmpdir, 'qubes.xml'))
+ qubes_xml = os.path.join(self.tmpdir, 'qubes.xml')
+ backup_app = qubes.Qubes(qubes_xml)
+
+ files_to_backup = self._files_to_backup
+ # make sure backup_content isn't set initially
+ for vm in backup_app.domains:
+ vm.features['backup-content'] = False
+
+ for qid, vm_info in files_to_backup.items():
+ if qid != 0 and vm_info.vm.is_running():
+ raise qubes.exc.QubesVMNotHaltedError(vm_info.vm)
+ # VM is included in the backup
+ backup_app.domains[qid].features['backup-content'] = True
+ backup_app.domains[qid].features['backup-path'] = vm_info.subdir
+ backup_app.domains[qid].features['backup-size'] = vm_info.size
+ backup_app.save()
+
+ vmproc = None
+ tar_sparse = None
+ if self.target_vm is not None:
+ # Prepare the backup target (Qubes service call)
+ # If APPVM, STDOUT is a PIPE
+ vmproc = self.target_vm.run_service('qubes.Backup',
+ passio_popen=True, passio_stderr=True)
+ vmproc.stdin.write((self.target_dir.
+ replace("\r", "").replace("\n", "") + "\n").encode())
+ vmproc.stdin.flush()
+ backup_stdout = vmproc.stdin
+ self.processes_to_kill_on_cancel.append(vmproc)
+ else:
+ # Prepare the backup target (local file)
+ if os.path.isdir(self.target_dir):
+ backup_target = self.target_dir + "/qubes-{0}". \
+ format(time.strftime("%Y-%m-%dT%H%M%S"))
+ else:
+ backup_target = self.target_dir
+
+ # Create the target directory
+ if not os.path.exists(os.path.dirname(self.target_dir)):
+ raise qubes.exc.QubesException(
+ "ERROR: the backup directory for {0} does not exists".
+ format(self.target_dir))
+
+ # If not APPVM, STDOUT is a local file
+ backup_stdout = open(backup_target, 'wb')
+
+ # Tar with tape length does not deals well with stdout
+ # (close stdout between two tapes)
+ # For this reason, we will use named pipes instead
+ self.log.debug("Working in {}".format(self.tmpdir))
+
+ backup_pipe = os.path.join(self.tmpdir, "backup_pipe")
+ self.log.debug("Creating pipe in: {}".format(backup_pipe))
+ os.mkfifo(backup_pipe)
+
+ self.log.debug("Will backup: {}".format(files_to_backup))
+
+ header_files = self.prepare_backup_header()
+
+ # Setup worker to send encrypted data chunks to the backup_target
+ to_send = Queue(10)
+ send_proc = SendWorker(to_send, self.tmpdir, backup_stdout)
+ send_proc.start()
+
+ for f in header_files:
+ to_send.put(f)
+
+ qubes_xml_info = self.VMToBackup(
+ None,
+ [self.FileToBackup(qubes_xml, '')],
+ ''
+ )
+ for vm_info in itertools.chain([qubes_xml_info],
+ files_to_backup.values()):
+ for file_info in vm_info.files:
+
+ self.log.debug("Backing up {}".format(file_info))
+
+ backup_tempfile = os.path.join(
+ self.tmpdir, file_info.subdir,
+ file_info.name)
+ self.log.debug("Using temporary location: {}".format(
+ backup_tempfile))
+
+ # Ensure the temporary directory exists
+ if not os.path.isdir(os.path.dirname(backup_tempfile)):
+ os.makedirs(os.path.dirname(backup_tempfile))
+
+ # The first tar cmd can use any complex feature as we want.
+ # Files will be verified before untaring this.
+ # Prefix the path in archive with filename["subdir"] to have it
+ # verified during untar
+ tar_cmdline = (["tar", "-Pc", '--sparse',
+ "-f", backup_pipe,
+ '-C', os.path.dirname(file_info.path)] +
+ (['--dereference'] if
+ file_info.subdir != "dom0-home/" else []) +
+ ['--xform=s:^%s:%s\\0:' % (
+ os.path.basename(file_info.path),
+ file_info.subdir),
+ os.path.basename(file_info.path)
+ ])
+ file_stat = os.stat(file_info.path)
+ if stat.S_ISBLK(file_stat.st_mode) or \
+ file_info.name != os.path.basename(file_info.path):
+ # tar doesn't handle content of block device, use our
+ # writer
+ # also use our tar writer when renaming file
+ assert not stat.S_ISDIR(file_stat.st_mode),\
+ "Renaming directories not supported"
+ tar_cmdline = ['python3', '-m', 'qubes.tarwriter',
+ '--override-name=%s' % (
+ os.path.join(file_info.subdir, os.path.basename(
+ file_info.name))),
+ file_info.path,
+ backup_pipe]
+ if self.compressed:
+ tar_cmdline.insert(-2,
+ "--use-compress-program=%s" % self.compression_filter)
+
+ self.log.debug(" ".join(tar_cmdline))
+
+ # Pipe: tar-sparse | scrypt | tar | backup_target
+ # TODO: log handle stderr
+ tar_sparse = subprocess.Popen(
+ tar_cmdline)
+ self.processes_to_kill_on_cancel.append(tar_sparse)
+
+ # Wait for compressor (tar) process to finish or for any
+ # error of other subprocesses
+ i = 0
+ pipe = open(backup_pipe, 'rb')
+ run_error = "paused"
+ while run_error == "paused":
+ # Prepare a first chunk
+ chunkfile = backup_tempfile + ".%03d.enc" % i
+ i += 1
+
+ # Start encrypt, scrypt will also handle integrity
+ # protection
+ scrypt_passphrase = \
+ u'{backup_id}!{filename}!{passphrase}'.format(
+ backup_id=self.backup_id,
+ filename=os.path.relpath(chunkfile[:-4],
+ self.tmpdir),
+ passphrase=self.passphrase)
+ scrypt = launch_scrypt(
+ "enc", "-", chunkfile, scrypt_passphrase)
+
+ run_error = handle_streams(
+ pipe,
+ {'backup_target': scrypt.stdin},
+ {'vmproc': vmproc,
+ 'addproc': tar_sparse,
+ 'scrypt': scrypt,
+ },
+ self.chunk_size,
+ self._add_vm_progress
+ )
+
+ self.log.debug(
+ "Wait_backup_feedback returned: {}".format(run_error))
+
+ if self.canceled:
+ try:
+ tar_sparse.terminate()
+ except OSError:
+ pass
+ tar_sparse.wait()
+ to_send.put(QUEUE_ERROR)
+ send_proc.join()
+ shutil.rmtree(self.tmpdir)
+ raise BackupCanceledError("Backup canceled")
+ if run_error and run_error != "size_limit":
+ send_proc.terminate()
+ if run_error == "VM" and vmproc:
+ raise qubes.exc.QubesException(
+ "Failed to write the backup, VM output:\n" +
+ vmproc.stderr.read(MAX_STDERR_BYTES))
+ else:
+ raise qubes.exc.QubesException(
+ "Failed to perform backup: error in " +
+ run_error)
+
+ scrypt.stdin.close()
+ scrypt.wait()
+ self.log.debug("scrypt return code: {}".format(
+ scrypt.poll()))
+
+ # Send the chunk to the backup target
+ self._queue_put_with_check(
+ send_proc, vmproc, to_send,
+ os.path.relpath(chunkfile, self.tmpdir))
+
+ if tar_sparse.poll() is None or run_error == "size_limit":
+ run_error = "paused"
+ else:
+ self.processes_to_kill_on_cancel.remove(tar_sparse)
+ self.log.debug(
+ "Finished tar sparse with exit code {}".format(
+ tar_sparse.poll()))
+ pipe.close()
+
+ # This VM done, update progress
+ self._done_vms_bytes += vm_info.size
+ self._current_vm_bytes = 0
+ self._send_progress_update()
+ # Save date of last backup
+ if vm_info.vm:
+ vm_info.vm.backup_timestamp = datetime.datetime.now()
+
+ self._queue_put_with_check(send_proc, vmproc, to_send, QUEUE_FINISHED)
+ send_proc.join()
+ shutil.rmtree(self.tmpdir)
+
+ if self.canceled:
+ raise BackupCanceledError("Backup canceled")
+
+ if send_proc.exitcode != 0:
+ raise qubes.exc.QubesException(
+ "Failed to send backup: error in the sending process")
+
+ if vmproc:
+ self.log.debug("VMProc1 proc return code: {}".format(vmproc.poll()))
+ if tar_sparse is not None:
+ self.log.debug("Sparse1 proc return code: {}".format(
+ tar_sparse.poll()))
+ vmproc.stdin.close()
+
+ self.app.save()
+
+
+def handle_streams(stream_in, streams_out, processes, size_limit=None,
+ progress_callback=None):
+ '''
+ Copy stream_in to all streams_out and monitor all mentioned processes.
+ If any of them terminate with non-zero code, interrupt the process. Copy
+ at most `size_limit` data (if given).
+
+ :param stream_in: file-like object to read data from
+ :param streams_out: dict of file-like objects to write data to
+ :param processes: dict of subprocess.Popen objects to monitor
+ :param size_limit: int maximum data amount to process
+ :param progress_callback: callable function to report progress, will be
+ given copied data size (it should accumulate internally)
+ :return: failed process name, failed stream name, "size_limit" or None (
+ no error)
+ '''
+ buffer_size = 409600
+ bytes_copied = 0
+ while True:
+ if size_limit:
+ to_copy = min(buffer_size, size_limit - bytes_copied)
+ if to_copy <= 0:
+ return "size_limit"
+ else:
+ to_copy = buffer_size
+ buf = stream_in.read(to_copy)
+ if not len(buf):
+ # done
+ return None
+
+ if callable(progress_callback):
+ progress_callback(len(buf))
+ for name, stream in streams_out.items():
+ if stream is None:
+ continue
+ try:
+ stream.write(buf)
+ except IOError:
+ return name
+ bytes_copied += len(buf)
+
+ for name, proc in processes.items():
+ if proc is None:
+ continue
+ if proc.poll():
+ return name
+
+
+class ExtractWorker2(Process):
+ def __init__(self, queue, base_dir, passphrase, encrypted,
+ progress_callback, vmproc=None,
+ compressed=False, crypto_algorithm=DEFAULT_CRYPTO_ALGORITHM,
+ verify_only=False, relocate=None):
+ super(ExtractWorker2, self).__init__()
+ #: queue with files to extract
+ self.queue = queue
+ #: paths on the queue are relative to this dir
+ self.base_dir = base_dir
+ #: passphrase to decrypt/authenticate data
+ self.passphrase = passphrase
+ #: extract those files/directories to alternative locations (truncate,
+ # but not unlink target beforehand); if specific file is in the map,
+ # redirect it accordingly, otherwise check if the whole directory is
+ # there
+ self.relocate = relocate
+ #: is the backup encrypted?
+ self.encrypted = encrypted
+ #: is the backup compressed?
+ self.compressed = compressed
+ #: what crypto algorithm is used for encryption?
+ self.crypto_algorithm = crypto_algorithm
+ #: only verify integrity, don't extract anything
+ self.verify_only = verify_only
+ #: progress
+ self.blocks_backedup = 0
+ #: inner tar layer extraction (subprocess.Popen instance)
+ self.tar2_process = None
+ #: current inner tar archive name
+ self.tar2_current_file = None
+ #: set size of this file when tar report it on stderr (adjust LVM
+ # volume size)
+ self.adjust_output_size = None
+ #: decompressor subprocess.Popen instance
+ self.decompressor_process = None
+ #: decryptor subprocess.Popen instance
+ self.decryptor_process = None
+ #: callback reporting progress to UI
+ self.progress_callback = progress_callback
+ #: process (subprocess.Popen instance) feeding the data into
+ # extraction tool
+ self.vmproc = vmproc
+
+ #: pipe to feed the data into tar (use pipe instead of stdin,
+ # as stdin is used for tar control commands)
+ self.restore_pipe = os.path.join(self.base_dir, "restore_pipe")
+
+ self.log = logging.getLogger('qubes.backup.extract')
+ self.log.debug("Creating pipe in: {}".format(self.restore_pipe))
+ os.mkfifo(self.restore_pipe)
+
+ self.stderr_encoding = sys.stderr.encoding or 'utf-8'
+
+ def collect_tar_output(self):
+ if not self.tar2_process.stderr:
+ return
+
+ if self.tar2_process.poll() is None:
+ try:
+ new_lines = self.tar2_process.stderr \
+ .read(MAX_STDERR_BYTES).splitlines()
+ except IOError as e:
+ if e.errno == errno.EAGAIN:
+ return
+ else:
+ raise
+ else:
+ new_lines = self.tar2_process.stderr.readlines()
+
+ new_lines = map(lambda x: x.decode(self.stderr_encoding), new_lines)
+
+ msg_re = re.compile(r".*#[0-9].*restore_pipe")
+ debug_msg = filter(msg_re.match, new_lines)
+ self.log.debug('tar2_stderr: {}'.format('\n'.join(debug_msg)))
+ new_lines = filter(lambda x: not msg_re.match(x), new_lines)
+ if self.adjust_output_size:
+ # search for first file size reported by tar, after setting
+ # self.adjust_output_size (so don't look at self.tar2_stderr)
+ # this is used only when extracting single-file archive, so don't
+ # bother with checking file name
+ file_size_re = re.compile(r"^[^ ]+ [^ ]+/[^ ]+ *([0-9]+) .*")
+ for line in new_lines:
+ match = file_size_re.match(line)
+ if match:
+ file_size = match.groups()[0]
+ self.resize_lvm(self.adjust_output_size, file_size)
+ self.adjust_output_size = None
+ self.tar2_stderr += new_lines
+
+ def resize_lvm(self, dev, size):
+ # FIXME: HACK
+ try:
+ subprocess.check_call(
+ ['sudo', 'lvresize', '-f', '-L', str(size) + 'B', dev],
+ stdout=open(os.devnull, 'w'), stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as e:
+ if e.returncode == 3:
+ # already at the right size
+ pass
+ else:
+ raise
+
+ def run(self):
+ try:
+ self.__run__()
+ except Exception as e:
+ # Cleanup children
+ for process in [self.decompressor_process,
+ self.decryptor_process,
+ self.tar2_process]:
+ if process:
+ try:
+ process.terminate()
+ except OSError:
+ pass
+ process.wait()
+ self.log.error("ERROR: " + str(e))
+ raise
+
+ def handle_dir_relocations(self, dirname):
+ ''' Relocate files in given director when it's already extracted
+
+ :param dirname: directory path to handle (relative to backup root),
+ without trailing slash
+ '''
+
+ for old, new in self.relocate:
+ if not old.startswith(dirname + '/'):
+ continue
+ # if directory is relocated too (most likely is), the file
+ # is extracted there
+ if dirname in self.relocate:
+ old = old.replace(dirname, self.relocate[dirname], 1)
+ try:
+ stat_buf = os.stat(new)
+ if stat.S_ISBLK(stat_buf.st_mode):
+ # output file is block device (LVM) - adjust its
+ # size, otherwise it may fail
+ # from lack of space
+ self.resize_lvm(new, stat_buf.st_size)
+ except OSError: # ENOENT
+ pass
+ subprocess.check_call(
+ ['dd', 'if='+old, 'of='+new, 'conv=sparse'])
+ os.unlink(old)
+
+ def cleanup_tar2(self, wait=True, terminate=False):
+ if self.tar2_process is None:
+ return
+ if terminate:
+ self.tar2_process.terminate()
+ if wait:
+ self.tar2_process.wait()
+ elif self.tar2_process.poll() is None:
+ return
+ if self.tar2_process.returncode != 0:
+ self.collect_tar_output()
+ self.log.error(
+ "ERROR: unable to extract files for {0}, tar "
+ "output:\n {1}".
+ format(self.tar2_current_file,
+ "\n ".join(self.tar2_stderr)))
+ else:
+ # Finished extracting the tar file
+ self.collect_tar_output()
+ self.tar2_process = None
+ # if that was whole-directory archive, handle
+ # relocated files now
+ inner_name = os.path.splitext(self.tar2_current_file)[0]\
+ .replace(self.base_dir + '/', '')
+ if os.path.basename(inner_name) == '.':
+ self.handle_dir_relocations(
+ os.path.dirname(inner_name))
+ self.tar2_current_file = None
+ self.adjust_output_size = None
+
+ def __run__(self):
+ self.log.debug("Started sending thread")
+ self.log.debug("Moving to dir " + self.base_dir)
+ os.chdir(self.base_dir)
+
+ filename = None
+
+ for filename in iter(self.queue.get, None):
+ if filename in (QUEUE_FINISHED, QUEUE_ERROR):
+ break
+
+ self.log.debug("Extracting file " + filename)
+
+ if filename.endswith('.000'):
+ # next file
+ self.cleanup_tar2(wait=True, terminate=False)
+
+ inner_name = filename.rstrip('.000').replace(
+ self.base_dir + '/', '')
+ redirect_stdout = None
+ if self.relocate and inner_name in self.relocate:
+ # TODO: add `dd conv=sparse` when removing tar layer
+ tar2_cmdline = ['tar',
+ '-%sMvvOf' % ("t" if self.verify_only else "x"),
+ self.restore_pipe,
+ inner_name]
+ output_file = self.relocate[inner_name]
+ try:
+ stat_buf = os.stat(output_file)
+ if stat.S_ISBLK(stat_buf.st_mode):
+ # output file is block device (LVM) - adjust its
+ # size during extraction, otherwise it may fail
+ # from lack of space
+ self.adjust_output_size = output_file
+ except OSError: # ENOENT
+ pass
+ redirect_stdout = open(output_file, 'w')
+ elif self.relocate and \
+ os.path.dirname(inner_name) in self.relocate:
+ tar2_cmdline = ['tar',
+ '-%sMf' % ("t" if self.verify_only else "x"),
+ self.restore_pipe,
+ '-C', self.relocate[os.path.dirname(inner_name)],
+ # strip all directories - leave only final filename
+ '--strip-components', str(inner_name.count(os.sep)),
+ inner_name]
+
+ else:
+ tar2_cmdline = ['tar',
+ '-%sMkf' % ("t" if self.verify_only else "x"),
+ self.restore_pipe,
+ inner_name]
+
+ self.log.debug("Running command " + str(tar2_cmdline))
+ self.tar2_process = subprocess.Popen(tar2_cmdline,
+ stdin=subprocess.PIPE, stderr=subprocess.PIPE,
+ stdout=redirect_stdout)
+ fcntl.fcntl(self.tar2_process.stderr.fileno(), fcntl.F_SETFL,
+ fcntl.fcntl(self.tar2_process.stderr.fileno(),
+ fcntl.F_GETFL) | os.O_NONBLOCK)
+ self.tar2_stderr = []
+ elif not self.tar2_process:
+ # Extracting of the current archive failed, skip to the next
+ # archive
+ os.remove(filename)
+ continue
+ else:
+ self.collect_tar_output()
+ self.log.debug("Releasing next chunck")
+ self.tar2_process.stdin.write("\n")
+ self.tar2_process.stdin.flush()
+ self.tar2_current_file = filename
+
+ pipe = open(self.restore_pipe, 'wb')
+ monitor_processes = {
+ 'vmproc': self.vmproc,
+ 'addproc': self.tar2_process,
+ }
+ if self.encrypted:
+ # Start decrypt
+ self.decryptor_process = subprocess.Popen(
+ ["openssl", "enc",
+ "-d",
+ "-" + self.crypto_algorithm,
+ "-pass",
+ "pass:" + self.passphrase] +
+ (["-z"] if self.compressed else []),
+ stdin=open(filename, 'rb'),
+ stdout=subprocess.PIPE)
+ in_stream = self.decryptor_process.stdout
+ monitor_processes['decryptor'] = self.decryptor_process
+ elif self.compressed:
+ self.decompressor_process = subprocess.Popen(
+ ["gzip", "-d"],
+ stdin=open(filename, 'rb'),
+ stdout=subprocess.PIPE)
+ in_stream = self.decompressor_process.stdout
+ monitor_processes['decompresor'] = self.decompressor_process
+ else:
+ in_stream = open(filename, 'rb')
+
+ run_error = handle_streams(
+ in_stream,
+ {'target': pipe},
+ monitor_processes,
+ progress_callback=self.progress_callback)
+
+ try:
+ pipe.close()
+ except IOError as e:
+ if e.errno == errno.EPIPE:
+ self.log.debug(
+ "Got EPIPE while closing pipe to "
+ "the inner tar process")
+ # ignore the error
+ else:
+ raise
+ if run_error:
+ if run_error == "target":
+ self.collect_tar_output()
+ details = "\n".join(self.tar2_stderr)
+ else:
+ details = "%s failed" % run_error
+ self.log.error("Error while processing '{}': {}".format(
+ self.tar2_current_file, details))
+ self.cleanup_tar2(wait=True, terminate=True)
+
+ # Delete the file as we don't need it anymore
+ self.log.debug("Removing file " + filename)
+ os.remove(filename)
+
+ os.unlink(self.restore_pipe)
+
+ self.cleanup_tar2(wait=True, terminate=(filename == QUEUE_ERROR))
+ self.log.debug("Finished extracting thread")
+
+
+class ExtractWorker3(ExtractWorker2):
+ def __init__(self, queue, base_dir, passphrase, encrypted,
+ progress_callback, vmproc=None,
+ compressed=False, crypto_algorithm=DEFAULT_CRYPTO_ALGORITHM,
+ compression_filter=None, verify_only=False, relocate=None):
+ super(ExtractWorker3, self).__init__(queue, base_dir, passphrase,
+ encrypted,
+ progress_callback, vmproc,
+ compressed, crypto_algorithm,
+ verify_only, relocate)
+ self.compression_filter = compression_filter
+ os.unlink(self.restore_pipe)
+
+ def __run__(self):
+ self.log.debug("Started sending thread")
+ self.log.debug("Moving to dir " + self.base_dir)
+ os.chdir(self.base_dir)
+
+ filename = None
+
+ input_pipe = None
+ for filename in iter(self.queue.get, None):
+ if filename in (QUEUE_FINISHED, QUEUE_ERROR):
+ break
+
+ self.log.debug("Extracting file " + filename)
+
+ if filename.endswith('.000'):
+ # next file
+ if self.tar2_process is not None:
+ input_pipe.close()
+ self.cleanup_tar2(wait=True, terminate=False)
+
+ inner_name = filename.rstrip('.000').replace(
+ self.base_dir + '/', '')
+ redirect_stdout = None
+ if self.relocate and inner_name in self.relocate:
+ # TODO: add dd conv=sparse when removing tar layer
+ tar2_cmdline = ['tar',
+ '-%svvO' % ("t" if self.verify_only else "x"),
+ inner_name]
+ output_file = self.relocate[inner_name]
+ try:
+ stat_buf = os.stat(output_file)
+ if stat.S_ISBLK(stat_buf.st_mode):
+ # output file is block device (LVM) - adjust its
+ # size during extraction, otherwise it may fail
+ # from lack of space
+ self.adjust_output_size = output_file
+ except OSError: # ENOENT
+ pass
+ redirect_stdout = open(output_file, 'w')
+ elif self.relocate and \
+ os.path.dirname(inner_name) in self.relocate:
+ tar2_cmdline = ['tar',
+ '-%s' % ("t" if self.verify_only else "x"),
+ '-C', self.relocate[os.path.dirname(inner_name)],
+ # strip all directories - leave only final filename
+ '--strip-components', str(inner_name.count(os.sep)),
+ inner_name]
+ else:
+ tar2_cmdline = ['tar',
+ '-%sk' % ("t" if self.verify_only else "x"),
+ inner_name]
+
+ if self.compressed:
+ if self.compression_filter:
+ tar2_cmdline.insert(-1,
+ "--use-compress-program=%s" %
+ self.compression_filter)
+ else:
+ tar2_cmdline.insert(-1, "--use-compress-program=%s" %
+ DEFAULT_COMPRESSION_FILTER)
+
+ self.log.debug("Running command " + str(tar2_cmdline))
+ if self.encrypted:
+ # Start decrypt
+ self.decryptor_process = subprocess.Popen(
+ ["openssl", "enc",
+ "-d",
+ "-" + self.crypto_algorithm,
+ "-pass",
+ "pass:" + self.passphrase],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE)
+
+ self.tar2_process = subprocess.Popen(
+ tar2_cmdline,
+ stdin=self.decryptor_process.stdout,
+ stdout=redirect_stdout,
+ stderr=subprocess.PIPE)
+ input_pipe = self.decryptor_process.stdin
+ else:
+ self.tar2_process = subprocess.Popen(
+ tar2_cmdline,
+ stdin=subprocess.PIPE,
+ stdout=redirect_stdout,
+ stderr=subprocess.PIPE)
+ input_pipe = self.tar2_process.stdin
+
+ fcntl.fcntl(self.tar2_process.stderr.fileno(), fcntl.F_SETFL,
+ fcntl.fcntl(self.tar2_process.stderr.fileno(),
+ fcntl.F_GETFL) | os.O_NONBLOCK)
+ self.tar2_stderr = []
+ elif not self.tar2_process:
+ # Extracting of the current archive failed, skip to the next
+ # archive
+ os.remove(filename)
+ continue
+ else:
+ (basename, ext) = os.path.splitext(self.tar2_current_file)
+ previous_chunk_number = int(ext[1:])
+ expected_filename = basename + '.%03d' % (
+ previous_chunk_number+1)
+ if expected_filename != filename:
+ self.cleanup_tar2(wait=True, terminate=True)
+ self.log.error(
+ 'Unexpected file in archive: {}, expected {}'.format(
+ filename, expected_filename))
+ os.remove(filename)
+ continue
+ self.log.debug("Releasing next chunck")
+
+ self.tar2_current_file = filename
+
+ run_error = handle_streams(
+ open(filename, 'rb'),
+ {'target': input_pipe},
+ {'vmproc': self.vmproc,
+ 'addproc': self.tar2_process,
+ 'decryptor': self.decryptor_process,
+ },
+ progress_callback=self.progress_callback)
+
+ if run_error:
+ if run_error == "target":
+ self.collect_tar_output()
+ details = "\n".join(self.tar2_stderr)
+ else:
+ details = "%s failed" % run_error
+ if self.decryptor_process:
+ self.decryptor_process.terminate()
+ self.decryptor_process.wait()
+ self.decryptor_process = None
+ self.log.error("Error while processing '{}': {}".format(
+ self.tar2_current_file, details))
+ self.cleanup_tar2(wait=True, terminate=True)
+
+ # Delete the file as we don't need it anymore
+ self.log.debug("Removing file " + filename)
+ os.remove(filename)
+
+ if self.tar2_process is not None:
+ input_pipe.close()
+ if filename == QUEUE_ERROR:
+ if self.decryptor_process:
+ self.decryptor_process.terminate()
+ self.decryptor_process.wait()
+ self.decryptor_process = None
+ self.cleanup_tar2(terminate=(filename == QUEUE_ERROR))
+
+ self.log.debug("Finished extracting thread")
+
+
+def get_supported_hmac_algo(hmac_algorithm=None):
+ # Start with provided default
+ if hmac_algorithm:
+ yield hmac_algorithm
+ if hmac_algorithm != 'scrypt':
+ yield 'scrypt'
+ proc = subprocess.Popen(['openssl', 'list-message-digest-algorithms'],
+ stdout=subprocess.PIPE)
+ for algo in proc.stdout.readlines():
+ algo = algo.decode('ascii')
+ if '=>' in algo:
+ continue
+ yield algo.strip()
+ proc.wait()
+
+
+class BackupRestoreOptions(object):
+ def __init__(self):
+ #: use default NetVM if the one referenced in backup do not exists on
+ # the host
+ self.use_default_netvm = True
+ #: set NetVM to "none" if the one referenced in backup do not exists
+ # on the host
+ self.use_none_netvm = False
+ #: set template to default if the one referenced in backup do not
+ # exists on the host
+ self.use_default_template = True
+ #: use default kernel if the one referenced in backup do not exists
+ # on the host
+ self.use_default_kernel = True
+ #: restore dom0 home
+ self.dom0_home = True
+ #: dictionary how what templates should be used instead of those
+ # referenced in backup
+ self.replace_template = {}
+ #: restore dom0 home even if username is different
+ self.ignore_username_mismatch = False
+ #: do not restore data, only verify backup integrity
+ self.verify_only = False
+ #: automatically rename VM during restore, when it would conflict
+ # with existing one
+ self.rename_conflicting = True
+ #: list of VM names to exclude
+ self.exclude = []
+ #: restore VMs into selected storage pool
+ self.override_pool = None
+
+
+class BackupRestore(object):
+ """Usage:
+
+ >>> restore_op = BackupRestore(...)
+ >>> # adjust restore_op.options here
+ >>> restore_info = restore_op.get_restore_info()
+ >>> # manipulate restore_info to select VMs to restore here
+ >>> restore_op.restore_do(restore_info)
+ """
+
+ class VMToRestore(object):
+ #: VM excluded from restore by user
+ EXCLUDED = object()
+ #: VM with such name already exists on the host
+ ALREADY_EXISTS = object()
+ #: NetVM used by the VM does not exists on the host
+ MISSING_NETVM = object()
+ #: TemplateVM used by the VM does not exists on the host
+ MISSING_TEMPLATE = object()
+ #: Kernel used by the VM does not exists on the host
+ MISSING_KERNEL = object()
+
+ def __init__(self, vm):
+ self.vm = vm
+ if 'backup-path' in vm.features:
+ self.subdir = vm.features['backup-path']
+ else:
+ self.subdir = None
+ if 'backup-size' in vm.features and vm.features['backup-size']:
+ self.size = int(vm.features['backup-size'])
+ else:
+ self.size = 0
+ self.problems = set()
+ if hasattr(vm, 'template') and vm.template:
+ self.template = vm.template.name
+ else:
+ self.template = None
+ if vm.netvm:
+ self.netvm = vm.netvm.name
+ else:
+ self.netvm = None
+ self.name = vm.name
+ self.orig_template = None
+ self.restored_vm = None
+
+ @property
+ def good_to_go(self):
+ return len(self.problems) == 0
+
+ class Dom0ToRestore(VMToRestore):
+ #: backup was performed on system with different dom0 username
+ USERNAME_MISMATCH = object()
+
+ def __init__(self, vm, subdir=None):
+ super(BackupRestore.Dom0ToRestore, self).__init__(vm)
+ if subdir:
+ self.subdir = subdir
+ self.username = os.path.basename(subdir)
+
+ def __init__(self, app, backup_location, backup_vm, passphrase):
+ super(BackupRestore, self).__init__()
+
+ #: qubes.Qubes instance
+ self.app = app
+
+ #: options how the backup should be restored
+ self.options = BackupRestoreOptions()
+
+ #: VM from which backup should be retrieved
+ self.backup_vm = backup_vm
+ if backup_vm and backup_vm.qid == 0:
+ self.backup_vm = None
+
+ #: backup path, inside VM pointed by :py:attr:`backup_vm`
+ self.backup_location = backup_location
+
+ #: passphrase protecting backup integrity and optionally decryption
+ self.passphrase = passphrase
+
+ #: temporary directory used to extract the data before moving to the
+ # final location; should be on the same filesystem as /var/lib/qubes
+ self.tmpdir = tempfile.mkdtemp(prefix="restore", dir="/var/tmp")
+
+ #: list of processes (Popen objects) to kill on cancel
+ self.processes_to_kill_on_cancel = []
+
+ #: is the backup operation canceled
+ self.canceled = False
+
+ #: report restore progress, called with one argument - percents of
+ # data restored
+ # FIXME: convert to float [0,1]
+ self.progress_callback = None
+
+ self.log = logging.getLogger('qubes.backup')
+
+ #: basic information about the backup
+ self.header_data = self._retrieve_backup_header()
+
+ #: VMs included in the backup
+ self.backup_app = self._process_qubes_xml()
+
+ def cancel(self):
+ """Cancel running backup operation. Can be called from another thread.
+ """
+ self.canceled = True
+ for proc in self.processes_to_kill_on_cancel:
+ try:
+ proc.terminate()
+ except OSError:
+ pass
+
+ def _start_retrieval_process(self, filelist, limit_count, limit_bytes):
+ """Retrieve backup stream and extract it to :py:attr:`tmpdir`
+
+ :param filelist: list of files to extract; listing directory name
+ will extract the whole directory; use empty list to extract the whole
+ archive
+ :param limit_count: maximum number of files to extract
+ :param limit_bytes: maximum size of extracted data
+ :return: a touple of (Popen object of started process, file-like
+ object for reading extracted files list, file-like object for reading
+ errors)
+ """
+
+ vmproc = None
+ if self.backup_vm is not None:
+ # If APPVM, STDOUT is a PIPE
+ vmproc = self.backup_vm.run_service('qubes.Restore',
+ passio_popen=True, passio_stderr=True)
+ vmproc.stdin.write(
+ (self.backup_location.replace("\r", "").replace("\n",
+ "") + "\n").encode())
+ vmproc.stdin.flush()
+
+ # Send to tar2qfile the VMs that should be extracted
+ vmproc.stdin.write((" ".join(filelist) + "\n").encode())
+ vmproc.stdin.flush()
+ self.processes_to_kill_on_cancel.append(vmproc)
+
+ backup_stdin = vmproc.stdout
+ tar1_command = ['/usr/libexec/qubes/qfile-dom0-unpacker',
+ str(os.getuid()), self.tmpdir, '-v']
+ else:
+ backup_stdin = open(self.backup_location, 'rb')
+
+ tar1_command = ['tar',
+ '-ixv',
+ '-C', self.tmpdir] + filelist
+
+ tar1_env = os.environ.copy()
+ tar1_env['UPDATES_MAX_BYTES'] = str(limit_bytes)
+ tar1_env['UPDATES_MAX_FILES'] = str(limit_count)
+ self.log.debug("Run command" + str(tar1_command))
+ command = subprocess.Popen(
+ tar1_command,
+ stdin=backup_stdin,
+ stdout=vmproc.stdin if vmproc else subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=tar1_env)
+ self.processes_to_kill_on_cancel.append(command)
+
+ # qfile-dom0-unpacker output filelist on stderr
+ # and have stdout connected to the VM), while tar output filelist
+ # on stdout
+ if self.backup_vm:
+ filelist_pipe = command.stderr
+ # let qfile-dom0-unpacker hold the only open FD to the write end of
+ # pipe, otherwise qrexec-client will not receive EOF when
+ # qfile-dom0-unpacker terminates
+ vmproc.stdin.close()
+ else:
+ filelist_pipe = command.stdout
+
+ if self.backup_vm:
+ error_pipe = vmproc.stderr
+ else:
+ error_pipe = command.stderr
+ return command, filelist_pipe, error_pipe
+
+ def _verify_hmac(self, filename, hmacfile, algorithm=None):
+ def load_hmac(hmac_text):
+ if any(ord(x) not in range(128) for x in hmac_text):
+ raise qubes.exc.QubesException(
+ "Invalid content of {}".format(hmacfile))
+ hmac_text = hmac_text.strip().split("=")
+ if len(hmac_text) > 1:
+ hmac_text = hmac_text[1].strip()
+ else:
+ raise qubes.exc.QubesException(
+ "ERROR: invalid hmac file content")
+
+ return hmac_text
+ if algorithm is None:
+ algorithm = self.header_data.hmac_algorithm
+ passphrase = self.passphrase.encode('utf-8')
+ self.log.debug("Verifying file {}".format(filename))
+
+ if os.stat(os.path.join(self.tmpdir, hmacfile)).st_size > \
+ HMAC_MAX_SIZE:
+ raise qubes.exc.QubesException('HMAC file {} too large'.format(
+ hmacfile))
+
+ if hmacfile != filename + ".hmac":
+ raise qubes.exc.QubesException(
+ "ERROR: expected hmac for {}, but got {}".
+ format(filename, hmacfile))
+
+ if algorithm == 'scrypt':
+ # in case of 'scrypt' _verify_hmac is only used for backup header
+ assert filename == HEADER_FILENAME
+ self._verify_and_decrypt(hmacfile, HEADER_FILENAME + '.dec')
+ if open(os.path.join(self.tmpdir, filename), 'rb').read() != \
+ open(os.path.join(self.tmpdir, filename + '.dec'),
+ 'rb').read():
+ raise qubes.exc.QubesException(
+ 'Invalid hmac on {}'.format(filename))
+ else:
+ return True
+
+ hmac_proc = subprocess.Popen(
+ ["openssl", "dgst", "-" + algorithm, "-hmac", passphrase],
+ stdin=open(os.path.join(self.tmpdir, filename), 'rb'),
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ hmac_stdout, hmac_stderr = hmac_proc.communicate()
+
+ if len(hmac_stderr) > 0:
+ raise qubes.exc.QubesException(
+ "ERROR: verify file {0}: {1}".format(filename, hmac_stderr))
+ else:
+ self.log.debug("Loading hmac for file {}".format(filename))
+ hmac = load_hmac(open(os.path.join(self.tmpdir, hmacfile),
+ 'r', encoding='ascii').read())
+
+ if len(hmac) > 0 and load_hmac(hmac_stdout.decode('ascii')) == hmac:
+ os.unlink(os.path.join(self.tmpdir, hmacfile))
+ self.log.debug(
+ "File verification OK -> Sending file {}".format(filename))
+ return True
+ else:
+ raise qubes.exc.QubesException(
+ "ERROR: invalid hmac for file {0}: {1}. "
+ "Is the passphrase correct?".
+ format(filename, load_hmac(hmac_stdout.decode('ascii'))))
+
+ def _verify_and_decrypt(self, filename, output=None):
+ assert filename.endswith('.enc') or filename.endswith('.hmac')
+ fullname = os.path.join(self.tmpdir, filename)
+ (origname, _) = os.path.splitext(filename)
+ if output:
+ fulloutput = os.path.join(self.tmpdir, output)
+ else:
+ fulloutput = os.path.join(self.tmpdir, origname)
+ if origname == HEADER_FILENAME:
+ passphrase = u'{filename}!{passphrase}'.format(
+ filename=origname,
+ passphrase=self.passphrase)
+ else:
+ passphrase = u'{backup_id}!{filename}!{passphrase}'.format(
+ backup_id=self.header_data.backup_id,
+ filename=origname,
+ passphrase=self.passphrase)
+ p = launch_scrypt('dec', fullname, fulloutput, passphrase)
+ (_, stderr) = p.communicate()
+ if p.returncode != 0:
+ os.unlink(fulloutput)
+ raise qubes.exc.QubesException('failed to decrypt {}: {}'.format(
+ fullname, stderr))
+ # encrypted file is no longer needed
+ os.unlink(fullname)
+ return origname
+
+ def _retrieve_backup_header_files(self, files, allow_none=False):
+ (retrieve_proc, filelist_pipe, error_pipe) = \
+ self._start_retrieval_process(
+ files, len(files), 1024 * 1024)
+ filelist = filelist_pipe.read()
+ retrieve_proc_returncode = retrieve_proc.wait()
+ if retrieve_proc in self.processes_to_kill_on_cancel:
+ self.processes_to_kill_on_cancel.remove(retrieve_proc)
+ extract_stderr = error_pipe.read(MAX_STDERR_BYTES)
+
+ # wait for other processes (if any)
+ for proc in self.processes_to_kill_on_cancel:
+ if proc.wait() != 0:
+ raise qubes.exc.QubesException(
+ "Backup header retrieval failed (exit code {})".format(
+ proc.wait())
+ )
+
+ if retrieve_proc_returncode != 0:
+ if not filelist and 'Not found in archive' in extract_stderr:
+ if allow_none:
+ return None
+ else:
+ raise qubes.exc.QubesException(
+ "unable to read the qubes backup file {0} ({1}): {2}".format(
+ self.backup_location,
+ retrieve_proc.wait(),
+ extract_stderr
+ ))
+ actual_files = filelist.decode('ascii').splitlines()
+ if sorted(actual_files) != sorted(files):
+ raise qubes.exc.QubesException(
+ 'unexpected files in archive: got {!r}, expected {!r}'.format(
+ actual_files, files
+ ))
+ for f in files:
+ if not os.path.exists(os.path.join(self.tmpdir, f)):
+ if allow_none:
+ return None
+ else:
+ raise qubes.exc.QubesException(
+ 'Unable to retrieve file {} from backup {}: {}'.format(
+ f, self.backup_location, extract_stderr
+ )
+ )
+ return files
+
+ def _retrieve_backup_header(self):
+ """Retrieve backup header and qubes.xml. Only backup header is
+ analyzed, qubes.xml is left as-is
+ (not even verified/decrypted/uncompressed)
+
+ :return header_data
+ :rtype :py:class:`BackupHeader`
+ """
+
+ if not self.backup_vm and os.path.exists(
+ os.path.join(self.backup_location, 'qubes.xml')):
+ # backup format version 1 doesn't have header
+ header_data = BackupHeader()
+ header_data.version = 1
+ return header_data
+
+ header_files = self._retrieve_backup_header_files(
+ ['backup-header', 'backup-header.hmac'], allow_none=True)
+
+ if not header_files:
+ # R2-Beta3 didn't have backup header, so if none is found,
+ # assume it's version=2 and use values present at that time
+ header_data = BackupHeader(
+ version=2,
+ # place explicitly this value, because it is what format_version
+ # 2 have
+ hmac_algorithm='SHA1',
+ crypto_algorithm='aes-256-cbc',
+ # TODO: set encrypted to something...
+ )
+ else:
+ filename = HEADER_FILENAME
+ hmacfile = HEADER_FILENAME + '.hmac'
+ self.log.debug("Got backup header and hmac: {}, {}".format(
+ filename, hmacfile))
+
+ file_ok = False
+ hmac_algorithm = DEFAULT_HMAC_ALGORITHM
+ for hmac_algo in get_supported_hmac_algo(hmac_algorithm):
+ try:
+ if self._verify_hmac(filename, hmacfile, hmac_algo):
+ file_ok = True
+ break
+ except qubes.exc.QubesException as e:
+ self.log.debug(
+ 'Failed to verify {} using {}: {}'.format(
+ hmacfile, hmac_algo, str(e)))
+ # Ignore exception here, try the next algo
+ pass
+ if not file_ok:
+ raise qubes.exc.QubesException(
+ "Corrupted backup header (hmac verification "
+ "failed). Is the password correct?")
+ filename = os.path.join(self.tmpdir, filename)
+ header_data = BackupHeader(open(filename, 'rb').read())
+ os.unlink(filename)
+
+ return header_data
+
+ def _start_inner_extraction_worker(self, queue, relocate):
+ """Start a worker process, extracting inner layer of bacup archive,
+ extract them to :py:attr:`tmpdir`.
+ End the data by pushing QUEUE_FINISHED or QUEUE_ERROR to the queue.
+
+ :param queue :py:class:`Queue` object to handle files from
+ """
+
+ # Setup worker to extract encrypted data chunks to the restore dirs
+ # Create the process here to pass it options extracted from
+ # backup header
+ extractor_params = {
+ 'queue': queue,
+ 'base_dir': self.tmpdir,
+ 'passphrase': self.passphrase,
+ 'encrypted': self.header_data.encrypted,
+ 'compressed': self.header_data.compressed,
+ 'crypto_algorithm': self.header_data.crypto_algorithm,
+ 'verify_only': self.options.verify_only,
+ 'progress_callback': self.progress_callback,
+ 'relocate': relocate,
+ }
+ self.log.debug('Starting extraction worker in {}, file relocation '
+ 'map: {!r}'.format(self.tmpdir, relocate))
+ format_version = self.header_data.version
+ if format_version == 2:
+ extract_proc = ExtractWorker2(**extractor_params)
+ elif format_version in [3, 4]:
+ extractor_params['compression_filter'] = \
+ self.header_data.compression_filter
+ if format_version == 4:
+ # encryption already handled
+ extractor_params['encrypted'] = False
+ extract_proc = ExtractWorker3(**extractor_params)
+ else:
+ raise NotImplementedError(
+ "Backup format version %d not supported" % format_version)
+ extract_proc.start()
+ return extract_proc
+
+ def _process_qubes_xml(self):
+ """Verify, unpack and load qubes.xml. Possibly convert its format if
+ necessary. It expect that :py:attr:`header_data` is already populated,
+ and :py:meth:`retrieve_backup_header` was called.
+ """
+ if self.header_data.version == 1:
+ backup_app = qubes.core2migration.Core2Qubes(
+ os.path.join(self.backup_location, 'qubes.xml'),
+ offline_mode=True)
+ return backup_app
+ else:
+ if self.header_data.version in [2, 3]:
+ self._retrieve_backup_header_files(
+ ['qubes.xml.000', 'qubes.xml.000.hmac'])
+ self._verify_hmac("qubes.xml.000", "qubes.xml.000.hmac")
+ else:
+ self._retrieve_backup_header_files(['qubes.xml.000.enc'])
+ self._verify_and_decrypt('qubes.xml.000.enc')
+
+ queue = Queue()
+ queue.put("qubes.xml.000")
+ queue.put(QUEUE_FINISHED)
+
+ extract_proc = self._start_inner_extraction_worker(queue, None)
+ extract_proc.join()
+ if extract_proc.exitcode != 0:
+ raise qubes.exc.QubesException(
+ "unable to extract the qubes backup. "
+ "Check extracting process errors.")
+
+ if self.header_data.version in [2, 3]:
+ backup_app = qubes.core2migration.Core2Qubes(
+ os.path.join(self.tmpdir, 'qubes.xml'), offline_mode=True)
+ else:
+ backup_app = qubes.Qubes(os.path.join(self.tmpdir, 'qubes.xml'),
+ offline_mode=True)
+ # Not needed anymore - all the data stored in backup_app
+ os.unlink(os.path.join(self.tmpdir, 'qubes.xml'))
+ return backup_app
+
+ def _restore_vm_dirs(self, vms_dirs, vms_size, relocate):
+ # Currently each VM consists of at most 7 archives (count
+ # file_to_backup calls in backup_prepare()), but add some safety
+ # margin for further extensions. Each archive is divided into 100MB
+ # chunks. Additionally each file have own hmac file. So assume upper
+ # limit as 2*(10*COUNT_OF_VMS+TOTAL_SIZE/100MB)
+ limit_count = str(2 * (10 * len(vms_dirs) +
+ int(vms_size / (100 * 1024 * 1024))))
+
+ self.log.debug("Working in temporary dir:" + self.tmpdir)
+ self.log.info(
+ "Extracting data: " + size_to_human(vms_size) + " to restore")
+
+ # retrieve backup from the backup stream (either VM, or dom0 file)
+ (retrieve_proc, filelist_pipe, error_pipe) = \
+ self._start_retrieval_process(
+ vms_dirs, limit_count, vms_size)
+
+ to_extract = Queue()
+
+ # extract data retrieved by retrieve_proc
+ extract_proc = self._start_inner_extraction_worker(
+ to_extract, relocate)
+
+ try:
+ filename = None
+ hmacfile = None
+ nextfile = None
+ while True:
+ if self.canceled:
+ break
+ if not extract_proc.is_alive():
+ retrieve_proc.terminate()
+ retrieve_proc.wait()
+ if retrieve_proc in self.processes_to_kill_on_cancel:
+ self.processes_to_kill_on_cancel.remove(retrieve_proc)
+ # wait for other processes (if any)
+ for proc in self.processes_to_kill_on_cancel:
+ proc.wait()
+ break
+ if nextfile is not None:
+ filename = nextfile
+ else:
+ filename = filelist_pipe.readline().decode('ascii').strip()
+
+ self.log.debug("Getting new file:" + filename)
+
+ if not filename or filename == "EOF":
+ break
+
+ # if reading archive directly with tar, wait for next filename -
+ # tar prints filename before processing it, so wait for
+ # the next one to be sure that whole file was extracted
+ if not self.backup_vm:
+ nextfile = filelist_pipe.readline().decode('ascii').strip()
+
+ if self.header_data.version in [2, 3]:
+ if not self.backup_vm:
+ hmacfile = nextfile
+ nextfile = filelist_pipe.readline().\
+ decode('ascii').strip()
+ else:
+ hmacfile = filelist_pipe.readline().\
+ decode('ascii').strip()
+
+ if self.canceled:
+ break
+
+ self.log.debug("Getting hmac:" + hmacfile)
+ if not hmacfile or hmacfile == "EOF":
+ # Premature end of archive, either of tar1_command or
+ # vmproc exited with error
+ break
+ else: # self.header_data.version == 4
+ if not filename.endswith('.enc'):
+ raise qubes.exc.QubesException(
+ 'Invalid file extension found in archive: {}'.
+ format(filename))
+
+ if not any(map(lambda x: filename.startswith(x), vms_dirs)):
+ self.log.debug("Ignoring VM not selected for restore")
+ os.unlink(os.path.join(self.tmpdir, filename))
+ if hmacfile:
+ os.unlink(os.path.join(self.tmpdir, hmacfile))
+ continue
+
+ if self.header_data.version in [2, 3]:
+ self._verify_hmac(filename, hmacfile)
+ else:
+ # _verify_and_decrypt will write output to a file with
+ # '.enc' extension cut off. This is safe because:
+ # - `scrypt` tool will override output, so if the file was
+ # already there (received from the VM), it will be removed
+ # - incoming archive extraction will refuse to override
+ # existing file, so if `scrypt` already created one,
+ # it can not be manipulated by the VM
+ # - when the file is retrieved from the VM, it appears at
+ # the final form - if it's visible, VM have no longer
+ # influence over its content
+ #
+ # This all means that if the file was correctly verified
+ # + decrypted, we will surely access the right file
+ filename = self._verify_and_decrypt(filename)
+ to_extract.put(os.path.join(self.tmpdir, filename))
+
+ if self.canceled:
+ raise BackupCanceledError("Restore canceled",
+ tmpdir=self.tmpdir)
+
+ if retrieve_proc.wait() != 0:
+ raise qubes.exc.QubesException(
+ "unable to read the qubes backup file {0}: {1}"
+ .format(self.backup_location, error_pipe.read(
+ MAX_STDERR_BYTES)))
+ # wait for other processes (if any)
+ for proc in self.processes_to_kill_on_cancel:
+ proc.wait()
+ if proc.returncode != 0:
+ raise qubes.exc.QubesException(
+ "Backup completed, but VM receiving it reported an error "
+ "(exit code {})".format(proc.returncode))
+
+ if filename and filename != "EOF":
+ raise qubes.exc.QubesException(
+ "Premature end of archive, the last file was %s" % filename)
+ except:
+ to_extract.put(QUEUE_ERROR)
+ extract_proc.join()
+ raise
+ else:
+ to_extract.put(QUEUE_FINISHED)
+
+ self.log.debug("Waiting for the extraction process to finish...")
+ extract_proc.join()
+ self.log.debug("Extraction process finished with code: {}".format(
+ extract_proc.exitcode))
+ if extract_proc.exitcode != 0:
+ raise qubes.exc.QubesException(
+ "unable to extract the qubes backup. "
+ "Check extracting process errors.")
+
+ def generate_new_name_for_conflicting_vm(self, orig_name, restore_info):
+ number = 1
+ if len(orig_name) > 29:
+ orig_name = orig_name[0:29]
+ new_name = orig_name
+ while (new_name in restore_info.keys() or
+ new_name in map(lambda x: x.name,
+ restore_info.values()) or
+ new_name in self.app.domains):
+ new_name = str('{}{}'.format(orig_name, number))
+ number += 1
+ if number == 100:
+ # give up
+ return None
+ return new_name
+
+ def restore_info_verify(self, restore_info):
+ for vm in restore_info.keys():
+ if vm in ['dom0']:
+ continue
+
+ vm_info = restore_info[vm]
+ assert isinstance(vm_info, self.VMToRestore)
+
+ vm_info.problems.clear()
+ if vm in self.options.exclude:
+ vm_info.problems.add(self.VMToRestore.EXCLUDED)
+
+ if not self.options.verify_only and \
+ vm_info.name in self.app.domains:
+ if self.options.rename_conflicting:
+ new_name = self.generate_new_name_for_conflicting_vm(
+ vm, restore_info
+ )
+ if new_name is not None:
+ vm_info.name = new_name
+ else:
+ vm_info.problems.add(self.VMToRestore.ALREADY_EXISTS)
+ else:
+ vm_info.problems.add(self.VMToRestore.ALREADY_EXISTS)
+
+ # check template
+ if vm_info.template:
+ template_name = vm_info.template
+ try:
+ host_template = self.app.domains[template_name]
+ except KeyError:
+ host_template = None
+ if not host_template \
+ or not isinstance(host_template,
+ qubes.vm.templatevm.TemplateVM):
+ # Maybe the (custom) template is in the backup?
+ if not (template_name in restore_info.keys() and
+ restore_info[template_name].good_to_go and
+ isinstance(restore_info[template_name].vm,
+ qubes.vm.templatevm.TemplateVM)):
+ if self.options.use_default_template and \
+ self.app.default_template:
+ if vm_info.orig_template is None:
+ vm_info.orig_template = template_name
+ vm_info.template = self.app.default_template.name
+ else:
+ vm_info.problems.add(
+ self.VMToRestore.MISSING_TEMPLATE)
+
+ # check netvm
+ if not vm_info.vm.property_is_default('netvm') and vm_info.netvm:
+ netvm_name = vm_info.netvm
+
+ try:
+ netvm_on_host = self.app.domains[netvm_name]
+ except KeyError:
+ netvm_on_host = None
+ # No netvm on the host?
+ if not ((netvm_on_host is not None)
+ and netvm_on_host.provides_network):
+
+ # Maybe the (custom) netvm is in the backup?
+ if not (netvm_name in restore_info.keys() and
+ restore_info[netvm_name].good_to_go and
+ restore_info[netvm_name].vm.provides_network):
+ if self.options.use_default_netvm:
+ vm_info.vm.netvm = qubes.property.DEFAULT
+ elif self.options.use_none_netvm:
+ vm_info.netvm = None
+ else:
+ vm_info.problems.add(self.VMToRestore.MISSING_NETVM)
+
+ # check kernel
+ if hasattr(vm_info.vm, 'kernel'):
+ installed_kernels = os.listdir(os.path.join(
+ qubes.config.qubes_base_dir,
+ qubes.config.system_path['qubes_kernels_base_dir']))
+ # if uses default kernel - do not validate it
+ # allow kernel=None only for HVM,
+ # otherwise require valid kernel
+ if not (vm_info.vm.property_is_default('kernel')
+ or (not vm_info.vm.kernel and vm_info.vm.hvm)
+ or vm_info.vm.kernel in installed_kernels):
+ if self.options.use_default_kernel:
+ vm_info.vm.kernel = qubes.property.DEFAULT
+ else:
+ vm_info.problems.add(self.VMToRestore.MISSING_KERNEL)
+
+ return restore_info
+
+ def _is_vm_included_in_backup_v1(self, check_vm):
+ if check_vm.qid == 0:
+ return os.path.exists(
+ os.path.join(self.backup_location, 'dom0-home'))
+
+ # DisposableVM
+ if check_vm.dir_path is None:
+ return False
+
+ backup_vm_dir_path = check_vm.dir_path.replace(
+ qubes.config.system_path["qubes_base_dir"], self.backup_location)
+
+ if os.path.exists(backup_vm_dir_path):
+ return True
+ else:
+ return False
+
+ @staticmethod
+ def _is_vm_included_in_backup_v2(check_vm):
+ if 'backup-content' in check_vm.features:
+ return check_vm.features['backup-content']
+ else:
+ return False
+
+ def _find_template_name(self, template):
+ if template in self.options.replace_template:
+ return self.options.replace_template[template]
+ return template
+
+ def _is_vm_included_in_backup(self, vm):
+ if self.header_data.version == 1:
+ return self._is_vm_included_in_backup_v1(vm)
+ elif self.header_data.version in [2, 3, 4]:
+ return self._is_vm_included_in_backup_v2(vm)
+ else:
+ raise qubes.exc.QubesException(
+ "Unknown backup format version: {}".format(
+ self.header_data.version))
+
+ def get_restore_info(self):
+ # Format versions:
+ # 1 - Qubes R1, Qubes R2 beta1, beta2
+ # 2 - Qubes R2 beta3+
+
+ vms_to_restore = {}
+
+ for vm in self.backup_app.domains:
+ if vm.qid == 0:
+ # Handle dom0 as special case later
+ continue
+ if self._is_vm_included_in_backup(vm):
+ self.log.debug("{} is included in backup".format(vm.name))
+
+ vms_to_restore[vm.name] = self.VMToRestore(vm)
+
+ if hasattr(vm, 'template'):
+ templatevm_name = self._find_template_name(
+ vm.template.name)
+ vms_to_restore[vm.name].template = templatevm_name
+
+ # Set to None to not confuse QubesVm object from backup
+ # collection with host collection (further in clone_attrs).
+ vm.netvm = None
+
+ vms_to_restore = self.restore_info_verify(vms_to_restore)
+
+ # ...and dom0 home
+ if self.options.dom0_home and \
+ self._is_vm_included_in_backup(self.backup_app.domains[0]):
+ vm = self.backup_app.domains[0]
+ if self.header_data.version == 1:
+ subdir = os.listdir(os.path.join(self.backup_location,
+ 'dom0-home'))[0]
+ else:
+ subdir = None
+ vms_to_restore['dom0'] = self.Dom0ToRestore(vm, subdir)
+ local_user = grp.getgrnam('qubes').gr_mem[0]
+
+ if vms_to_restore['dom0'].username != local_user:
+ if not self.options.ignore_username_mismatch:
+ vms_to_restore['dom0'].problems.add(
+ self.Dom0ToRestore.USERNAME_MISMATCH)
+
+ return vms_to_restore
+
+ @staticmethod
+ def get_restore_summary(restore_info):
+ fields = {
+ "qid": {"func": "vm.qid"},
+
+ "name": {"func": "('[' if isinstance(vm, qubes.vm.templatevm.TemplateVM) else '')\
+ + ('{' if vm.provides_network else '')\
+ + vm.name \
+ + (']' if isinstance(vm, qubes.vm.templatevm.TemplateVM) else '')\
+ + ('}' if vm.provides_network else '')"},
+
+ "type": {"func": "'Tpl' if isinstance(vm, qubes.vm.templatevm.TemplateVM) else \
+ 'App' if isinstance(vm, qubes.vm.appvm.AppVM) else \
+ vm.__class__.__name__.replace('VM','')"},
+
+ "updbl": {"func": "'Yes' if vm.updateable else ''"},
+
+ "template": {"func": "'n/a' if not hasattr(vm, 'template') "
+ "else vm_info.template"},
+
+ "netvm": {"func": "('*' if vm.property_is_default('netvm') else '') +\
+ vm_info.netvm if vm_info.netvm is not None "
+ "else '-'"},
+
+ "label": {"func": "vm.label.name"},
+ }
+
+ fields_to_display = ["name", "type", "template", "updbl",
+ "netvm", "label"]
+
+ # First calculate the maximum width of each field we want to display
+ total_width = 0
+ for f in fields_to_display:
+ fields[f]["max_width"] = len(f)
+ for vm_info in restore_info.values():
+ if vm_info.vm:
+ # noinspection PyUnusedLocal
+ vm = vm_info.vm
+ l = len(str(eval(fields[f]["func"])))
+ if l > fields[f]["max_width"]:
+ fields[f]["max_width"] = l
+ total_width += fields[f]["max_width"]
+
+ summary = ""
+ summary += "The following VMs are included in the backup:\n"
+ summary += "\n"
+
+ # Display the header
+ for f in fields_to_display:
+ # noinspection PyTypeChecker
+ fmt = "{{0:-^{0}}}-+".format(fields[f]["max_width"] + 1)
+ summary += fmt.format('-')
+ summary += "\n"
+ for f in fields_to_display:
+ # noinspection PyTypeChecker
+ fmt = "{{0:>{0}}} |".format(fields[f]["max_width"] + 1)
+ summary += fmt.format(f)
+ summary += "\n"
+ for f in fields_to_display:
+ # noinspection PyTypeChecker
+ fmt = "{{0:-^{0}}}-+".format(fields[f]["max_width"] + 1)
+ summary += fmt.format('-')
+ summary += "\n"
+
+ for vm_info in restore_info.values():
+ assert isinstance(vm_info, BackupRestore.VMToRestore)
+ # Skip non-VM here
+ if not vm_info.vm:
+ continue
+ # noinspection PyUnusedLocal
+ vm = vm_info.vm
+ s = ""
+ for f in fields_to_display:
+ # noinspection PyTypeChecker
+ fmt = "{{0:>{0}}} |".format(fields[f]["max_width"] + 1)
+ s += fmt.format(eval(fields[f]["func"]))
+
+ if BackupRestore.VMToRestore.EXCLUDED in vm_info.problems:
+ s += " <-- Excluded from restore"
+ elif BackupRestore.VMToRestore.ALREADY_EXISTS in vm_info.problems:
+ s += " <-- A VM with the same name already exists on the host!"
+ elif BackupRestore.VMToRestore.MISSING_TEMPLATE in \
+ vm_info.problems:
+ s += " <-- No matching template on the host " \
+ "or in the backup found!"
+ elif BackupRestore.VMToRestore.MISSING_NETVM in \
+ vm_info.problems:
+ s += " <-- No matching netvm on the host " \
+ "or in the backup found!"
+ else:
+ if vm_info.orig_template:
+ s += " <-- Original template was '{}'".format(
+ vm_info.orig_template)
+ if vm_info.name != vm_info.vm.name:
+ s += " <-- Will be renamed to '{}'".format(
+ vm_info.name)
+
+ summary += s + "\n"
+
+ if 'dom0' in restore_info.keys():
+ s = ""
+ for f in fields_to_display:
+ # noinspection PyTypeChecker
+ fmt = "{{0:>{0}}} |".format(fields[f]["max_width"] + 1)
+ if f == "name":
+ s += fmt.format("Dom0")
+ elif f == "type":
+ s += fmt.format("Home")
+ else:
+ s += fmt.format("")
+ if BackupRestore.Dom0ToRestore.USERNAME_MISMATCH in \
+ restore_info['dom0'].problems:
+ s += " <-- username in backup and dom0 mismatch"
+
+ summary += s + "\n"
+
+ return summary
+
+ def _restore_vm_dir_v1(self, src_dir, dst_dir):
+
+ backup_src_dir = src_dir.replace(
+ qubes.config.system_path["qubes_base_dir"], self.backup_location)
+
+ # We prefer to use Linux's cp, because it nicely handles sparse files
+ cp_retcode = subprocess.call(
+ ["cp", "-rp", "--reflink=auto", backup_src_dir, dst_dir])
+ if cp_retcode != 0:
+ raise qubes.exc.QubesException(
+ "*** Error while copying file {0} to {1}".format(backup_src_dir,
+ dst_dir))
+
+ @staticmethod
+ def _templates_first(vms):
+ def key_function(instance):
+ if isinstance(instance, qubes.vm.BaseVM):
+ return isinstance(instance, qubes.vm.templatevm.TemplateVM)
+ elif hasattr(instance, 'vm'):
+ return key_function(instance.vm)
+ else:
+ return 0
+ return sorted(vms,
+ key=key_function,
+ reverse=True)
+
+ def restore_do(self, restore_info):
+ '''
+
+
+ High level workflow:
+ 1. Create VMs object in host collection (qubes.xml)
+ 2. Create them on disk (vm.create_on_disk)
+ 3. Restore VM data, overriding/converting VM files
+ 4. Apply possible fixups and save qubes.xml
+
+ :param restore_info:
+ :return:
+ '''
+
+ # FIXME handle locking
+
+ restore_info = self.restore_info_verify(restore_info)
+
+ self._restore_vms_metadata(restore_info)
+
+ # Perform VM restoration in backup order
+ vms_dirs = []
+ relocate = {}
+ vms_size = 0
+ for vm_info in self._templates_first(restore_info.values()):
+ vm = vm_info.restored_vm
+ if vm:
+ vms_size += int(vm_info.size)
+ vms_dirs.append(vm_info.subdir)
+ relocate[vm_info.subdir.rstrip('/')] = vm.dir_path
+ for name, volume in vm.volumes.items():
+ if not volume.save_on_stop:
+ continue
+ export_path = vm.storage.export(name)
+ backup_path = os.path.join(
+ vm_info.vm.dir_path, name + '.img')
+ if backup_path != export_path:
+ relocate[
+ os.path.join(vm_info.subdir, name + '.img')] = \
+ export_path
+
+ if self.header_data.version >= 2:
+ if 'dom0' in restore_info.keys() and \
+ restore_info['dom0'].good_to_go:
+ vms_dirs.append(os.path.dirname(restore_info['dom0'].subdir))
+ vms_size += restore_info['dom0'].size
+
+ try:
+ self._restore_vm_dirs(vms_dirs=vms_dirs, vms_size=vms_size,
+ relocate=relocate)
+ except qubes.exc.QubesException:
+ if self.options.verify_only:
+ raise
+ else:
+ self.log.warning(
+ "Some errors occurred during data extraction, "
+ "continuing anyway to restore at least some "
+ "VMs")
+ else:
+ for vm_info in self._templates_first(restore_info.values()):
+ vm = vm_info.restored_vm
+ if vm:
+ try:
+ self._restore_vm_dir_v1(vm_info.vm.dir_path,
+ os.path.dirname(vm.dir_path))
+ except qubes.exc.QubesException as e:
+ if self.options.verify_only:
+ raise
+ else:
+ self.log.error(
+ "Failed to restore VM '{}': {}".format(
+ vm.name, str(e)))
+ vm.remove_from_disk()
+ del self.app.domains[vm]
+
+ if self.options.verify_only:
+ self.log.warning(
+ "Backup verification not supported for this backup format.")
+
+ if self.options.verify_only:
+ shutil.rmtree(self.tmpdir)
+ return
+
+ for vm_info in self._templates_first(restore_info.values()):
+ if not vm_info.restored_vm:
+ continue
+ try:
+ vm_info.restored_vm.fire_event('domain-restore')
+ except Exception as err:
+ self.log.error("ERROR during appmenu restore: "
+ "{0}".format(err))
+ self.log.warning(
+ "*** VM '{0}' will not have appmenus".format(vm_info.name))
+
+ try:
+ vm_info.restored_vm.storage.verify()
+ except Exception as err:
+ self.log.error("ERROR: {0}".format(err))
+ if vm_info.restored_vm:
+ vm_info.restored_vm.remove_from_disk()
+ del self.app.domains[vm_info.restored_vm]
+
+ self.app.save()
+
+ if self.canceled:
+ if self.header_data.version >= 2:
+ raise BackupCanceledError("Restore canceled",
+ tmpdir=self.tmpdir)
+ else:
+ raise BackupCanceledError("Restore canceled")
+
+ # ... and dom0 home as last step
+ if 'dom0' in restore_info.keys() and restore_info['dom0'].good_to_go:
+ backup_path = restore_info['dom0'].subdir
+ local_user = grp.getgrnam('qubes').gr_mem[0]
+ home_dir = pwd.getpwnam(local_user).pw_dir
+ if self.header_data.version == 1:
+ backup_dom0_home_dir = os.path.join(self.backup_location,
+ backup_path)
+ else:
+ backup_dom0_home_dir = os.path.join(self.tmpdir, backup_path)
+ restore_home_backupdir = "home-pre-restore-{0}".format(
+ time.strftime("%Y-%m-%d-%H%M%S"))
+
+ self.log.info(
+ "Restoring home of user '{0}'...".format(local_user))
+ self.log.info(
+ "Existing files/dirs backed up in '{0}' dir".format(
+ restore_home_backupdir))
+ os.mkdir(home_dir + '/' + restore_home_backupdir)
+ for f in os.listdir(backup_dom0_home_dir):
+ home_file = home_dir + '/' + f
+ if os.path.exists(home_file):
+ os.rename(home_file,
+ home_dir + '/' + restore_home_backupdir + '/' + f)
+ if self.header_data.version == 1:
+ subprocess.call(
+ ["cp", "-nrp", "--reflink=auto",
+ backup_dom0_home_dir + '/' + f, home_file])
+ elif self.header_data.version >= 2:
+ shutil.move(backup_dom0_home_dir + '/' + f, home_file)
+ retcode = subprocess.call(['sudo', 'chown', '-R',
+ local_user, home_dir])
+ if retcode != 0:
+ self.log.error("*** Error while setting home directory owner")
+
+ shutil.rmtree(self.tmpdir)
+ self.log.info("-> Done. Please install updates for all the restored "
+ "templates.")
+
+ def _restore_vms_metadata(self, restore_info):
+ vms = {}
+ for vm_info in restore_info.values():
+ assert isinstance(vm_info, self.VMToRestore)
+ if not vm_info.vm:
+ continue
+ if not vm_info.good_to_go:
+ continue
+ vm = vm_info.vm
+ vms[vm.name] = vm
+
+ # First load templates, then other VMs
+ for vm in self._templates_first(vms.values()):
+ if self.canceled:
+ # only break the loop to save qubes.xml
+ # with already restored VMs
+ break
+ self.log.info("-> Restoring {0}...".format(vm.name))
+ kwargs = {}
+ if hasattr(vm, 'template'):
+ template = restore_info[vm.name].template
+ # handle potentially renamed template
+ if template in restore_info \
+ and restore_info[template].good_to_go:
+ template = restore_info[template].name
+ kwargs['template'] = template
+
+ new_vm = None
+ vm_name = restore_info[vm.name].name
+
+ try:
+ # first only minimal set, later clone_properties
+ # will be called
+ cls = self.app.get_vm_class(vm.__class__.__name__)
+ new_vm = self.app.add_new_vm(
+ cls,
+ name=vm_name,
+ label=vm.label,
+ installed_by_rpm=False,
+ **kwargs)
+ if os.path.exists(new_vm.dir_path):
+ move_to_path = tempfile.mkdtemp('', os.path.basename(
+ new_vm.dir_path), os.path.dirname(new_vm.dir_path))
+ try:
+ os.rename(new_vm.dir_path, move_to_path)
+ self.log.warning(
+ "*** Directory {} already exists! It has "
+ "been moved to {}".format(new_vm.dir_path,
+ move_to_path))
+ except OSError:
+ self.log.error(
+ "*** Directory {} already exists and "
+ "cannot be moved!".format(new_vm.dir_path))
+ self.log.warning("Skipping VM {}...".format(
+ vm.name))
+ continue
+ except Exception as err:
+ self.log.error("ERROR: {0}".format(err))
+ self.log.warning("*** Skipping VM: {0}".format(vm.name))
+ if new_vm:
+ del self.app.domains[new_vm.qid]
+ continue
+
+ # remove no longer needed backup metadata
+ if 'backup-content' in vm.features:
+ del vm.features['backup-content']
+ del vm.features['backup-size']
+ del vm.features['backup-path']
+ try:
+ # exclude VM references - handled manually according to
+ # restore options
+ proplist = [prop for prop in new_vm.property_list()
+ if prop.clone and prop.__name__ not in
+ ['template', 'netvm', 'dispvm_netvm']]
+ new_vm.clone_properties(vm, proplist=proplist)
+ except Exception as err:
+ self.log.error("ERROR: {0}".format(err))
+ self.log.warning("*** Some VM property will not be "
+ "restored")
+
+ if not self.options.verify_only:
+ try:
+ # have it here, to (maybe) patch storage config before
+ # creating child VMs (template first)
+ # TODO: adjust volumes config - especially size
+ new_vm.create_on_disk(pool=self.options.override_pool)
+ except qubes.exc.QubesException as e:
+ self.log.warning("Failed to create VM {}: {}".format(
+ vm.name, str(e)))
+ del self.app.domains[new_vm]
+ continue
+
+ restore_info[vm.name].restored_vm = new_vm
+
+ # Set network dependencies - only non-default netvm setting
+ for vm in vms.values():
+ vm_info = restore_info[vm.name]
+ vm_name = vm_info.name
+ try:
+ host_vm = self.app.domains[vm_name]
+ except KeyError:
+ # Failed/skipped VM
+ continue
+
+ if not vm.property_is_default('netvm'):
+ if vm_info.netvm in restore_info:
+ host_vm.netvm = restore_info[vm_info.netvm].name
+ else:
+ host_vm.netvm = vm_info.netvm
+
+# vim:sw=4:et:
diff --git a/qubes/config.py b/qubes/config.py
new file mode 100644
index 00000000..0663f8dd
--- /dev/null
+++ b/qubes/config.py
@@ -0,0 +1,110 @@
+#
+# The Qubes OS Project, https://www.qubes-os.org/
+#
+# Copyright (C) 2010-2015 Joanna Rutkowska
+# Copyright (C) 2014-2015 Wojtek Porczyk
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+
+#
+# THIS FILE SHOULD BE CONFIGURED PER PRODUCT
+# or better, once first custom product arrives,
+# make a real /etc/qubes/master.conf or whatever
+#
+
+'''Constants which can be configured in one place'''
+
+import os.path
+
+qubes_base_dir = "/var/lib/qubes"
+system_path = {
+ 'qubes_guid_path': '/usr/bin/qubes-guid',
+ 'qrexec_daemon_path': '/usr/lib/qubes/qrexec-daemon',
+ 'qrexec_client_path': '/usr/lib/qubes/qrexec-client',
+ 'qubesdb_daemon_path': '/usr/sbin/qubesdb-daemon',
+
+ 'qubes_base_dir': qubes_base_dir,
+
+ # Relative to qubes_base_dir
+ 'qubes_appvms_dir': 'appvms',
+ 'qubes_templates_dir': 'vm-templates',
+ 'qubes_servicevms_dir': 'servicevms',
+ 'qubes_store_filename': 'qubes.xml',
+ 'qubes_kernels_base_dir': 'vm-kernels',
+
+ # qubes_icon_dir is obsolete
+ # use QIcon.fromTheme() where applicable
+ 'qubes_icon_dir': '/usr/share/icons/hicolor/128x128/devices',
+
+ 'qrexec_policy_dir': '/etc/qubes-rpc/policy',
+
+ 'config_template_pv': '/usr/share/qubes/vm-template.xml',
+}
+
+vm_files = {
+ 'root_img': 'root.img',
+ 'rootcow_img': 'root-cow.img',
+ 'volatile_img': 'volatile.img',
+ 'clean_volatile_img': 'clean-volatile.img.tar',
+ 'private_img': 'private.img',
+ 'kernels_subdir': 'kernels',
+ 'firewall_conf': 'firewall.xml',
+ 'whitelisted_appmenus': 'whitelisted-appmenus.list',
+ 'updates_stat_file': 'updates.stat',
+}
+
+defaults = {
+ 'libvirt_uri': 'xen:///',
+ 'memory': 400,
+ 'hvm_memory': 512,
+ 'kernelopts': "nopat",
+ 'kernelopts_pcidevs': "nopat iommu=soft swiotlb=8192",
+
+ 'dom0_update_check_interval': 6*3600,
+
+ 'private_img_size': 2*1024*1024*1024,
+ 'root_img_size': 10*1024*1024*1024,
+
+ 'pool_configs': {
+ # create file pool even when the default one is LVM
+ 'varlibqubes': {'dir_path': qubes_base_dir,
+ 'driver': 'file',
+ 'name': 'varlibqubes'},
+ 'linux-kernel': {
+ 'dir_path': os.path.join(qubes_base_dir,
+ system_path['qubes_kernels_base_dir']),
+ 'driver': 'linux-kernel',
+ 'name': 'linux-kernel'
+ }
+ },
+
+ # how long (in sec) to wait for VMs to shutdown,
+ # before killing them (when used qvm-run with --wait option),
+ 'shutdown_counter_max': 60,
+
+ 'vm_default_netmask': "255.255.255.0",
+
+ 'appvm_label': 'red',
+ 'template_label': 'black',
+ 'servicevm_label': 'red',
+}
+
+max_qid = 254
+max_netid = 254
+max_dispid = 10000
+#: built-in standard labels, if creating new one, allocate them above this
+# number, at least until label index is removed from API
+max_default_label = 8
diff --git a/qubes/core2migration.py b/qubes/core2migration.py
new file mode 100644
index 00000000..2286ad1b
--- /dev/null
+++ b/qubes/core2migration.py
@@ -0,0 +1,286 @@
+#
+# The Qubes OS Project, http://www.qubes-os.org
+#
+# Copyright (C) 2016 Marek Marczykowski-Górecki
+#
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
+#
+
+import ast
+import xml.parsers.expat
+
+import lxml.etree
+
+import qubes
+import qubes.devices
+import qubes.vm.appvm
+import qubes.vm.standalonevm
+import qubes.vm.templatevm
+import qubes.vm.adminvm
+import qubes.ext.r3compatibility
+
+
+class AppVM(qubes.vm.appvm.AppVM): # pylint: disable=too-many-ancestors
+ """core2 compatibility AppVM class, with variable dir_path"""
+ dir_path = qubes.property('dir_path',
+ # pylint: disable=undefined-variable
+ default=(lambda self: super(AppVM, self).dir_path),
+ saver=qubes.property.dontsave,
+ doc="VM storage directory",
+ )
+
+ def is_running(self):
+ return False
+
+class StandaloneVM(qubes.vm.standalonevm.StandaloneVM):
+ """core2 compatibility StandaloneVM class, with variable dir_path
+ """ # pylint: disable=too-many-ancestors
+ dir_path = qubes.property('dir_path',
+ # pylint: disable=undefined-variable
+ default=(lambda self: super(StandaloneVM, self).dir_path),
+ saver=qubes.property.dontsave,
+ doc="VM storage directory")
+
+ def is_running(self):
+ return False
+
+
+class Core2Qubes(qubes.Qubes):
+
+ def __init__(self, store=None, load=True, **kwargs):
+ if store is None:
+ raise ValueError("store path required")
+ super(Core2Qubes, self).__init__(store, load, **kwargs)
+
+ def load_default_template(self, element):
+ default_template = element.get("default_template")
+ self.default_template = int(default_template) \
+ if default_template.lower() != "none" else None
+
+ def load_globals(self, element):
+ default_netvm = element.get("default_netvm")
+ if default_netvm is not None:
+ self.default_netvm = int(default_netvm) \
+ if default_netvm != "None" else None
+
+ default_fw_netvm = element.get("default_fw_netvm")
+ if default_fw_netvm is not None:
+ self.default_fw_netvm = int(default_fw_netvm) \
+ if default_fw_netvm != "None" else None
+
+ updatevm = element.get("updatevm")
+ if updatevm is not None:
+ self.updatevm = int(updatevm) \
+ if updatevm != "None" else None
+
+ clockvm = element.get("clockvm")
+ if clockvm is not None:
+ self.clockvm = int(clockvm) \
+ if clockvm != "None" else None
+
+
+ def set_netvm_dependency(self, element):
+ kwargs = {}
+ attr_list = ("qid", "uses_default_netvm", "netvm_qid")
+
+ for attribute in attr_list:
+ kwargs[attribute] = element.get(attribute)
+
+ vm = self.domains[int(kwargs["qid"])]
+
+ if element.get("uses_default_netvm") is None:
+ uses_default_netvm = True
+ else:
+ uses_default_netvm = (
+ True if element.get("uses_default_netvm") == "True" else False)
+ if not uses_default_netvm:
+ netvm_qid = element.get("netvm_qid")
+ if netvm_qid is None or netvm_qid == "none":
+ vm.netvm = None
+ else:
+ vm.netvm = int(netvm_qid)
+
+ def set_dispvm_netvm_dependency(self, element):
+ kwargs = {}
+ attr_list = ("qid", "uses_default_netvm", "netvm_qid")
+
+ for attribute in attr_list:
+ kwargs[attribute] = element.get(attribute)
+
+ vm = self.domains[int(kwargs["qid"])]
+
+ if element.get("uses_default_dispvm_netvm") is None:
+ uses_default_dispvm_netvm = True
+ else:
+ uses_default_dispvm_netvm = (
+ True if element.get("uses_default_dispvm_netvm") == "True"
+ else False)
+ if not uses_default_dispvm_netvm:
+ dispvm_netvm_qid = element.get("dispvm_netvm_qid")
+ if dispvm_netvm_qid is None or dispvm_netvm_qid == "none":
+ dispvm_netvm = None
+ else:
+ dispvm_netvm = self.domains[int(dispvm_netvm_qid)]
+ else:
+ dispvm_netvm = vm.netvm
+
+ if dispvm_netvm:
+ dispvm_tpl_name = 'disp-{}'.format(dispvm_netvm.name)
+ else:
+ dispvm_tpl_name = 'disp-no-netvm'
+
+ if dispvm_tpl_name not in self.domains:
+ vm = self.add_new_vm(qubes.vm.appvm.AppVM,
+ name=dispvm_tpl_name)
+ # TODO: add support for #2075
+ # TODO: set qrexec policy based on dispvm_netvm value
+
+ def import_core2_vm(self, element):
+ vm_class_name = element.tag
+ try:
+ kwargs = {}
+ if vm_class_name in ["QubesTemplateVm", "QubesTemplateHVm"]:
+ vm_class = qubes.vm.templatevm.TemplateVM
+ elif element.get('template_qid').lower() == "none":
+ kwargs['dir_path'] = element.get('dir_path')
+ vm_class = StandaloneVM
+ else:
+ kwargs['dir_path'] = element.get('dir_path')
+ kwargs['template'] = self.domains[int(element.get(
+ 'template_qid'))]
+ vm_class = AppVM
+ # simple attributes
+ for attr, default in {
+ 'installed_by_rpm': 'False',
+ 'include_in_backups': 'True',
+ 'qrexec_timeout': '60',
+ 'internal': 'False',
+ 'label': None,
+ 'name': None,
+ 'vcpus': '2',
+ 'memory': '400',
+ 'maxmem': '4000',
+ 'default_user': 'user',
+ 'debug': 'False',
+ 'pci_strictreset': 'True',
+ 'mac': None,
+ 'autostart': 'False'}.items():
+ value = element.get(attr)
+ if value and value != default:
+ kwargs[attr] = value
+ # attributes with default value
+ for attr in ["kernel", "kernelopts"]:
+ value = element.get(attr)
+ if value and value.lower() == "none":
+ value = None
+ value_is_default = element.get(
+ "uses_default_{}".format(attr))
+ if value_is_default and value_is_default.lower() != \
+ "true":
+ kwargs[attr] = value
+ kwargs['hvm'] = "HVm" in vm_class_name
+ kwargs['provides_network'] = \
+ vm_class_name in ('QubesNetVm', 'QubesProxyVm')
+ if vm_class_name == 'QubesNetVm':
+ kwargs['netvm'] = None
+ vm = self.add_new_vm(vm_class,
+ qid=int(element.get('qid')), **kwargs)
+ services = element.get('services')
+ if services:
+ services = ast.literal_eval(services)
+ else:
+ services = {}
+ for service, value in services.items():
+ feature = service
+ for repl_feature, repl_service in \
+ qubes.ext.r3compatibility.\
+ R3Compatibility.features_to_services.\
+ items():
+ if repl_service == service:
+ feature = repl_feature
+ vm.features[feature] = value
+ for attr in ['backup_content', 'backup_path',
+ 'backup_size']:
+ value = element.get(attr)
+ vm.features[attr.replace('_', '-')] = value
+ pcidevs = element.get('pcidevs')
+ if pcidevs:
+ pcidevs = ast.literal_eval(pcidevs)
+ for pcidev in pcidevs:
+ try:
+ dev = self.domains[0].devices['pci'][pcidev]
+ assignment = qubes.devices.DeviceAssignment(
+ backend_domain=dev.backend_domain, ident=dev.ident)
+ vm.devices["pci"].attach(assignment)
+ except qubes.exc.QubesException as e:
+ self.log.error("VM {}: {}".format(vm.name, str(e)))
+ except (ValueError, LookupError) as err:
+ self.log.error("import error ({1}): {2}".format(
+ vm_class_name, err))
+ if 'vm' in locals():
+ del self.domains[vm]
+
+ def load(self, lock=False):
+ fh = self._acquire_lock()
+
+ try:
+ fh.seek(0)
+ tree = lxml.etree.parse(fh)
+ except (EnvironmentError, # pylint: disable=broad-except
+ xml.parsers.expat.ExpatError) as err:
+ self.log.error(err)
+ return False
+
+ self.load_initial_values()
+
+ self.default_kernel = tree.getroot().get("default_kernel")
+
+ vm_classes = ["TemplateVm", "TemplateHVm",
+ "AppVm", "HVm", "NetVm", "ProxyVm"]
+ # First load templates
+ for vm_class_name in ["TemplateVm", "TemplateHVm"]:
+ vms_of_class = tree.findall("Qubes" + vm_class_name)
+ for element in vms_of_class:
+ self.import_core2_vm(element)
+
+ # Then set default template ...
+ self.load_default_template(tree.getroot())
+
+ # ... and load other VMs
+ for vm_class_name in ["AppVm", "HVm", "NetVm", "ProxyVm"]:
+ vms_of_class = tree.findall("Qubes" + vm_class_name)
+ # first non-template based, then template based
+ sorted_vms_of_class = sorted(vms_of_class,
+ key=lambda x: str(x.get('template_qid')).lower() != "none")
+ for element in sorted_vms_of_class:
+ self.import_core2_vm(element)
+
+ # After importing all VMs, set netvm references, in the same order
+ for vm_class_name in vm_classes:
+ for element in tree.findall("Qubes" + vm_class_name):
+ try:
+ self.set_netvm_dependency(element)
+ except (ValueError, LookupError) as err:
+ self.log.error("VM {}: failed to set netvm dependency: {}".
+ format(element.get('name'), err))
+
+ # and load other defaults (default netvm, updatevm etc)
+ self.load_globals(tree.getroot())
+
+ if not lock:
+ self._release_lock()
+
+ def save(self, lock=False):
+ raise NotImplementedError("Saving old qubes.xml not supported")
diff --git a/qubes/devices.py b/qubes/devices.py
new file mode 100644
index 00000000..fdc929c4
--- /dev/null
+++ b/qubes/devices.py
@@ -0,0 +1,388 @@
+#
+# The Qubes OS Project, https://www.qubes-os.org/
+#
+# Copyright (C) 2010-2016 Joanna Rutkowska
+# Copyright (C) 2015-2016 Wojtek Porczyk
+# Copyright (C) 2016 Bahtiar `kalkin-` Gadimov
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+
+'''API for various types of devices.
+
+Main concept is that some domain main
+expose (potentially multiple) devices, which can be attached to other domains.
+Devices can be of different classes (like 'pci', 'usb', etc). Each device
+class is implemented by an extension.
+
+Devices are identified by pair of (backend domain, `ident`), where `ident` is
+:py:class:`str` and can contain only characters from `[a-zA-Z0-9._-]` set.
+
+Such extension should provide:
+ - `qubes.devices` endpoint - a class descendant from
+ :py:class:`qubes.devices.DeviceInfo`, designed to hold device description (
+ including class-specific properties)
+ - handle `device-attach:class` and `device-detach:class` events for
+ performing the attach/detach action; events are fired even when domain isn't
+ running and extension should be prepared for this
+ - handle `device-list:class` event - list devices exposed by particular
+ domain; it should return list of appropriate DeviceInfo objects
+ - handle `device-get:class` event - get one device object exposed by this
+ domain of given identifier
+ - handle `device-list-attached:class` event - list currently attached
+ devices to this domain
+'''
+
+import qubes.utils
+
+class DeviceNotAttached(qubes.exc.QubesException, KeyError):
+ '''Trying to detach not attached device'''
+ pass
+
+class DeviceAlreadyAttached(qubes.exc.QubesException, KeyError):
+ '''Trying to attach already attached device'''
+ pass
+
+
+class DeviceAssignment(object): # pylint: disable=too-few-public-methods
+ ''' Maps a device to a frontend_domain. '''
+
+ def __init__(self, backend_domain, ident, options=None, persistent=False,
+ frontend_domain=None):
+ self.backend_domain = backend_domain
+ self.ident = ident
+ self.options = options or {}
+ self.persistent = persistent
+ self.frontend_domain = frontend_domain
+
+ def __repr__(self):
+ return "[%s]:%s" % (self.backend_domain, self.ident)
+
+ def __hash__(self):
+ return hash((self.backend_domain, self.ident))
+
+ def __eq__(self, other):
+ if not isinstance(self, other.__class__):
+ return NotImplemented
+
+ return self.backend_domain == other.backend_domain \
+ and self.ident == other.ident
+
+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
+
+ This class emits following events on VM object:
+
+ .. event:: device-attach: (device)
+
+ Fired when device is attached to a VM.
+
+ :param device: :py:class:`DeviceInfo` object to be attached
+
+ .. event:: device-pre-attach: (device)
+
+ Fired before device is attached to a VM
+
+ :param device: :py:class:`DeviceInfo` object to be attached
+
+ .. event:: device-detach: (device)
+
+ Fired when device is detached from a VM.
+
+ :param device: :py:class:`DeviceInfo` object to be attached
+
+ .. event:: device-pre-detach: (device)
+
+ Fired before device is detached from a VM
+
+ :param device: :py:class:`DeviceInfo` object to be attached
+
+ .. event:: device-list:
+
+ Fired to get list of devices exposed by a VM. Handlers of this
+ event should return a list of py:class:`DeviceInfo` objects (or
+ appropriate class specific descendant)
+
+ .. event:: device-get: (ident)
+
+ Fired to get a single device, given by the `ident` parameter.
+ Handlers of this event should either return appropriate object of
+ :py:class:`DeviceInfo`, or :py:obj:`None`. Especially should not
+ raise :py:class:`exceptions.KeyError`.
+
+ .. event:: device-list-attached: (persistent)
+
+ Fired to get list of currently attached devices to a VM. Handlers
+ of this event should return list of devices actually attached to
+ a domain, regardless of its settings.
+
+ '''
+
+ def __init__(self, vm, class_):
+ self._vm = vm
+ self._class = class_
+ self._set = PersistentCollection()
+
+ self.devclass = qubes.utils.get_entry_point_one(
+ 'qubes.devices', self._class)
+
+ def attach(self, device_assignment: DeviceAssignment):
+ '''Attach (add) device to domain.
+
+ :param DeviceInfo device: device object
+ '''
+
+ if not device_assignment.frontend_domain:
+ device_assignment.frontend_domain = self._vm
+ else:
+ assert device_assignment.frontend_domain == self._vm, \
+ "Trying to attach DeviceAssignment belonging to other domain"
+
+ if not device_assignment.persistent and self._vm.is_halted():
+ raise qubes.exc.QubesVMNotRunningError(self._vm,
+ "Devices can only be attached non-persistent to a running vm")
+ device = self._device(device_assignment)
+ if device in self.assignments():
+ raise DeviceAlreadyAttached(
+ 'device {!s} of class {} already attached to {!s}'.format(
+ device, self._class, self._vm))
+ self._vm.fire_event_pre('device-pre-attach:'+self._class, device=device)
+ if device_assignment.persistent:
+ self._set.add(device_assignment)
+ self._vm.fire_event('device-attach:' + self._class, device=device)
+
+ def detach(self, device_assignment: DeviceAssignment):
+ '''Detach (remove) device from domain.
+
+ :param DeviceInfo device: device object
+ '''
+
+ if not device_assignment.frontend_domain:
+ device_assignment.frontend_domain = self._vm
+
+ if device_assignment in self._set and not self._vm.is_halted():
+ raise qubes.exc.QubesVMNotHaltedError(self._vm,
+ "Can not remove a persistent attachment from a non halted vm")
+ if device_assignment not in self.assignments():
+ raise DeviceNotAttached(
+ 'device {!s} of class {} not attached to {!s}'.format(
+ device_assignment.ident, self._class, self._vm))
+
+ device = self._device(device_assignment)
+ self._vm.fire_event_pre('device-pre-detach:'+self._class, device=device)
+ if device in self._set:
+ device_assignment.persistent = True
+ self._set.discard(device_assignment)
+
+ self._vm.fire_event('device-detach:' + self._class, device=device)
+
+ def attached(self):
+ '''List devices which are (or may be) attached to this vm '''
+ attached = self._vm.fire_event('device-list-attached:' + self._class)
+ if attached:
+ return [dev for dev, _ in attached]
+
+ return []
+
+ def persistent(self):
+ ''' Devices persistently attached and safe to access before libvirt
+ bootstrap.
+ '''
+ return [self._device(a) for a in self._set]
+
+ def assignments(self, persistent=None):
+ '''List assignments for devices which are (or may be) attached to the
+ vm.
+
+ Devices may be attached persistently (so they are included in
+ :file:`qubes.xml`) or not. Device can also be in :file:`qubes.xml`,
+ but be temporarily detached.
+
+ :param bool persistent: only include devices which are or are not
+ attached persistently.
+ '''
+
+ devices = self._vm.fire_event('device-list-attached:' + self._class,
+ persistent=persistent)
+ result = []
+ for dev, options in devices:
+ if dev in self._set and not persistent:
+ continue
+ elif dev in self._set:
+ result.append(self._set.get(dev))
+ elif dev not in self._set and persistent:
+ continue
+ else:
+ result.append(
+ DeviceAssignment(backend_domain=dev.backend_domain,
+ ident=dev.ident, options=options,
+ frontend_domain=self._vm))
+ if persistent is not False and not result:
+ result.extend(self._set)
+ return result
+
+ def available(self):
+ '''List devices exposed by this vm'''
+ devices = self._vm.fire_event('device-list:' + self._class)
+ return devices
+
+ def _device(self, assignment: DeviceAssignment):
+ ''' Helper method for geting a `qubes.devices.DeviceInfo` object from
+ `qubes.devices.DeviceAssignment`. '''
+
+ return assignment.backend_domain.devices[self._class][assignment.ident]
+
+ def __iter__(self):
+ return iter(self.available())
+
+ def __getitem__(self, ident):
+ '''Get device object with given ident.
+
+ :returns: py:class:`DeviceInfo`
+
+ If domain isn't running, it is impossible to check device validity,
+ so return UnknownDevice object. Also do the same for non-existing
+ devices - otherwise it will be impossible to detach already
+ disconnected device.
+
+ :raises AssertionError: when multiple devices with the same ident are
+ found
+ '''
+ dev = self._vm.fire_event('device-get:' + self._class, ident=ident)
+ if dev:
+ assert len(dev) == 1
+ return dev[0]
+
+ return UnknownDevice(self._vm, ident)
+
+
+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):
+ self[key] = DeviceCollection(self._vm, key)
+ return self[key]
+
+
+class DeviceInfo(object):
+ ''' Holds all information about a device '''
+ # pylint: disable=too-few-public-methods
+ def __init__(self, backend_domain, ident, description=None,
+ frontend_domain=None, options=None, **kwargs):
+ #: domain providing this device
+ self.backend_domain = backend_domain
+ #: device identifier (unique for given domain and device type)
+ self.ident = ident
+ # allow redefining those as dynamic properties in subclasses
+ try:
+ #: human readable description/name of the device
+ self.description = description
+ except AttributeError:
+ pass
+ try:
+ #: (running) domain to which device is currently attached
+ self.frontend_domain = frontend_domain
+ except AttributeError:
+ pass
+ self.options = options or dict()
+ self.data = kwargs
+
+ if hasattr(self, 'regex'):
+ # pylint: disable=no-member
+ dev_match = self.regex.match(ident)
+ if not dev_match:
+ raise ValueError('Invalid device identifier: {!r}'.format(
+ ident))
+
+ for group in self.regex.groupindex:
+ setattr(self, group, dev_match.group(group))
+
+ def __hash__(self):
+ return hash(self.ident)
+
+ def __eq__(self, other):
+ return (
+ self.backend_domain == other.backend_domain and
+ self.ident == other.ident
+ )
+
+ def __str__(self):
+ return '{!s}:{!s}'.format(self.backend_domain, self.ident)
+
+class UnknownDevice(DeviceInfo):
+ # pylint: disable=too-few-public-methods
+ '''Unknown device - for example exposed by domain not running currently'''
+
+ def __init__(self, backend_domain, ident, description=None,
+ frontend_domain=None, **kwargs):
+ if description is None:
+ description = "Unknown device"
+ super(UnknownDevice, self).__init__(backend_domain, ident, description,
+ frontend_domain, **kwargs)
+
+
+class PersistentCollection(object):
+
+ ''' Helper object managing persistent `DeviceAssignment`s.
+ '''
+
+ def __init__(self):
+ self._dict = {}
+
+ def add(self, assignment: DeviceAssignment):
+ ''' Add assignment to collection '''
+ assert assignment.persistent and assignment.frontend_domain
+ vm = assignment.backend_domain
+ ident = assignment.ident
+ key = (vm, ident)
+ assert key not in self._dict
+
+ self._dict[key] = assignment
+
+ def discard(self, assignment):
+ ''' Discard assignment from collection '''
+ assert assignment.persistent and assignment.frontend_domain
+ vm = assignment.backend_domain
+ ident = assignment.ident
+ key = (vm, ident)
+ if key not in self._dict:
+ raise KeyError
+ del self._dict[key]
+
+ def __contains__(self, device) -> bool:
+ return (device.backend_domain, device.ident) in self._dict
+
+ def get(self, device: DeviceInfo) -> DeviceAssignment:
+ ''' Returns the corresponding `qubes.devices.DeviceAssignment` for the
+ device. '''
+ return self._dict[(device.backend_domain, device.ident)]
+
+ def __iter__(self):
+ return self._dict.values().__iter__()
+
+ def __len__(self) -> int:
+ return len(self._dict.keys())
diff --git a/qubes/dochelpers.py b/qubes/dochelpers.py
new file mode 100644
index 00000000..45ef93c4
--- /dev/null
+++ b/qubes/dochelpers.py
@@ -0,0 +1,450 @@
+#
+# The Qubes OS Project, https://www.qubes-os.org/
+#
+# Copyright (C) 2010-2015 Joanna Rutkowska
+# Copyright (C) 2014-2015 Wojtek Porczyk
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+
+'''Documentation helpers.
+
+This module contains classes and functions which help to maintain documentation,
+particularly our custom Sphinx extension.
+'''
+
+import argparse
+import io
+import json
+import os
+import re
+import urllib.error
+import urllib.request
+
+import docutils
+import docutils.nodes
+import docutils.parsers.rst
+import docutils.parsers.rst.roles
+import docutils.statemachine
+import sphinx
+import sphinx.errors
+import sphinx.locale
+import sphinx.util.docfields
+
+import qubes.tools
+
+SUBCOMMANDS_TITLE = 'COMMANDS'
+OPTIONS_TITLE = 'OPTIONS'
+
+
+class GithubTicket(object):
+ # pylint: disable=too-few-public-methods
+ def __init__(self, data):
+ self.number = data['number']
+ self.summary = data['title']
+ self.uri = data['html_url']
+
+def fetch_ticket_info(app, number):
+ '''Fetch info about particular trac ticket given
+
+ :param app: Sphinx app object
+ :param str number: number of the ticket, without #
+ :rtype: mapping
+ :raises: urllib.error.HTTPError
+ '''
+
+ response = urllib.request.urlopen(urllib.request.Request(
+ app.config.ticket_base_uri.format(number=number),
+ headers={
+ 'Accept': 'application/vnd.github.v3+json',
+ 'User-agent': __name__}))
+ return GithubTicket(json.load(response))
+
+def ticket(name, rawtext, text, lineno, inliner, options=None, content=None):
+ '''Link to qubes ticket
+
+ :param str name: The role name used in the document
+ :param str rawtext: The entire markup snippet, with role
+ :param str text: The text marked with the role
+ :param int lineno: The line number where rawtext appears in the input
+ :param docutils.parsers.rst.states.Inliner inliner: The inliner instance \
+ that called this function
+ :param options: Directive options for customisation
+ :param content: The directive content for customisation
+ ''' # pylint: disable=unused-argument
+
+ if options is None:
+ options = {}
+
+ ticketno = text.lstrip('#')
+ if not ticketno.isdigit():
+ msg = inliner.reporter.error(
+ 'Invalid ticket identificator: {!r}'.format(text), line=lineno)
+ prb = inliner.problematic(rawtext, rawtext, msg)
+ return [prb], [msg]
+
+ try:
+ info = fetch_ticket_info(inliner.document.settings.env.app, ticketno)
+ except urllib.error.HTTPError as e:
+ msg = inliner.reporter.error(
+ 'Error while fetching ticket info: {!s}'.format(e), line=lineno)
+ prb = inliner.problematic(rawtext, rawtext, msg)
+ return [prb], [msg]
+
+ docutils.parsers.rst.roles.set_classes(options)
+
+ node = docutils.nodes.reference(
+ rawtext,
+ '#{} ({})'.format(info.number, info.summary),
+ refuri=info.uri,
+ **options)
+
+ return [node], []
+
+
+class versioncheck(docutils.nodes.warning):
+ # pylint: disable=invalid-name
+ pass
+
+def visit(self, node):
+ self.visit_admonition(node, 'version')
+
+def depart(self, node):
+ self.depart_admonition(node)
+
+sphinx.locale.admonitionlabels['version'] = 'Version mismatch'
+
+
+class VersionCheck(docutils.parsers.rst.Directive):
+ '''Directive versioncheck
+
+ Check if current version (from ``conf.py``) equals version specified as
+ argument. If not, generate warning.'''
+
+ has_content = True
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = True
+ option_spec = {}
+
+ def run(self):
+ current = self.state.document.settings.env.app.config.version
+ version = self.arguments[0]
+
+ if current == version:
+ return []
+
+ text = ' '.join('''This manual page was written for version **{}**, but
+ current version at the time when this page was generated is **{}**.
+ This may or may not mean that page is outdated or has
+ inconsistencies.'''.format(version, current).split())
+
+ node = versioncheck(text)
+ node['classes'] = ['admonition', 'warning']
+
+ self.state.nested_parse(docutils.statemachine.StringList([text]),
+ self.content_offset, node)
+ return [node]
+
+
+def make_rst_section(heading, char):
+ return '{}\n{}\n\n'.format(heading, char[0] * len(heading))
+
+
+def prepare_manpage(command):
+ parser = qubes.tools.get_parser_for_command(command)
+ stream = io.StringIO()
+ stream.write('.. program:: {}\n\n'.format(command))
+ stream.write(make_rst_section(
+ ':program:`{}` -- {}'.format(command, parser.description), '='))
+ stream.write('''.. warning::
+
+ This page was autogenerated from command-line parser. It shouldn't be 1:1
+ conversion, because it would add little value. Please revise it and add
+ more descriptive help, which normally won't fit in standard ``--help``
+ option.
+
+ After rewrite, please remove this admonition.\n\n''')
+
+ stream.write(make_rst_section('Synopsis', '-'))
+ usage = ' '.join(parser.format_usage().strip().split())
+ if usage.startswith('usage: '):
+ usage = usage[len('usage: '):]
+
+ # replace METAVARS with *METAVARS*
+ usage = re.sub(r'\b([A-Z]{2,})\b', r'*\1*', usage)
+
+ stream.write(':command:`{}` {}\n\n'.format(command, usage))
+
+ stream.write(make_rst_section('Options', '-'))
+
+ for action in parser._actions: # pylint: disable=protected-access
+ stream.write('.. option:: ')
+ if action.metavar:
+ stream.write(', '.join('{}{}{}'.format(
+ option,
+ '=' if option.startswith('--') else ' ',
+ action.metavar)
+ for option in sorted(action.option_strings)))
+ else:
+ stream.write(', '.join(sorted(action.option_strings)))
+ stream.write('\n\n {}\n\n'.format(action.help))
+
+ stream.write(make_rst_section('Authors', '-'))
+ stream.write('''\
+| Joanna Rutkowska
+| Rafal Wojtczuk
+| Marek Marczykowski
+| Wojtek Porczyk
+
+.. vim: ts=3 sw=3 et tw=80
+''')
+
+ return stream.getvalue()
+
+
+class OptionsCheckVisitor(docutils.nodes.SparseNodeVisitor):
+ ''' Checks if the visited option nodes and the specified args are in sync.
+ '''
+ def __init__(self, command, args, document):
+ assert isinstance(args, set)
+ docutils.nodes.SparseNodeVisitor.__init__(self, document)
+ self.command = command
+ self.args = args
+
+ def visit_desc(self, node):
+ ''' Skips all but 'option' elements '''
+ # pylint: disable=no-self-use
+ if not node.get('desctype', None) == 'option':
+ raise docutils.nodes.SkipChildren
+
+
+ def visit_desc_name(self, node):
+ ''' Checks if the option is defined `self.args` '''
+ if not isinstance(node[0], docutils.nodes.Text):
+ raise sphinx.errors.SphinxError('first child should be Text')
+
+ arg = str(node[0])
+ try:
+ self.args.remove(arg)
+ except KeyError:
+ raise sphinx.errors.SphinxError(
+ 'No such argument for {!r}: {!r}'.format(self.command, arg))
+
+ def check_undocumented_arguments(self, ignored_options=None):
+ ''' Call this to check if any undocumented arguments are left.
+
+ While the documentation talks about a
+ 'SparseNodeVisitor.depart_document()' function, this function does
+ not exists. (For details see implementation of
+ :py:method:`NodeVisitor.dispatch_departure()`) So we need to
+ manually call this.
+ '''
+ if ignored_options is None:
+ ignored_options = set()
+ left_over_args = self.args - ignored_options
+ if left_over_args:
+ raise sphinx.errors.SphinxError(
+ 'Undocumented arguments for command {!r}: {!r}'.format(
+ self.command, ', '.join(sorted(left_over_args))))
+
+
+class CommandCheckVisitor(docutils.nodes.SparseNodeVisitor):
+ ''' Checks if the visited sub command section nodes and the specified sub
+ command args are in sync.
+ '''
+
+ def __init__(self, command, sub_commands, document):
+ docutils.nodes.SparseNodeVisitor.__init__(self, document)
+ self.command = command
+ self.sub_commands = sub_commands
+
+ def visit_section(self, node):
+ ''' Checks if the visited sub-command section nodes exists and it
+ options are in sync.
+
+ Uses :py:class:`OptionsCheckVisitor` for checking
+ sub-commands options
+ '''
+ # pylint: disable=no-self-use
+ title = str(node[0][0])
+ if title.upper() == SUBCOMMANDS_TITLE:
+ return
+
+ sub_cmd = self.command + ' ' + title
+
+ try:
+ args = self.sub_commands[title]
+ options_visitor = OptionsCheckVisitor(sub_cmd, args, self.document)
+ node.walkabout(options_visitor)
+ options_visitor.check_undocumented_arguments(
+ {'--help', '--quiet', '--verbose', '-h', '-q', '-v'})
+ del self.sub_commands[title]
+ except KeyError:
+ raise sphinx.errors.SphinxError(
+ 'No such sub-command {!r}'.format(sub_cmd))
+
+ def visit_Text(self, node):
+ ''' If the visited text node starts with 'alias: ', all the provided
+ comma separted alias in this node, are removed from
+ `self.sub_commands`
+ '''
+ # pylint: disable=invalid-name
+ text = str(node).strip()
+ if text.startswith('aliases:'):
+ aliases = {a.strip() for a in text.split('aliases:')[1].split(',')}
+ for alias in aliases:
+ assert alias in self.sub_commands
+ del self.sub_commands[alias]
+
+
+ def check_undocumented_sub_commands(self):
+ ''' Call this to check if any undocumented sub_commands are left.
+
+ While the documentation talks about a
+ 'SparseNodeVisitor.depart_document()' function, this function does
+ not exists. (For details see implementation of
+ :py:method:`NodeVisitor.dispatch_departure()`) So we need to
+ manually call this.
+ '''
+ if self.sub_commands:
+ raise sphinx.errors.SphinxError(
+ 'Undocumented commands for {!r}: {!r}'.format(
+ self.command, ', '.join(sorted(self.sub_commands.keys()))))
+
+
+class ManpageCheckVisitor(docutils.nodes.SparseNodeVisitor):
+ ''' Checks if the sub-commands and options specified in the 'COMMAND' and
+ 'OPTIONS' (case insensitve) sections in sync the command parser.
+ '''
+ def __init__(self, app, command, document):
+ docutils.nodes.SparseNodeVisitor.__init__(self, document)
+ try:
+ parser = qubes.tools.get_parser_for_command(command)
+ except ImportError:
+ app.warn('cannot import module for command')
+ self.parser = None
+ return
+ except AttributeError:
+ raise sphinx.errors.SphinxError('cannot find parser in module')
+
+ self.command = command
+ self.parser = parser
+ self.options = set()
+ self.sub_commands = {}
+ self.app = app
+
+ # pylint: disable=protected-access
+ for action in parser._actions:
+ if action.help == argparse.SUPPRESS:
+ continue
+
+ if issubclass(action.__class__,
+ qubes.tools.AliasedSubParsersAction):
+ for cmd, cmd_parser in action._name_parser_map.items():
+ self.sub_commands[cmd] = set()
+ for sub_action in cmd_parser._actions:
+ if sub_action.help != argparse.SUPPRESS:
+ self.sub_commands[cmd].update(
+ sub_action.option_strings)
+ else:
+ self.options.update(action.option_strings)
+
+ def visit_section(self, node):
+ ''' If section title is OPTIONS or COMMANDS dispatch the apropriate
+ `NodeVisitor`.
+ '''
+ if self.parser is None:
+ return
+
+ section_title = str(node[0][0]).upper()
+ if section_title == OPTIONS_TITLE:
+ options_visitor = OptionsCheckVisitor(self.command, self.options,
+ self.document)
+ node.walkabout(options_visitor)
+ options_visitor.check_undocumented_arguments()
+ elif section_title == SUBCOMMANDS_TITLE:
+ sub_cmd_visitor = CommandCheckVisitor(
+ self.command, self.sub_commands, self.document)
+ node.walkabout(sub_cmd_visitor)
+ sub_cmd_visitor.check_undocumented_sub_commands()
+
+def check_man_args(app, doctree, docname):
+ ''' Checks the manpage for undocumented or obsolete sub-commands and
+ options.
+ '''
+ dirname, command = os.path.split(docname)
+ if os.path.basename(dirname) != 'manpages':
+ return
+
+ app.info('Checking arguments for {!r}'.format(command))
+ doctree.walk(ManpageCheckVisitor(app, command, doctree))
+
+
+#
+# this is lifted from sphinx' own conf.py
+#
+
+event_sig_re = re.compile(r'([a-zA-Z-:<>]+)\s*\((.*)\)')
+
+def parse_event(env, sig, signode):
+ # pylint: disable=unused-argument
+ m = event_sig_re.match(sig)
+ if not m:
+ signode += sphinx.addnodes.desc_name(sig, sig)
+ return sig
+ name, args = m.groups()
+ signode += sphinx.addnodes.desc_name(name, name)
+ plist = sphinx.addnodes.desc_parameterlist()
+ for arg in args.split(','):
+ arg = arg.strip()
+ plist += sphinx.addnodes.desc_parameter(arg, arg)
+ signode += plist
+ return name
+
+#
+# end of codelifting
+#
+
+
+def break_to_pdb(app, *dummy):
+ if not app.config.break_to_pdb:
+ return
+ import pdb
+ pdb.set_trace()
+
+
+def setup(app):
+ app.add_role('ticket', ticket)
+ app.add_config_value(
+ 'ticket_base_uri',
+ 'https://api.github.com/repos/QubesOS/qubes-issues/issues/{number}',
+ 'env')
+ app.add_config_value('break_to_pdb', False, 'env')
+ app.add_node(versioncheck,
+ html=(visit, depart),
+ man=(visit, depart))
+ app.add_directive('versioncheck', VersionCheck)
+
+ fdesc = sphinx.util.docfields.GroupedField('parameter', label='Parameters',
+ names=['param'], can_collapse=True)
+ app.add_object_type('event', 'event', 'pair: %s; event', parse_event,
+ doc_field_types=[fdesc])
+
+ app.connect('doctree-resolved', break_to_pdb)
+ app.connect('doctree-resolved', check_man_args)
+
+
+# vim: ts=4 sw=4 et
diff --git a/qubes/events.py b/qubes/events.py
new file mode 100644
index 00000000..ca111876
--- /dev/null
+++ b/qubes/events.py
@@ -0,0 +1,206 @@
+#
+# The Qubes OS Project, https://www.qubes-os.org/
+#
+# Copyright (C) 2014-2015 Joanna Rutkowska
+# Copyright (C) 2014-2015 Wojtek Porczyk
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+
+'''Qubes events.
+
+Events are fired when something happens, like VM start or stop, property change
+etc.
+'''
+
+import collections
+
+import itertools
+
+
+def handler(*events):
+ '''Event handler decorator factory.
+
+ To hook an event, decorate a method in your plugin class with this
+ decorator.
+
+ It probably makes no sense to specify more than one handler for specific
+ event in one class, because handlers are not run concurrently and there is
+ no guarantee of the order of execution.
+
+ .. note::
+ For hooking events from extensions, see :py:func:`qubes.ext.handler`.
+
+ :param str event: event type
+ '''
+
+ def decorator(func):
+ # pylint: disable=missing-docstring
+ func.ha_events = events
+ # mark class own handler (i.e. not from extension)
+ func.ha_bound = True
+ return func
+
+ return decorator
+
+
+def ishandler(obj):
+ '''Test if a method is hooked to an event.
+
+ :param object o: suspected hook
+ :return: :py:obj:`True` when function is a hook, :py:obj:`False` otherwise
+ :rtype: bool
+ '''
+
+ return callable(obj) \
+ and hasattr(obj, 'ha_events')
+
+
+class EmitterMeta(type):
+ '''Metaclass for :py:class:`Emitter`'''
+ def __init__(cls, name, bases, dict_):
+ super(EmitterMeta, cls).__init__(name, bases, dict_)
+ cls.__handlers__ = collections.defaultdict(set)
+
+ try:
+ propnames = set(prop.__name__ for prop in cls.property_list())
+ except AttributeError:
+ propnames = set()
+
+ for attr in dict_:
+ if attr in propnames:
+ # we have to be careful, not to getattr() on properties which
+ # may be unset
+ continue
+
+ attr = dict_[attr]
+ if not ishandler(attr):
+ continue
+
+ for event in attr.ha_events:
+ cls.__handlers__[event].add(attr)
+
+
+class Emitter(object, metaclass=EmitterMeta):
+ '''Subject that can emit events.
+
+ By default all events are disabled not to interfere with loading from XML.
+ To enable event dispatch, set :py:attr:`events_enabled` to :py:obj:`True`.
+ '''
+
+ def __init__(self, *args, **kwargs):
+ super(Emitter, self).__init__(*args, **kwargs)
+ if not hasattr(self, 'events_enabled'):
+ self.events_enabled = False
+ self.__handlers__ = collections.defaultdict(set)
+
+
+ def add_handler(self, event, func):
+ '''Add event handler to subject's class.
+
+ This is class method, it is invalid to call it on object instance.
+
+ :param str event: event identificator
+ :param collections.Callable handler: handler callable
+ '''
+
+ # pylint: disable=no-member
+ self.__handlers__[event].add(func)
+
+ def remove_handler(self, event, func):
+ '''Remove event handler from subject's class.
+
+ This is class method, it is invalid to call it on object instance.
+
+ This method must be called on the same class as
+ :py:meth:`add_handler` was called to register the handler.
+
+ :param str event: event identificator
+ :param collections.Callable handler: handler callable
+ '''
+
+ # pylint: disable=no-member
+ self.__handlers__[event].remove(func)
+
+ def _fire_event_in_order(self, order, event, kwargs):
+ '''Fire event for classes in given order.
+
+ Do not use this method. Use :py:meth:`fire_event` or
+ :py:meth:`fire_event_pre`.
+ '''
+
+ if not self.events_enabled:
+ return []
+
+ effects = []
+ for i in order:
+ try:
+ handlers_dict = i.__handlers__
+ except AttributeError:
+ continue
+ handlers = handlers_dict.get(event, set())
+ if '*' in handlers_dict:
+ handlers = handlers_dict['*'] | handlers
+ for func in sorted(handlers,
+ key=(lambda handler: hasattr(handler, 'ha_bound')),
+ reverse=True):
+ effect = func(self, event, **kwargs)
+ if effect is not None:
+ effects.extend(effect)
+ return effects
+
+ def fire_event(self, event, **kwargs):
+ '''Call all handlers for an event.
+
+ Handlers are called for class and all parent classess, in **reversed**
+ method resolution order. For each class first are called bound handlers
+ (specified in class definition), then handlers from extensions. Aside
+ from above, remaining order is undefined.
+
+ .. seealso::
+ :py:meth:`fire_event_pre`
+
+ :param str event: event identificator
+ :returns: list of effects
+
+ All *kwargs* are passed verbatim. They are different for different
+ events.
+ '''
+
+ return self._fire_event_in_order(
+ itertools.chain(reversed(self.__class__.__mro__), (self,)),
+ event, kwargs)
+
+
+ def fire_event_pre(self, event, **kwargs):
+ '''Call all handlers for an event.
+
+ Handlers are called for class and all parent classess, in **true**
+ method resolution order. This is intended for ``-pre-`` events, where
+ order of invocation should be reversed.
+
+ .. seealso::
+ :py:meth:`fire_event`
+
+ :param str event: event identificator
+ :returns: list of effects
+
+ All *kwargs* are passed verbatim. They are different for different
+ events.
+ '''
+
+ return self._fire_event_in_order(
+ itertools.chain((self,), self.__class__.__mro__),
+ event, kwargs)
diff --git a/qubes/exc.py b/qubes/exc.py
new file mode 100644
index 00000000..ef2e815d
--- /dev/null
+++ b/qubes/exc.py
@@ -0,0 +1,153 @@
+#
+# The Qubes OS Project, https://www.qubes-os.org/
+#
+# Copyright (C) 2015 Joanna Rutkowska
+# Copyright (C) 2015 Wojtek Porczyk
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+
+'''
+Qubes OS exception hierarchy
+'''
+
+class QubesException(Exception):
+ '''Exception that can be shown to the user'''
+ pass
+
+
+class QubesVMNotFoundError(QubesException, KeyError):
+ '''Domain cannot be found in the system'''
+ def __init__(self, vmname):
+ super(QubesVMNotFoundError, self).__init__(
+ 'No such domain: {!r}'.format(vmname))
+ self.vmname = vmname
+
+
+class QubesVMError(QubesException):
+ '''Some problem with domain state.'''
+ def __init__(self, vm, msg):
+ super(QubesVMError, self).__init__(msg)
+ self.vm = vm
+
+
+class QubesVMNotStartedError(QubesVMError):
+ '''Domain is not started.
+
+ This exception is thrown when machine is halted, but should be started
+ (that is, either running or paused).
+ '''
+ def __init__(self, vm, msg=None):
+ super(QubesVMNotStartedError, self).__init__(vm,
+ msg or 'Domain is powered off: {!r}'.format(vm.name))
+
+
+class QubesVMNotRunningError(QubesVMNotStartedError):
+ '''Domain is not running.
+
+ This exception is thrown when machine should be running but is either
+ halted or paused.
+ '''
+ def __init__(self, vm, msg=None):
+ super(QubesVMNotRunningError, self).__init__(vm,
+ msg or 'Domain not running (either powered off or paused): {!r}' \
+ .format(vm.name))
+
+
+class QubesVMNotPausedError(QubesVMNotStartedError):
+ '''Domain is not paused.
+
+ This exception is thrown when machine should be paused, but is not.
+ '''
+ def __init__(self, vm, msg=None):
+ super(QubesVMNotPausedError, self).__init__(vm,
+ msg or 'Domain is not paused: {!r}'.format(vm.name))
+
+
+class QubesVMNotSuspendedError(QubesVMError):
+ '''Domain is not suspended.
+
+ This exception is thrown when machine should be suspended but is either
+ halted or running.
+ '''
+ def __init__(self, vm, msg=None):
+ super(QubesVMNotSuspendedError, self).__init__(vm,
+ msg or 'Domain is not suspended: {!r}'.format(vm.name))
+
+
+class QubesVMNotHaltedError(QubesVMError):
+ '''Domain is not halted.
+
+ This exception is thrown when machine should be halted, but is not (either
+ running or paused).
+ '''
+ def __init__(self, vm, msg=None):
+ super(QubesVMNotHaltedError, self).__init__(vm,
+ msg or 'Domain is not powered off: {!r}'.format(vm.name))
+
+
+class QubesNoTemplateError(QubesVMError):
+ '''Cannot start domain, because there is no template'''
+ def __init__(self, vm, msg=None):
+ super(QubesNoTemplateError, self).__init__(
+ msg or 'Template for the domain {!r} not found'.format(vm.name))
+
+
+class QubesValueError(QubesException, ValueError):
+ '''Cannot set some value, because it is invalid, out of bounds, etc.'''
+ pass
+
+
+class QubesPropertyValueError(QubesValueError):
+ '''Cannot set value of qubes.property, because user-supplied value is wrong.
+ '''
+ def __init__(self, holder, prop, value, msg=None):
+ super(QubesPropertyValueError, self).__init__(
+ msg or 'Invalid value {!r} for property {!r} of {!r}'.format(
+ value, prop.__name__, holder))
+ self.holder = holder
+ self.prop = prop
+ self.value = value
+
+
+class QubesNotImplementedError(QubesException, NotImplementedError):
+ '''Thrown at user when some feature is not implemented'''
+ def __init__(self, msg=None):
+ super(QubesNotImplementedError, self).__init__(
+ msg or 'This feature is not available')
+
+
+class BackupCancelledError(QubesException):
+ '''Thrown at user when backup was manually cancelled'''
+ def __init__(self, msg=None):
+ super(BackupCancelledError, self).__init__(
+ msg or 'Backup cancelled')
+
+
+class QubesMemoryError(QubesException, MemoryError):
+ '''Cannot start domain, because not enough memory is available'''
+ def __init__(self, vm, msg=None):
+ super(QubesMemoryError, self).__init__(
+ msg or 'Not enough memory to start domain {!r}'.format(vm.name))
+ self.vm = vm
+
+
+class QubesFeatureNotFoundError(QubesException, KeyError):
+ '''Feature not set for a given domain'''
+ def __init__(self, domain, feature):
+ super(QubesFeatureNotFoundError, self).__init__(
+ 'Feature not set for domain {}: {}'.format(domain, feature))
+ self.feature = feature
+ self.vm = domain
diff --git a/qubes/ext/__init__.py b/qubes/ext/__init__.py
new file mode 100644
index 00000000..f9bcd81a
--- /dev/null
+++ b/qubes/ext/__init__.py
@@ -0,0 +1,91 @@
+#
+# The Qubes OS Project, https://www.qubes-os.org/
+#
+# Copyright (C) 2014-2015 Joanna Rutkowska
+# Copyright (C) 2014-2015 Wojtek Porczyk