commit
8736f738ca
9
.travis.yml
Normal file
9
.travis.yml
Normal file
@ -0,0 +1,9 @@
|
||||
sudo: required
|
||||
dist: trusty
|
||||
language: generic
|
||||
install: git clone https://github.com/QubesOS/qubes-builder ~/qubes-builder
|
||||
# debootstrap in trusty is old...
|
||||
before_script: sudo ln -s sid /usr/share/debootstrap/scripts/stretch
|
||||
script: ~/qubes-builder/scripts/travis-build
|
||||
env:
|
||||
- DIST_DOM0=fc23 USE_QUBES_REPO_VERSION=3.2 USE_QUBES_REPO_TESTING=1
|
||||
3
Makefile
3
Makefile
@ -72,12 +72,15 @@ endif
|
||||
mkdir -p $(DESTDIR)/usr/libexec/qubes
|
||||
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
|
||||
cp qubes-rpc-policy/qubes.VMShell.policy $(DESTDIR)/etc/qubes-rpc/policy/qubes.VMShell
|
||||
cp qubes-rpc-policy/qubes.NotifyUpdates.policy $(DESTDIR)/etc/qubes-rpc/policy/qubes.NotifyUpdates
|
||||
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/qubes.GetRandomizedTime $(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)"
|
||||
|
||||
@ -26,6 +26,7 @@ import datetime
|
||||
import base64
|
||||
import hashlib
|
||||
import logging
|
||||
import grp
|
||||
import lxml.etree
|
||||
import os
|
||||
import os.path
|
||||
@ -37,15 +38,16 @@ import time
|
||||
import uuid
|
||||
import xml.parsers.expat
|
||||
import signal
|
||||
import pwd
|
||||
from qubes import qmemman
|
||||
from qubes import qmemman_algo
|
||||
import libvirt
|
||||
import warnings
|
||||
|
||||
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:
|
||||
@ -109,6 +111,7 @@ class QubesVm(object):
|
||||
"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"),
|
||||
@ -133,9 +136,10 @@ class QubesVm(object):
|
||||
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": None },
|
||||
"vcpus": { "default": 2 },
|
||||
"uses_default_kernel": { "default": True, 'order': 30 },
|
||||
"uses_default_kernelopts": { "default": True, 'order': 30 },
|
||||
"kernel": {
|
||||
@ -198,7 +202,8 @@ class QubesVm(object):
|
||||
'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' ]:
|
||||
'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']:
|
||||
@ -326,11 +331,6 @@ class QubesVm(object):
|
||||
if self.maxmem > self.memory * 10:
|
||||
self.maxmem = self.memory * 10
|
||||
|
||||
# By default allow use all VCPUs
|
||||
if self.vcpus is None and not vmm.offline_mode:
|
||||
qubes_host = QubesHost()
|
||||
self.vcpus = qubes_host.no_cpus
|
||||
|
||||
# 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)
|
||||
@ -345,7 +345,11 @@ class QubesVm(object):
|
||||
self.services['qubes-update-check'] = False
|
||||
|
||||
# Initialize VM image storage class
|
||||
self.storage = defaults["storage_class"](self)
|
||||
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")
|
||||
@ -561,6 +565,10 @@ class QubesVm(object):
|
||||
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
|
||||
return re.match(r"^[a-zA-Z][a-zA-Z0-9_.-]*$", name) is not None
|
||||
|
||||
def pre_rename(self, new_name):
|
||||
@ -582,9 +590,18 @@ class QubesVm(object):
|
||||
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)
|
||||
if self.libvirt_domain:
|
||||
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
|
||||
@ -714,6 +731,8 @@ class QubesVm(object):
|
||||
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
|
||||
|
||||
|
||||
@ -766,9 +785,13 @@ class QubesVm(object):
|
||||
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_INTERNAL_ERROR:
|
||||
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):
|
||||
@ -783,9 +806,13 @@ class QubesVm(object):
|
||||
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_INTERNAL_ERROR:
|
||||
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):
|
||||
@ -827,6 +854,8 @@ class QubesVm(object):
|
||||
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):
|
||||
@ -901,7 +930,14 @@ class QubesVm(object):
|
||||
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):
|
||||
@ -913,7 +949,14 @@ class QubesVm(object):
|
||||
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):
|
||||
@ -924,7 +967,7 @@ class QubesVm(object):
|
||||
uuid = self.uuid
|
||||
|
||||
start_time = vmm.xs.read('', "/vm/%s/start_time" % str(uuid))
|
||||
if start_time != '':
|
||||
if start_time:
|
||||
return datetime.datetime.fromtimestamp(float(start_time))
|
||||
else:
|
||||
return None
|
||||
@ -1061,6 +1104,7 @@ class QubesVm(object):
|
||||
|
||||
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)
|
||||
@ -1069,6 +1113,7 @@ class QubesVm(object):
|
||||
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()
|
||||
@ -1148,6 +1193,7 @@ class QubesVm(object):
|
||||
# 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
|
||||
@ -1156,8 +1202,10 @@ class QubesVm(object):
|
||||
args['dns2'] = self.secondary_dns
|
||||
args['netmask'] = self.netmask
|
||||
args['netdev'] = self._format_net_dev(self.ip, self.mac, self.netvm.name)
|
||||
args['disable_network1'] = '';
|
||||
args['disable_network2'] = '';
|
||||
args['network_begin'] = ''
|
||||
args['network_end'] = ''
|
||||
args['no_network_begin'] = '<!--'
|
||||
args['no_network_end'] = '-->'
|
||||
else:
|
||||
args['ip'] = ''
|
||||
args['mac'] = ''
|
||||
@ -1166,8 +1214,12 @@ class QubesVm(object):
|
||||
args['dns2'] = ''
|
||||
args['netmask'] = ''
|
||||
args['netdev'] = ''
|
||||
args['disable_network1'] = '<!--';
|
||||
args['disable_network2'] = '-->';
|
||||
args['network_begin'] = '<!--'
|
||||
args['network_end'] = '-->'
|
||||
args['no_network_begin'] = ''
|
||||
args['no_network_end'] = ''
|
||||
if len(self.pcidevs) and self.pci_e820_host:
|
||||
args['features'] = '<xen><e820_host state=\'on\'/></xen>'
|
||||
args.update(self.storage.get_config_params())
|
||||
if hasattr(self, 'kernelopts'):
|
||||
args['kernelopts'] = self.kernelopts
|
||||
@ -1262,9 +1314,10 @@ class QubesVm(object):
|
||||
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']
|
||||
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:
|
||||
@ -1357,8 +1410,16 @@ class QubesVm(object):
|
||||
# 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):
|
||||
@ -1623,17 +1684,22 @@ class QubesVm(object):
|
||||
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:
|
||||
return self.run("QUBESRPC %s %s" % (service, source),
|
||||
localcmd="echo %s" % input, user=user, wait=wait,
|
||||
gui=gui)
|
||||
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)
|
||||
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))
|
||||
@ -1695,7 +1761,15 @@ class QubesVm(object):
|
||||
if verbose:
|
||||
print >> sys.stderr, "--> Starting Qubes GUId..."
|
||||
|
||||
guid_cmd = [system_path["qubes_guid_path"],
|
||||
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,
|
||||
@ -1706,6 +1780,33 @@ class QubesVm(object):
|
||||
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', 'KDE_SESSION_VERSION']) == \
|
||||
'KDE_SESSION_VERSION = 5\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!")
|
||||
@ -1734,13 +1835,21 @@ class QubesVm(object):
|
||||
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")
|
||||
qrexec_env = os.environ
|
||||
qrexec_env['QREXEC_STARTUP_TIMEOUT'] = str(self.qrexec_timeout)
|
||||
retcode = subprocess.call ([system_path["qrexec_daemon_path"]] +
|
||||
qrexec_args, env=qrexec_env)
|
||||
retcode = subprocess.call(qrexec + qrexec_args)
|
||||
if (retcode != 0) :
|
||||
raise OSError ("Cannot execute qrexec-daemon!")
|
||||
|
||||
@ -1769,10 +1878,19 @@ class QubesVm(object):
|
||||
# force connection to a new daemon
|
||||
self._qdb_connection = None
|
||||
|
||||
retcode = subprocess.call ([
|
||||
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])
|
||||
self.name]
|
||||
|
||||
retcode = subprocess.call (qubesdb_cmd)
|
||||
if retcode != 0:
|
||||
raise OSError("ERROR: Cannot execute qubesdb-daemon!")
|
||||
|
||||
|
||||
@ -31,11 +31,12 @@ from qubes.qubes import (
|
||||
QubesException,
|
||||
QubesVm,
|
||||
)
|
||||
from time import sleep
|
||||
|
||||
|
||||
class QubesResizableVm(QubesVm):
|
||||
|
||||
def resize_root_img(self, size):
|
||||
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 "
|
||||
@ -56,12 +57,20 @@ class QubesResizableVm(QubesVm):
|
||||
|
||||
class QubesResizableVmWithResize2fs(QubesResizableVm):
|
||||
|
||||
def resize_root_img(self, size):
|
||||
super(QubesResizableVmWithResize2fs, self).resize_root_img(size)
|
||||
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("VM start required to complete the "
|
||||
"operation, but not allowed. Either run the "
|
||||
"operation again allowing VM start this "
|
||||
"time, or run resize2fs in the VM manually.")
|
||||
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)
|
||||
|
||||
@ -42,6 +42,7 @@ class QubesNetVm(QubesVm):
|
||||
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
|
||||
|
||||
|
||||
@ -168,8 +168,13 @@ class QubesDisposableVm(QubesVm):
|
||||
if self.get_power_state() != "Halted":
|
||||
raise QubesException ("VM is already running!")
|
||||
|
||||
# skip netvm state checking - calling VM have the same netvm, so it
|
||||
# must be 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)
|
||||
@ -232,5 +237,9 @@ class QubesDisposableVm(QubesVm):
|
||||
|
||||
return self.xid
|
||||
|
||||
def remove_from_disk(self):
|
||||
# nothing to remove
|
||||
pass
|
||||
|
||||
# register classes
|
||||
register_qubes_vm_class(QubesDisposableVm)
|
||||
|
||||
@ -100,8 +100,6 @@ class QubesHVm(QubesResizableVm):
|
||||
(not 'xml_element' in kwargs or kwargs['xml_element'].get('guiagent_installed') is None):
|
||||
self.services['meminfo-writer'] = False
|
||||
|
||||
self.storage.rootcow_img = None
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return "HVM"
|
||||
@ -314,7 +312,16 @@ class QubesHVm(QubesResizableVm):
|
||||
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
|
||||
@ -352,25 +359,28 @@ class QubesHVm(QubesResizableVm):
|
||||
if (retcode != 0) :
|
||||
raise QubesException("Cannot start qubes-guid!")
|
||||
|
||||
def start_guid(self, verbose = True, notify_function = None,
|
||||
before_qrexec=False, **kwargs):
|
||||
# If user force the guiagent, start_guid will mimic a standard QubesVM
|
||||
if not before_qrexec and self.guiagent_installed:
|
||||
kwargs['extra_guid_args'] = kwargs.get('extra_guid_args', []) + \
|
||||
['-Q']
|
||||
super(QubesHVm, self).start_guid(verbose, notify_function, **kwargs)
|
||||
stubdom_guid_pidfile = '/var/run/qubes/guid-running.%d' % self.stubdom_xid
|
||||
if os.path.exists(stubdom_guid_pidfile) and not self.debug:
|
||||
try:
|
||||
stubdom_guid_pid = int(open(stubdom_guid_pidfile, 'r').read())
|
||||
os.kill(stubdom_guid_pid, signal.SIGTERM)
|
||||
except Exception as ex:
|
||||
print >> sys.stderr, "WARNING: Failed to kill stubdom gui daemon: %s" % str(ex)
|
||||
elif before_qrexec and (not self.guiagent_installed or self.debug):
|
||||
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):
|
||||
|
||||
@ -29,7 +29,7 @@ import stat
|
||||
import sys
|
||||
import re
|
||||
|
||||
from qubes.qubes import QubesHVm,register_qubes_vm_class,dry_run
|
||||
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
|
||||
|
||||
@ -70,6 +70,7 @@ class QubesTemplateHVm(QubesHVm):
|
||||
def is_appvm(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def rootcow_img(self):
|
||||
return self.storage.rootcow_img
|
||||
|
||||
@ -95,7 +96,15 @@ class QubesTemplateHVm(QubesHVm):
|
||||
def commit_changes (self, verbose = False):
|
||||
self.log.debug('commit_changes()')
|
||||
|
||||
# nothing to do as long as root-cow.img is unused
|
||||
pass
|
||||
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)
|
||||
|
||||
161
core/backup.py
161
core/backup.py
@ -357,8 +357,8 @@ def backup_prepare(vms_list=None, exclude_list=None,
|
||||
|
||||
vms_not_for_backup = [vm.name for vm in qvm_collection.values()
|
||||
if not vm.backup_content]
|
||||
print_callback("VMs not selected for backup: %s" % " ".join(
|
||||
vms_not_for_backup))
|
||||
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.")
|
||||
@ -400,6 +400,10 @@ class SendWorker(Process):
|
||||
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(
|
||||
@ -445,9 +449,21 @@ def prepare_backup_header(target_directory, passphrase, compressed=False,
|
||||
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):
|
||||
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:
|
||||
@ -495,7 +511,7 @@ def backup_do(base_backup_dir, files_to_backup, passphrase,
|
||||
progress = blocks_backedup * 11 / total_backup_sz
|
||||
progress_callback(progress)
|
||||
|
||||
backup_tmpdir = tempfile.mkdtemp(prefix="/var/tmp/backup_")
|
||||
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
|
||||
@ -552,15 +568,16 @@ def backup_do(base_backup_dir, files_to_backup, passphrase,
|
||||
# be verified before untaring this.
|
||||
# Prefix the path in archive with filename["subdir"] to have it
|
||||
# verified during untar
|
||||
tar_cmdline = ["tar", "-Pc", '--sparse',
|
||||
tar_cmdline = (["tar", "-Pc", '--sparse',
|
||||
"-f", backup_pipe,
|
||||
'-C', os.path.dirname(filename["path"]),
|
||||
'--dereference',
|
||||
'--xform', 's:^%s:%s\\0:' % (
|
||||
'-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)
|
||||
@ -650,7 +667,9 @@ def backup_do(base_backup_dir, files_to_backup, passphrase,
|
||||
run_error)
|
||||
|
||||
# Send the chunk to the backup target
|
||||
to_send.put(os.path.relpath(chunkfile, backup_tmpdir))
|
||||
queue_put_with_check(
|
||||
send_proc, vmproc, to_send,
|
||||
os.path.relpath(chunkfile, backup_tmpdir))
|
||||
|
||||
# Close HMAC
|
||||
hmac.stdin.close()
|
||||
@ -668,7 +687,9 @@ def backup_do(base_backup_dir, files_to_backup, passphrase,
|
||||
hmac_file.close()
|
||||
|
||||
# Send the HMAC to the backup target
|
||||
to_send.put(os.path.relpath(chunkfile, backup_tmpdir) + ".hmac")
|
||||
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"
|
||||
@ -680,7 +701,7 @@ def backup_do(base_backup_dir, files_to_backup, passphrase,
|
||||
.poll()
|
||||
pipe.close()
|
||||
|
||||
to_send.put("FINISHED")
|
||||
queue_put_with_check(send_proc, vmproc, to_send, "FINISHED")
|
||||
send_proc.join()
|
||||
shutil.rmtree(backup_tmpdir)
|
||||
|
||||
@ -1553,6 +1574,8 @@ def backup_restore_set_defaults(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
|
||||
|
||||
@ -1620,6 +1643,22 @@ def backup_restore_header(source, passphrase,
|
||||
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$']
|
||||
@ -1637,7 +1676,16 @@ def restore_info_verify(restore_info, host_collection):
|
||||
vm_info.pop('already-exists', None)
|
||||
if not options['verify-only'] and \
|
||||
host_collection.get_vm_by_name(vm) is not None:
|
||||
vm_info['already-exists'] = True
|
||||
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)
|
||||
@ -1658,7 +1706,11 @@ def restore_info_verify(restore_info, host_collection):
|
||||
|
||||
# check netvm
|
||||
vm_info.pop('missing-netvm', None)
|
||||
if vm_info['netvm']:
|
||||
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)
|
||||
@ -1670,8 +1722,9 @@ def restore_info_verify(restore_info, host_collection):
|
||||
if not (netvm_name in restore_info.keys() and
|
||||
restore_info[netvm_name]['vm'].is_netvm()):
|
||||
if options['use-default-netvm']:
|
||||
vm_info['netvm'] = host_collection \
|
||||
.get_default_netvm().name
|
||||
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
|
||||
@ -1684,6 +1737,22 @@ def restore_info_verify(restore_info, host_collection):
|
||||
'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
|
||||
|
||||
|
||||
@ -1956,8 +2025,11 @@ def backup_restore_print_summary(restore_info, print_callback=print_stdout):
|
||||
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!"
|
||||
elif 'orig-template' in vm_info:
|
||||
s += " <-- Original template was '%s'" % (vm_info['orig-template'])
|
||||
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)
|
||||
|
||||
@ -2105,33 +2177,35 @@ def backup_restore_do(restore_info,
|
||||
error_callback("Skipping...")
|
||||
continue
|
||||
|
||||
if os.path.exists(vm.dir_path):
|
||||
move_to_path = tempfile.mkdtemp('', os.path.basename(
|
||||
vm.dir_path), os.path.dirname(vm.dir_path))
|
||||
try:
|
||||
os.rename(vm.dir_path, move_to_path)
|
||||
error_callback("*** Directory {} already exists! It has "
|
||||
"been moved to {}".format(vm.dir_path,
|
||||
move_to_path))
|
||||
except OSError:
|
||||
error_callback("*** Directory {} already exists and "
|
||||
"cannot be moved!".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,
|
||||
conf_file=vm.conf_file,
|
||||
dir_path=vm.dir_path,
|
||||
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,
|
||||
@ -2166,6 +2240,13 @@ def backup_restore_do(restore_info,
|
||||
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:
|
||||
@ -2175,7 +2256,11 @@ def backup_restore_do(restore_info,
|
||||
|
||||
# Set network dependencies - only non-default netvm setting
|
||||
for vm in vms.values():
|
||||
host_vm = host_collection.get_vm_by_name(vm.name)
|
||||
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
|
||||
@ -2231,6 +2316,10 @@ def backup_restore_do(restore_info,
|
||||
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:
|
||||
|
||||
@ -224,7 +224,7 @@ class QubesHost(object):
|
||||
cputime = vm.get_cputime()
|
||||
previous[vm.xid] = {}
|
||||
previous[vm.xid]['cpu_time'] = (
|
||||
cputime / vm.vcpus)
|
||||
cputime / max(vm.vcpus, 1))
|
||||
previous[vm.xid]['cpu_usage'] = 0
|
||||
time.sleep(wait_time)
|
||||
|
||||
@ -651,6 +651,8 @@ class QubesVmCollection(dict):
|
||||
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()')
|
||||
@ -711,18 +713,13 @@ class QubesVmCollection(dict):
|
||||
|
||||
def set_netvm_dependency(self, element):
|
||||
kwargs = {}
|
||||
attr_list = ("qid", "uses_default_netvm", "netvm_qid")
|
||||
attr_list = ("qid", "netvm_qid")
|
||||
|
||||
for attribute in attr_list:
|
||||
kwargs[attribute] = element.get(attribute)
|
||||
|
||||
vm = self[int(kwargs["qid"])]
|
||||
|
||||
if "uses_default_netvm" not in kwargs:
|
||||
vm.uses_default_netvm = True
|
||||
else:
|
||||
vm.uses_default_netvm = (
|
||||
True if kwargs["uses_default_netvm"] == "True" else False)
|
||||
if vm.uses_default_netvm is True:
|
||||
if vm.is_proxyvm():
|
||||
netvm = self.get_default_fw_netvm()
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import string
|
||||
import errno
|
||||
from lxml import etree
|
||||
from lxml.etree import ElementTree, SubElement, Element
|
||||
|
||||
@ -50,6 +51,9 @@ BLKSIZE = 512
|
||||
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)
|
||||
@ -420,6 +424,8 @@ def block_attach(qvmc, vm, device, frontend=None, mode="w", auto_detach=False, w
|
||||
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
|
||||
@ -463,263 +469,205 @@ def block_detach_all(vm):
|
||||
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_setup(backend_vm_xid, vm_xid, devid, usb_ver):
|
||||
"""
|
||||
Attach frontend to the backend.
|
||||
backend_vm_xid - id of the backend domain
|
||||
vm_xid - id of the frontend domain
|
||||
devid - id of the pvusb controller
|
||||
"""
|
||||
num_ports = 8
|
||||
trans = vmm.xs.transaction_start()
|
||||
|
||||
be_path = "/local/domain/%d/backend/vusb/%d/%d" % (backend_vm_xid, vm_xid, devid)
|
||||
fe_path = "/local/domain/%d/device/vusb/%d" % (vm_xid, devid)
|
||||
|
||||
be_perm = [{'dom': backend_vm_xid}, {'dom': vm_xid, 'read': True} ]
|
||||
fe_perm = [{'dom': vm_xid}, {'dom': backend_vm_xid, 'read': True} ]
|
||||
|
||||
# Create directories and set permissions
|
||||
vmm.xs.write(trans, be_path, "")
|
||||
vmm.xs.set_permissions(trans, be_path, be_perm)
|
||||
|
||||
vmm.xs.write(trans, fe_path, "")
|
||||
vmm.xs.set_permissions(trans, fe_path, fe_perm)
|
||||
|
||||
# Write backend information into the location that frontend looks for
|
||||
vmm.xs.write(trans, "%s/backend-id" % fe_path, str(backend_vm_xid))
|
||||
vmm.xs.write(trans, "%s/backend" % fe_path, be_path)
|
||||
|
||||
# Write frontend information into the location that backend looks for
|
||||
vmm.xs.write(trans, "%s/frontend-id" % be_path, str(vm_xid))
|
||||
vmm.xs.write(trans, "%s/frontend" % be_path, fe_path)
|
||||
|
||||
# Write USB Spec version field.
|
||||
vmm.xs.write(trans, "%s/usb-ver" % be_path, usb_ver)
|
||||
|
||||
# Write virtual root hub field.
|
||||
vmm.xs.write(trans, "%s/num-ports" % be_path, str(num_ports))
|
||||
for port in range(1, num_ports+1):
|
||||
# Set all port to disconnected state
|
||||
vmm.xs.write(trans, "%s/port/%d" % (be_path, port), "")
|
||||
|
||||
# Set state to XenbusStateInitialising
|
||||
vmm.xs.write(trans, "%s/state" % fe_path, "1")
|
||||
vmm.xs.write(trans, "%s/state" % be_path, "1")
|
||||
vmm.xs.write(trans, "%s/online" % be_path, "1")
|
||||
|
||||
vmm.xs.transaction_end(trans)
|
||||
|
||||
def usb_decode_device_from_xs(xs_encoded_device):
|
||||
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 xs_encoded_device.replace('_', '.')
|
||||
return qdb_encoded_device.replace('_', '.')
|
||||
|
||||
def usb_encode_device_for_xs(device):
|
||||
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():
|
||||
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 = name of the backend domain
|
||||
xid = xid of the backend domain
|
||||
device = <frontend device number>-<frontend port number>
|
||||
name = <name of backend domain>:<frontend device number>-<frontend port number>
|
||||
vm = backend domain object
|
||||
device = device ID
|
||||
name = <backend-vm>:<device>
|
||||
desc = description
|
||||
"""
|
||||
# FIXME: any better idea of desc_re?
|
||||
desc_re = re.compile(r"^.{1,255}$")
|
||||
if vm is not None:
|
||||
if not vm.is_running():
|
||||
return {}
|
||||
else:
|
||||
vm_list = [vm]
|
||||
else:
|
||||
vm_list = qvmc.values()
|
||||
|
||||
devices_list = {}
|
||||
|
||||
xs_trans = vmm.xs.transaction_start()
|
||||
vm_list = vmm.xs.ls(xs_trans, '/local/domain')
|
||||
|
||||
for xid in vm_list:
|
||||
vm_name = vmm.xs.read(xs_trans, '/local/domain/%s/name' % xid)
|
||||
vm_devices = vmm.xs.ls(xs_trans, '/local/domain/%s/qubes-usb-devices' % xid)
|
||||
if vm_devices is None:
|
||||
continue
|
||||
# when listing devices in xenstore we get encoded names
|
||||
for xs_encoded_device in vm_devices:
|
||||
# Sanitize device id
|
||||
if not usb_device_re.match(xs_encoded_device):
|
||||
print >> sys.stderr, "Invalid device id in backend VM '%s'" % vm_name
|
||||
continue
|
||||
device = usb_decode_device_from_xs(xs_encoded_device)
|
||||
device_desc = vmm.xs.read(xs_trans, '/local/domain/%s/qubes-usb-devices/%s/desc' % (xid, xs_encoded_device))
|
||||
if not desc_re.match(device_desc):
|
||||
print >> sys.stderr, "Invalid %s device desc in VM '%s'" % (device, vm_name)
|
||||
continue
|
||||
visible_name = "%s:%s" % (vm_name, device)
|
||||
# grab version
|
||||
usb_ver = vmm.xs.read(xs_trans, '/local/domain/%s/qubes-usb-devices/%s/usb-ver' % (xid, xs_encoded_device))
|
||||
if usb_ver is None or not usb_ver_re.match(usb_ver):
|
||||
print >> sys.stderr, "Invalid %s device USB version in VM '%s'" % (device, vm_name)
|
||||
continue
|
||||
devices_list[visible_name] = {"name": visible_name, "xid":int(xid),
|
||||
"vm": vm_name, "device":device,
|
||||
"desc":device_desc,
|
||||
"usb_ver":usb_ver}
|
||||
|
||||
vmm.xs.transaction_end(xs_trans)
|
||||
for vm in vm_list:
|
||||
devices_list.update(usb_list_vm(qvmc, vm))
|
||||
return devices_list
|
||||
|
||||
def usb_check_attached(xs_trans, backend_vm, device):
|
||||
"""
|
||||
Checks if the given device in the given backend attached to any frontend.
|
||||
Parameters:
|
||||
backend_vm - xid of the backend domain
|
||||
device - device name in the backend domain
|
||||
Returns None or a dictionary:
|
||||
vm - the name of the frontend domain
|
||||
xid - xid of the frontend domain
|
||||
frontend - frontend device number FIXME
|
||||
devid - frontend port number FIXME
|
||||
"""
|
||||
# sample xs content: /local/domain/0/backend/vusb/4/0/port/1 = "7-5"
|
||||
attached_dev = None
|
||||
vms = vmm.xs.ls(xs_trans, '/local/domain/%d/backend/vusb' % backend_vm)
|
||||
if vms is None:
|
||||
return None
|
||||
for vm in vms:
|
||||
if not vm.isdigit():
|
||||
print >> sys.stderr, "Invalid VM id"
|
||||
continue
|
||||
frontend_devs = vmm.xs.ls(xs_trans, '/local/domain/%d/backend/vusb/%s' % (backend_vm, vm))
|
||||
if frontend_devs is None:
|
||||
continue
|
||||
for frontend_dev in frontend_devs:
|
||||
if not frontend_dev.isdigit():
|
||||
print >> sys.stderr, "Invalid frontend in VM %s" % vm
|
||||
continue
|
||||
ports = vmm.xs.ls(xs_trans, '/local/domain/%d/backend/vusb/%s/%s/port' % (backend_vm, vm, frontend_dev))
|
||||
if ports is None:
|
||||
continue
|
||||
for port in ports:
|
||||
# FIXME: refactor, see similar loop in usb_find_unused_frontend(), use usb_list() instead?
|
||||
if not port.isdigit():
|
||||
print >> sys.stderr, "Invalid port in VM %s frontend %s" % (vm, frontend)
|
||||
continue
|
||||
dev = vmm.xs.read(xs_trans, '/local/domain/%d/backend/vusb/%s/%s/port/%s' % (backend_vm, vm, frontend_dev, port))
|
||||
if dev == "":
|
||||
continue
|
||||
# Sanitize device id
|
||||
if not usb_port_re.match(dev):
|
||||
print >> sys.stderr, "Invalid device id in backend VM %d @ %s/%s/port/%s" % \
|
||||
(backend_vm, vm, frontend_dev, port)
|
||||
continue
|
||||
if dev == device:
|
||||
frontend = "%s-%s" % (frontend_dev, port)
|
||||
#TODO
|
||||
vm_name = xl_ctx.domid_to_name(int(vm))
|
||||
if vm_name is None:
|
||||
# FIXME: should we wipe references to frontends running on nonexistent VMs?
|
||||
continue
|
||||
attached_dev = {"xid":int(vm), "frontend": frontend, "devid": device, "vm": vm_name}
|
||||
break
|
||||
return attached_dev
|
||||
|
||||
#def usb_check_frontend_busy(vm, front_dev, port):
|
||||
# devport = frontend.split("-")
|
||||
# if len(devport) != 2:
|
||||
# raise QubesException("Malformed frontend syntax, must be in device-port format")
|
||||
# # FIXME:
|
||||
# # return vmm.xs.read('', '/local/domain/%d/device/vusb/%d/state' % (vm.xid, frontend)) == '4'
|
||||
# return False
|
||||
|
||||
def usb_find_unused_frontend(xs_trans, backend_vm_xid, vm_xid, usb_ver):
|
||||
"""
|
||||
Find an unused frontend/port to link the given backend with the given frontend.
|
||||
Creates new frontend if needed.
|
||||
Returns frontend specification in <device>-<port> format.
|
||||
"""
|
||||
|
||||
# This variable holds an index of last frontend scanned by the loop below.
|
||||
# If nothing found, this value will be used to derive the index of a new frontend.
|
||||
last_frontend_dev = -1
|
||||
|
||||
frontend_devs = vmm.xs.ls(xs_trans, "/local/domain/%d/device/vusb" % vm_xid)
|
||||
if frontend_devs is not None:
|
||||
for frontend_dev in frontend_devs:
|
||||
if not frontend_dev.isdigit():
|
||||
print >> sys.stderr, "Invalid frontend_dev in VM %d" % vm_xid
|
||||
continue
|
||||
frontend_dev = int(frontend_dev)
|
||||
fe_path = "/local/domain/%d/device/vusb/%d" % (vm_xid, frontend_dev)
|
||||
if vmm.xs.read(xs_trans, "%s/backend-id" % fe_path) == str(backend_vm_xid):
|
||||
if vmm.xs.read(xs_trans, '/local/domain/%d/backend/vusb/%d/%d/usb-ver' % (backend_vm_xid, vm_xid, frontend_dev)) != usb_ver:
|
||||
last_frontend_dev = frontend_dev
|
||||
continue
|
||||
# here: found an existing frontend already connected to right backend using an appropriate USB version
|
||||
ports = vmm.xs.ls(xs_trans, '/local/domain/%d/backend/vusb/%d/%d/port' % (backend_vm_xid, vm_xid, frontend_dev))
|
||||
if ports is None:
|
||||
print >> sys.stderr, "No ports in VM %d frontend_dev %d?" % (vm_xid, frontend_dev)
|
||||
last_frontend_dev = frontend_dev
|
||||
continue
|
||||
for port in ports:
|
||||
# FIXME: refactor, see similar loop in usb_check_attached(), use usb_list() instead?
|
||||
if not port.isdigit():
|
||||
print >> sys.stderr, "Invalid port in VM %d frontend_dev %d" % (vm_xid, frontend_dev)
|
||||
continue
|
||||
port = int(port)
|
||||
dev = vmm.xs.read(xs_trans, '/local/domain/%d/backend/vusb/%d/%s/port/%s' % (backend_vm_xid, vm_xid, frontend_dev, port))
|
||||
# Sanitize device id
|
||||
if not usb_port_re.match(dev):
|
||||
print >> sys.stderr, "Invalid device id in backend VM %d @ %d/%d/port/%d" % \
|
||||
(backend_vm_xid, vm_xid, frontend_dev, port)
|
||||
continue
|
||||
if dev == "":
|
||||
return '%d-%d' % (frontend_dev, port)
|
||||
last_frontend_dev = frontend_dev
|
||||
|
||||
# create a new frontend_dev and link it to the backend
|
||||
frontend_dev = last_frontend_dev + 1
|
||||
usb_setup(backend_vm_xid, vm_xid, frontend_dev, usb_ver)
|
||||
return '%d-%d' % (frontend_dev, 1)
|
||||
|
||||
def usb_attach(vm, backend_vm, device, frontend=None, auto_detach=False, wait=True):
|
||||
device_attach_check(vm, backend_vm, device, frontend)
|
||||
|
||||
xs_trans = vmm.xs.transaction_start()
|
||||
|
||||
xs_encoded_device = usb_encode_device_for_xs(device)
|
||||
usb_ver = vmm.xs.read(xs_trans, '/local/domain/%s/qubes-usb-devices/%s/usb-ver' % (backend_vm.xid, xs_encoded_device))
|
||||
if usb_ver is None or not usb_ver_re.match(usb_ver):
|
||||
vmm.xs.transaction_end(xs_trans)
|
||||
raise QubesException("Invalid %s device USB version in VM '%s'" % (device, backend_vm.name))
|
||||
|
||||
if frontend is None:
|
||||
frontend = usb_find_unused_frontend(xs_trans, backend_vm.xid, vm.xid, usb_ver)
|
||||
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:
|
||||
# Check if any device attached at this frontend
|
||||
#if usb_check_frontend_busy(vm, frontend):
|
||||
# raise QubesException("Frontend %s busy in VM %s, detach it first" % (frontend, vm.name))
|
||||
vmm.xs.transaction_end(xs_trans)
|
||||
raise NotImplementedError("Explicit USB frontend specification is not implemented yet")
|
||||
connected_to = None
|
||||
return connected_to
|
||||
|
||||
# Check if this device is attached to some domain
|
||||
attached_vm = usb_check_attached(xs_trans, backend_vm.xid, device)
|
||||
vmm.xs.transaction_end(xs_trans)
|
||||
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 attached_vm:
|
||||
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(backend_vm, attached_vm)
|
||||
usb_detach(qvmc, device)
|
||||
else:
|
||||
raise QubesException("Device %s from %s already connected to VM %s as %s" % (device, backend_vm.name, attached_vm['vm'], attached_vm['frontend']))
|
||||
raise QubesException("Device {} already connected, to {}".format(
|
||||
device['name'], connected_to
|
||||
))
|
||||
|
||||
# Run helper script
|
||||
xl_cmd = [ '/usr/lib/qubes/xl-qvm-usb-attach.py', str(vm.xid), device, frontend, str(backend_vm.xid) ]
|
||||
subprocess.check_call(xl_cmd)
|
||||
# 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(backend_vm, attachment):
|
||||
xl_cmd = [ '/usr/lib/qubes/xl-qvm-usb-detach.py', str(attachment['xid']), attachment['devid'], attachment['frontend'], str(backend_vm.xid) ]
|
||||
subprocess.check_call(xl_cmd)
|
||||
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))
|
||||
|
||||
def usb_detach_all(vm):
|
||||
raise NotImplementedError("Detaching all devices from a given VM is not implemented yet")
|
||||
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 ######
|
||||
|
||||
@ -760,7 +708,8 @@ class QubesWatch(object):
|
||||
# which can just remove the domain
|
||||
if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
|
||||
pass
|
||||
raise
|
||||
else:
|
||||
raise
|
||||
# and for dom0
|
||||
self._register_watches(None)
|
||||
|
||||
@ -791,6 +740,10 @@ class QubesWatch(object):
|
||||
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:
|
||||
@ -811,6 +764,8 @@ class QubesWatch(object):
|
||||
return
|
||||
else:
|
||||
name = "dom0"
|
||||
if name in self._qdb:
|
||||
return
|
||||
self._qdb[name] = QubesDB(name)
|
||||
try:
|
||||
self._qdb[name].watch('/qubes-block-devices')
|
||||
@ -831,7 +786,10 @@ class QubesWatch(object):
|
||||
self._register_watches(libvirt_domain)
|
||||
|
||||
def _unregister_watches(self, libvirt_domain):
|
||||
name = libvirt_domain.name()
|
||||
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])
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
#!/usr/bin/python2
|
||||
|
||||
from __future__ import absolute_import
|
||||
import _winreg
|
||||
import os
|
||||
import sys
|
||||
|
||||
from qubes.storage.wni import QubesWniVmStorage
|
||||
|
||||
DEFAULT_INSTALLDIR = 'c:\\program files\\Invisible Things Lab\\Qubes WNI'
|
||||
DEFAULT_STOREDIR = 'c:\\qubes'
|
||||
|
||||
def apply(system_path, vm_files, defaults):
|
||||
system_path['qubes_base_dir'] = DEFAULT_STOREDIR
|
||||
installdir = DEFAULT_INSTALLDIR
|
||||
try:
|
||||
reg_key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE,
|
||||
"Software\\Invisible Things Lab\\Qubes WNI")
|
||||
installdir = _winreg.QueryValueEx(reg_key, "InstallDir")[0]
|
||||
system_path['qubes_base_dir'] = \
|
||||
_winreg.QueryValueEx(reg_key, "StoreDir")[0]
|
||||
except WindowsError as e:
|
||||
print >>sys.stderr, \
|
||||
"WARNING: invalid installation: missing registry entries (%s)" \
|
||||
% str(e)
|
||||
|
||||
system_path['config_template_pv'] = \
|
||||
os.path.join(installdir, 'vm-template.xml')
|
||||
system_path['config_template_hvm'] = \
|
||||
os.path.join(installdir, 'vm-template-hvm.xml')
|
||||
system_path['qubes_icon_dir'] = os.path.join(installdir, 'icons')
|
||||
system_path['qubesdb_daemon_path'] = \
|
||||
os.path.join(installdir, 'bin\\qubesdb-daemon.exe')
|
||||
system_path['qrexec_daemon_path'] = \
|
||||
os.path.join(installdir, 'bin\\qrexec-daemon.exe')
|
||||
system_path['qrexec_client_path'] = \
|
||||
os.path.join(installdir, 'bin\\qrexec-client.exe')
|
||||
system_path['qrexec_policy_dir'] = \
|
||||
os.path.join(installdir, 'qubes-rpc\\policy')
|
||||
# Specific to WNI - normally VM have this file
|
||||
system_path['qrexec_agent_path'] = \
|
||||
os.path.join(installdir, 'bin\\qrexec-agent.exe')
|
||||
|
||||
defaults['libvirt_uri'] = 'wni:///'
|
||||
defaults['storage_class'] = QubesWniVmStorage
|
||||
@ -2,7 +2,10 @@
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from qubes.storage.xen import QubesXenVmStorage
|
||||
from qubes.storage.xen import XenStorage, XenPool
|
||||
|
||||
|
||||
def apply(system_path, vm_files, defaults):
|
||||
defaults['storage_class'] = QubesXenVmStorage
|
||||
defaults['storage_class'] = XenStorage
|
||||
defaults['pool_drivers'] = {'xen': XenPool}
|
||||
defaults['pool_config'] = {'dir_path': '/var/lib/qubes/'}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
OS ?= Linux
|
||||
|
||||
SYSCONFDIR ?= /etc
|
||||
PYTHON_QUBESPATH = $(PYTHON_SITEPATH)/qubes
|
||||
|
||||
all:
|
||||
@ -13,6 +14,8 @@ 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 && \
|
||||
|
||||
3
core/storage/README.md
Normal file
3
core/storage/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# WNI File storage
|
||||
Before v3.1 there existed a draft wni storage. You can find it in the git
|
||||
history
|
||||
@ -16,22 +16,24 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# 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 re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from qubes.qubes import vm_files,system_path,defaults
|
||||
from qubes.qubes import QubesException
|
||||
import qubes.qubesutils
|
||||
from qubes.qubes import QubesException, defaults, system_path
|
||||
|
||||
CONFIG_FILE = '/etc/qubes/storage.conf'
|
||||
|
||||
|
||||
class QubesVmStorage(object):
|
||||
"""
|
||||
@ -55,12 +57,10 @@ class QubesVmStorage(object):
|
||||
else:
|
||||
self.root_img_size = defaults['root_img_size']
|
||||
|
||||
self.private_img = vm.absolute_path(vm_files["private_img"], None)
|
||||
if self.vm.template:
|
||||
self.root_img = self.vm.template.root_img
|
||||
else:
|
||||
self.root_img = vm.absolute_path(vm_files["root_img"], None)
|
||||
self.volatile_img = vm.absolute_path(vm_files["volatile_img"], None)
|
||||
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
|
||||
@ -69,9 +69,69 @@ class QubesVmStorage(object):
|
||||
# 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 = " <disk type='block' device='{type}'>\n" \
|
||||
" <driver name='phy'/>\n" \
|
||||
" <source dev='{path}'/>\n" \
|
||||
" <target dev='{vdev}' bus='xen'/>\n" \
|
||||
"{params}" \
|
||||
" </disk>\n"
|
||||
params = ""
|
||||
if not rw:
|
||||
params += " <readonly/>\n"
|
||||
if domain:
|
||||
params += " <backenddomain name='%s'/>\n" % domain
|
||||
if script:
|
||||
params += " <script path='%s'/>\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.
|
||||
@ -199,3 +259,188 @@ class QubesVmStorage(object):
|
||||
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)
|
||||
|
||||
12
core/storage/storage.conf
Normal file
12
core/storage/storage.conf
Normal file
@ -0,0 +1,12 @@
|
||||
[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
|
||||
|
||||
@ -1,138 +0,0 @@
|
||||
#!/usr/bin/python2
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2013 Marek Marczykowski <marmarek@invisiblethingslab.com>
|
||||
#
|
||||
# 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 sys
|
||||
import os
|
||||
import os.path
|
||||
import win32api
|
||||
import win32net
|
||||
import win32netcon
|
||||
import win32security
|
||||
import win32profile
|
||||
import pywintypes
|
||||
import random
|
||||
|
||||
from qubes.storage import QubesVmStorage
|
||||
from qubes.qubes import QubesException,system_path
|
||||
|
||||
class QubesWniVmStorage(QubesVmStorage):
|
||||
"""
|
||||
Class for VM storage of WNI VMs.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(QubesWniVmStorage, self).__init__(*args, **kwargs)
|
||||
# Use the user profile as "private.img"
|
||||
self.home_root = win32profile.GetProfilesDirectory()
|
||||
# FIXME: the assignment below may not always be correct,
|
||||
# but GetUserProfileDirectory needs a user token...
|
||||
self.private_img = os.path.join(self.home_root, self._get_username())
|
||||
|
||||
# Pass paths for WNI libvirt driver
|
||||
os.putenv("WNI_DRIVER_QUBESDB_PATH", system_path['qubesdb_daemon_path'])
|
||||
os.putenv("WNI_DRIVER_QREXEC_AGENT_PATH", system_path['qrexec_agent_path'])
|
||||
|
||||
def _get_username(self, vmname = None):
|
||||
if vmname is None:
|
||||
vmname = self.vm.name
|
||||
return "qubes-vm-%s" % vmname
|
||||
|
||||
def _get_random_password(self, vmname = None):
|
||||
if vmname is None:
|
||||
vmname = self.vm.name
|
||||
return '%x' % random.SystemRandom().getrandombits(256)
|
||||
|
||||
def get_config_params(self):
|
||||
return {}
|
||||
|
||||
def create_on_disk_private_img(self, verbose, source_template = None):
|
||||
# FIXME: this may not always be correct
|
||||
home_dir = os.path.join(self.home_root, self._get_username())
|
||||
# Create user data in information level 1 (PyUSER_INFO_1) format.
|
||||
user_data = {}
|
||||
user_data['name'] = self._get_username()
|
||||
user_data['full_name'] = self._get_username()
|
||||
# libvirt driver doesn't need to know the password anymore
|
||||
user_data['password'] = self._get_random_password()
|
||||
user_data['flags'] = (
|
||||
win32netcon.UF_NORMAL_ACCOUNT |
|
||||
win32netcon.UF_SCRIPT |
|
||||
win32netcon.UF_DONT_EXPIRE_PASSWD |
|
||||
win32netcon.UF_PASSWD_CANT_CHANGE
|
||||
)
|
||||
user_data['priv'] = win32netcon.USER_PRIV_USER
|
||||
user_data['home_dir'] = home_dir
|
||||
user_data['max_storage'] = win32netcon.USER_MAXSTORAGE_UNLIMITED
|
||||
# TODO: catch possible exception
|
||||
win32net.NetUserAdd(None, 1, user_data)
|
||||
|
||||
def create_on_disk_root_img(self, verbose, source_template = None):
|
||||
pass
|
||||
|
||||
def remove_from_disk(self):
|
||||
try:
|
||||
sid = win32security.LookupAccountName(None, self._get_username())[0]
|
||||
string_sid = win32security.ConvertSidToStringSid(sid)
|
||||
win32profile.DeleteProfile(string_sid)
|
||||
win32net.NetUserDel(None, self._get_username())
|
||||
except pywintypes.error, details:
|
||||
if details[0] == 2221:
|
||||
# "The user name cannot be found."
|
||||
raise IOError("User %s doesn't exist" % self._get_username())
|
||||
else:
|
||||
raise
|
||||
|
||||
super(QubesWniVmStorage, self).remove_from_disk()
|
||||
|
||||
def rename(self, old_name, new_name):
|
||||
super(QubesWniVmStorage, self).rename(old_name, new_name)
|
||||
user_data = {}
|
||||
user_data['name'] = self._get_username(new_name)
|
||||
win32net.NetUserSetInfo(None,
|
||||
self._get_username(old_name), 0, user_data)
|
||||
#TODO: rename user profile
|
||||
|
||||
def verify_files(self):
|
||||
if not os.path.exists (self.vmdir):
|
||||
raise QubesException (
|
||||
"VM directory doesn't exist: {0}".\
|
||||
format(self.vmdir))
|
||||
|
||||
try:
|
||||
# TemplateVm in WNI is quite virtual, so do not require the user
|
||||
if not self.vm.is_template():
|
||||
win32net.NetUserGetInfo(None, self._get_username(), 0)
|
||||
except pywintypes.error, details:
|
||||
if details[0] == 2221:
|
||||
# "The user name cannot be found."
|
||||
raise QubesException("User %s doesn't exist" % self._get_username())
|
||||
else:
|
||||
raise
|
||||
|
||||
def reset_volatile_storage(self, verbose = False, source_template = None):
|
||||
pass
|
||||
|
||||
def prepare_for_vm_startup(self, verbose = False):
|
||||
if self.vm.is_template():
|
||||
raise QubesException("Starting TemplateVM is not supported")
|
||||
@ -16,115 +16,92 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# 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
|
||||
import re
|
||||
|
||||
from qubes.storage import QubesVmStorage
|
||||
from qubes.qubes import QubesException, vm_files
|
||||
from qubes.storage import Pool, QubesVmStorage
|
||||
|
||||
|
||||
class QubesXenVmStorage(QubesVmStorage):
|
||||
class XenStorage(QubesVmStorage):
|
||||
"""
|
||||
Class for VM storage of Xen VMs.
|
||||
"""
|
||||
|
||||
def __init__(self, vm, **kwargs):
|
||||
super(QubesXenVmStorage, self).__init__(vm, **kwargs)
|
||||
def __init__(self, vm, vmdir, **kwargs):
|
||||
""" Instantiate the storage.
|
||||
|
||||
self.root_dev = "xvda"
|
||||
self.private_dev = "xvdb"
|
||||
self.volatile_dev = "xvdc"
|
||||
self.modules_dev = "xvdd"
|
||||
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"])
|
||||
self.rootcow_img = os.path.join(self.vmdir,
|
||||
vm_files["rootcow_img"])
|
||||
else:
|
||||
self.rootcow_img = None
|
||||
|
||||
def _format_disk_dev(self, path, script, vdev, rw=True, type="disk", domain=None):
|
||||
if path is None:
|
||||
return ''
|
||||
template = " <disk type='block' device='{type}'>\n" \
|
||||
" <driver name='phy'/>\n" \
|
||||
" <source dev='{path}'/>\n" \
|
||||
" <target dev='{vdev}' bus='xen'/>\n" \
|
||||
"{params}" \
|
||||
" </disk>\n"
|
||||
params = ""
|
||||
if not rw:
|
||||
params += " <readonly/>\n"
|
||||
if domain:
|
||||
params += " <backenddomain name='%s'/>\n" % domain
|
||||
if script:
|
||||
params += " <script path='%s'/>\n" % script
|
||||
return template.format(path=path, vdev=vdev, type=type,
|
||||
params=params)
|
||||
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 _get_rootdev(self):
|
||||
if self.vm.is_template() and \
|
||||
os.path.exists(os.path.join(self.vmdir, "root-cow.img")):
|
||||
return self._format_disk_dev(
|
||||
"{dir}/root.img:{dir}/root-cow.img".format(
|
||||
dir=self.vmdir),
|
||||
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 self.vm.template.storage.rootcow_img:
|
||||
# HVM template-based VM - template doesn't have own
|
||||
# root-cow.img, only one device-mapper layer
|
||||
return self._format_disk_dev(
|
||||
"{tpldir}/root.img:{vmdir}/volatile.img".format(
|
||||
tpldir=self.vm.template.dir_path,
|
||||
vmdir=self.vmdir),
|
||||
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(
|
||||
"{dir}/root.img:{dir}/root-cow.img".format(
|
||||
dir=self.vm.template.dir_path),
|
||||
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(
|
||||
"{dir}/root.img".format(dir=self.vmdir),
|
||||
return self.format_disk_dev(
|
||||
"{root}".format(root=self.root_img),
|
||||
None, self.root_dev, True)
|
||||
|
||||
def get_config_params(self):
|
||||
args = {}
|
||||
args['rootdev'] = self._get_rootdev()
|
||||
args['privatedev'] = \
|
||||
self._format_disk_dev(self.private_img,
|
||||
None, self.private_dev, True)
|
||||
args['volatiledev'] = \
|
||||
self._format_disk_dev(self.volatile_img,
|
||||
None, self.volatile_dev, True)
|
||||
if self.modules_img is not None:
|
||||
args['otherdevs'] = \
|
||||
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"
|
||||
if drive_domain.lower() == "dom0":
|
||||
drive_domain = None
|
||||
def private_dev_config(self):
|
||||
return self.format_disk_dev(self.private_img, None,
|
||||
self.private_dev, True)
|
||||
|
||||
args['otherdevs'] = self._format_disk_dev(drive_path, None,
|
||||
self.modules_dev,
|
||||
rw=True if drive_type == "disk" else False, type=drive_type,
|
||||
domain=drive_domain)
|
||||
else:
|
||||
args['otherdevs'] = ''
|
||||
|
||||
return args
|
||||
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:
|
||||
@ -158,7 +135,7 @@ class QubesXenVmStorage(QubesVmStorage):
|
||||
self.commit_template_changes()
|
||||
|
||||
def rename(self, old_name, new_name):
|
||||
super(QubesXenVmStorage, self).rename(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:
|
||||
@ -226,11 +203,11 @@ class QubesXenVmStorage(QubesVmStorage):
|
||||
f_volatile.close()
|
||||
f_root.close()
|
||||
return
|
||||
super(QubesXenVmStorage, self).reset_volatile_storage(
|
||||
super(XenStorage, self).reset_volatile_storage(
|
||||
verbose=verbose, source_template=source_template)
|
||||
|
||||
def prepare_for_vm_startup(self, verbose):
|
||||
super(QubesXenVmStorage, self).prepare_for_vm_startup(verbose=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(":")
|
||||
@ -249,3 +226,15 @@ class QubesXenVmStorage(QubesVmStorage):
|
||||
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)
|
||||
|
||||
@ -28,7 +28,7 @@ import sys
|
||||
import shutil
|
||||
import time
|
||||
|
||||
from qubes.qubes import QubesVmCollection
|
||||
from qubes.qubes import QubesVmCollection, QubesException
|
||||
from qubes.qubes import QubesDispVmLabels
|
||||
from qubes.notify import tray_notify, tray_notify_error, tray_notify_init
|
||||
|
||||
@ -51,61 +51,67 @@ class QfileDaemonDvm:
|
||||
|
||||
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')])
|
||||
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()))
|
||||
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 ?')
|
||||
qvm_collection.unlock_db()
|
||||
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 ?')
|
||||
qvm_collection.unlock_db()
|
||||
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
|
||||
if os.path.exists(vm.firewall_conf):
|
||||
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
|
||||
shutil.copy(vm.firewall_conf, disp_firewall_conf)
|
||||
dispvm.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')
|
||||
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()
|
||||
return None
|
||||
print >>sys.stderr, "time=%s, VM starting" % (str(time.time()))
|
||||
dispvm.start()
|
||||
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()
|
||||
qvm_collection.unlock_db()
|
||||
# Reload firewall rules
|
||||
print >>sys.stderr, "time=%s, reloading firewall" % (str(time.time()))
|
||||
for vm in qvm_collection.values():
|
||||
@ -144,7 +150,7 @@ class QfileDaemonDvm:
|
||||
return self.do_get_dvm()
|
||||
|
||||
@staticmethod
|
||||
def remove_disposable_from_qdb(name):
|
||||
def finish_disposable(name):
|
||||
qvm_collection = QubesVmCollection()
|
||||
qvm_collection.lock_db_for_writing()
|
||||
qvm_collection.load()
|
||||
@ -152,6 +158,12 @@ class QfileDaemonDvm:
|
||||
if vm is None:
|
||||
qvm_collection.unlock_db()
|
||||
return False
|
||||
|
||||
try:
|
||||
vm.force_shutdown()
|
||||
except QubesException:
|
||||
# VM already destroyed
|
||||
pass
|
||||
qvm_collection.pop(vm.qid)
|
||||
qvm_collection.save()
|
||||
qvm_collection.unlock_db()
|
||||
@ -159,6 +171,10 @@ class QfileDaemonDvm:
|
||||
|
||||
def main():
|
||||
exec_index = sys.argv[1]
|
||||
if exec_index == "FINISH":
|
||||
QfileDaemonDvm.finish_disposable(sys.argv[2])
|
||||
return
|
||||
|
||||
src_vmname = sys.argv[2]
|
||||
user = sys.argv[3]
|
||||
# accessed directly by get_dvm()
|
||||
@ -171,11 +187,14 @@ def main():
|
||||
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])
|
||||
dispvm.force_shutdown()
|
||||
qfile.remove_disposable_from_qdb(dispvm.name)
|
||||
QfileDaemonDvm.finish_disposable(dispvm.name)
|
||||
|
||||
main()
|
||||
|
||||
@ -74,8 +74,13 @@ 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
|
||||
tar -cSf saved-cows.tar volatile.img || exit 1
|
||||
else
|
||||
bsdtar -cSf saved-cows.tar volatile.img
|
||||
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."
|
||||
|
||||
@ -6,7 +6,7 @@ NAME
|
||||
====
|
||||
qvm-add-appvm - add an already installed appvm to the Qubes DB
|
||||
|
||||
WARNING: Noramlly you would not need this command, and you would use qvm-create instead!
|
||||
WARNING: Normally you should not need this command, and you should use qvm-create instead!
|
||||
|
||||
:Date: 2012-04-10
|
||||
|
||||
@ -22,6 +22,8 @@ OPTIONS
|
||||
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
|
||||
=======
|
||||
|
||||
@ -16,22 +16,34 @@ 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 or netvms, restore VMs anyway
|
||||
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, even with root privileges
|
||||
Force to run with root privileges
|
||||
--replace-template=REPLACE_TEMPLATE
|
||||
Restore VMs using another template, syntax: old-template-name:new-template-name (might be repeated)
|
||||
Restore VMs using another template, syntax: old-template-name:new-template-name (can be repeated)
|
||||
-x EXCLUDE, --exclude=EXCLUDE
|
||||
Skip restore of specified VM (might be repeated)
|
||||
Skip restore of specified VM (can be repeated)
|
||||
--skip-dom0-home
|
||||
Do not restore dom0 user home dir
|
||||
Do not restore dom0's user home directory
|
||||
--ignore-username-mismatch
|
||||
Ignore dom0 username mismatch while restoring homedir
|
||||
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
|
||||
=======
|
||||
|
||||
@ -18,6 +18,28 @@ OPTIONS
|
||||
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
|
||||
=======
|
||||
|
||||
@ -6,7 +6,6 @@ NAME
|
||||
====
|
||||
qvm-block - list/set VM PCI devices.
|
||||
|
||||
|
||||
:Date: 2012-04-10
|
||||
|
||||
SYNOPSIS
|
||||
@ -16,13 +15,14 @@ SYNOPSIS
|
||||
| qvm-block -d [options] <device>
|
||||
| qvm-block -d [options] <vm-name>
|
||||
|
||||
|
||||
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
|
||||
@ -33,6 +33,10 @@ OPTIONS
|
||||
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
|
||||
=======
|
||||
|
||||
32
doc/qvm-tools/qvm-check.rst
Normal file
32
doc/qvm-tools/qvm-check.rst
Normal file
@ -0,0 +1,32 @@
|
||||
=========
|
||||
qvm-check
|
||||
=========
|
||||
|
||||
NAME
|
||||
====
|
||||
qvm-check - Specify no state options to check if VM exists
|
||||
|
||||
:Date: 2013-06-23
|
||||
|
||||
SYNOPSIS
|
||||
========
|
||||
| qvm-check [options] <vm-name>
|
||||
|
||||
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 <joanna at invisiblethingslab dot com>
|
||||
| Rafal Wojtczuk <rafal at invisiblethingslab dot com>
|
||||
| Marek Marczykowski <marmarek at invisiblethingslab dot com>
|
||||
@ -1,29 +0,0 @@
|
||||
==================
|
||||
qvm-clone-template
|
||||
==================
|
||||
|
||||
NAME
|
||||
====
|
||||
qvm-clone-template - clones an existing template by copying all its disk files
|
||||
|
||||
:Date: 2012-04-10
|
||||
|
||||
SYNOPSIS
|
||||
========
|
||||
| qvm-clone-template [options] <src-template-name> <new-template-name>
|
||||
|
||||
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
|
||||
|
||||
AUTHORS
|
||||
=======
|
||||
| Joanna Rutkowska <joanna at invisiblethingslab dot com>
|
||||
| Rafal Wojtczuk <rafal at invisiblethingslab dot com>
|
||||
| Marek Marczykowski <marmarek at invisiblethingslab dot com>
|
||||
|
||||
@ -20,6 +20,10 @@ OPTIONS
|
||||
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
|
||||
=======
|
||||
|
||||
@ -36,8 +36,12 @@ OPTIONS
|
||||
-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
|
||||
=======
|
||||
|
||||
26
doc/qvm-tools/qvm-grow-root.rst
Normal file
26
doc/qvm-tools/qvm-grow-root.rst
Normal file
@ -0,0 +1,26 @@
|
||||
=============
|
||||
qvm-grow-root
|
||||
=============
|
||||
|
||||
NAME
|
||||
====
|
||||
qvm-grow-root - increase root storage capacity of a specified VM
|
||||
|
||||
:Date: 2014-03-21
|
||||
|
||||
SYNOPSIS
|
||||
========
|
||||
| qvm-grow-root <vm-name> <size>
|
||||
|
||||
OPTIONS
|
||||
=======
|
||||
-h, --help
|
||||
Show this help message and exit
|
||||
--allow-start
|
||||
Allow VM to be started to complete the operation
|
||||
|
||||
AUTHORS
|
||||
=======
|
||||
| Joanna Rutkowska <joanna at invisiblethingslab dot com>
|
||||
| Rafal Wojtczuk <rafal at invisiblethingslab dot com>
|
||||
| Marek Marczykowski <marmarek at invisiblethingslab dot com>
|
||||
@ -32,6 +32,8 @@ OPTIONS
|
||||
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.
|
||||
|
||||
AUTHORS
|
||||
=======
|
||||
|
||||
@ -23,8 +23,14 @@ OPTIONS
|
||||
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
|
||||
=======
|
||||
|
||||
@ -25,6 +25,10 @@ OPTIONS
|
||||
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
|
||||
==========
|
||||
@ -49,6 +53,14 @@ pci_strictreset
|
||||
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``
|
||||
@ -134,7 +146,7 @@ mac
|
||||
|
||||
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 some licencing depending on static MAC address.
|
||||
useful when licensing requires a static MAC address.
|
||||
For template-based HVM ``auto`` mode means to clone template MAC.
|
||||
|
||||
default_user
|
||||
@ -147,7 +159,7 @@ 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
|
||||
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
|
||||
@ -172,7 +184,7 @@ guiagent_installed
|
||||
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
|
||||
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
|
||||
|
||||
@ -17,7 +17,7 @@ OPTIONS
|
||||
-h, --help
|
||||
Show this help message and exit
|
||||
--force
|
||||
Do not prompt for comfirmation
|
||||
Do not prompt for confirmation
|
||||
|
||||
AUTHORS
|
||||
=======
|
||||
|
||||
@ -40,6 +40,20 @@ OPTIONS
|
||||
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)
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ OPTIONS
|
||||
SUPPORTED SERVICES
|
||||
==================
|
||||
|
||||
This list can be incomplete as VM can implement any additional service without knowlege of qubes-core code.
|
||||
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
|
||||
@ -38,7 +38,7 @@ meminfo-writer
|
||||
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 defult state), will be recreated with the rule: enabled
|
||||
remove it (reset to default state), will be recreated with the rule: enabled
|
||||
if VM have no PCI devices assigned, otherwise disabled.
|
||||
|
||||
qubes-dvm
|
||||
@ -73,7 +73,7 @@ cups
|
||||
|
||||
Enable CUPS service. The user can disable cups in VM which do not need printing to speed up booting.
|
||||
|
||||
cron
|
||||
crond
|
||||
Default: disabled
|
||||
|
||||
Enable CRON service.
|
||||
|
||||
@ -22,6 +22,8 @@ OPTIONS
|
||||
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
|
||||
|
||||
@ -18,14 +18,26 @@ OPTIONS
|
||||
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)
|
||||
--console
|
||||
Attach debugging console to the newly started VM
|
||||
--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
|
||||
=======
|
||||
|
||||
@ -16,6 +16,8 @@ OPTIONS
|
||||
=======
|
||||
-h, --help
|
||||
Show this help message and exit
|
||||
--offline-mode
|
||||
Offline mode
|
||||
|
||||
AUTHORS
|
||||
=======
|
||||
|
||||
36
doc/qvm-tools/qvm-usb.rst
Normal file
36
doc/qvm-tools/qvm-usb.rst
Normal file
@ -0,0 +1,36 @@
|
||||
=======
|
||||
qvm-usb
|
||||
=======
|
||||
|
||||
NAME
|
||||
====
|
||||
qvm-usb - List/set VM USB devices
|
||||
|
||||
:Date: 2013-03-16
|
||||
|
||||
SYNOPSIS
|
||||
========
|
||||
| qvm-usb -l [options]
|
||||
| qvm-usb -a [options] <vm-name> <device-vm-name>:<device>
|
||||
| qvm-usb -d [options] <device-vm-name>:<device>
|
||||
|
||||
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 <joanna at invisiblethingslab dot com>
|
||||
| Rafal Wojtczuk <rafal at invisiblethingslab dot com>
|
||||
| Marek Marczykowski <marmarek at invisiblethingslab dot com>
|
||||
@ -1,9 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ "`id -u`" != "0" ]; then
|
||||
exec sudo $0 $*
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
if ! echo $PATH | grep -q sbin; then
|
||||
@ -24,30 +20,6 @@ if [ -e "$FILENAME" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
umask 002
|
||||
TOTAL_SIZE=$[ $ROOT_SIZE + $SWAP_SIZE + 512 ]
|
||||
truncate -s ${TOTAL_SIZE}M "$FILENAME"
|
||||
sfdisk --no-reread -u M "$FILENAME" > /dev/null 2> /dev/null <<EOF
|
||||
0,${SWAP_SIZE},S
|
||||
,${ROOT_SIZE},L
|
||||
EOF
|
||||
|
||||
(
|
||||
flock 200
|
||||
loopdev=`losetup -f --show --partscan "$FILENAME"`
|
||||
udevadm settle
|
||||
created=
|
||||
if [ ! -e ${loopdev}p1 ]; then
|
||||
# device wasn't created automatically, probably udev isn't running;
|
||||
# create devs manually
|
||||
for partdev in /sys/block/$(basename ${loopdev})/loop*p*; do
|
||||
mknod /dev/$(basename ${partdev}) b $(cat ${partdev}/dev | tr : ' ')
|
||||
done
|
||||
created=yes
|
||||
fi
|
||||
mkswap -f ${loopdev}p1 > /dev/null
|
||||
if [ "$created" = "yes" ]; then
|
||||
rm -f ${loopdev}p*
|
||||
fi
|
||||
losetup -d ${loopdev} || :
|
||||
chown --reference `dirname "$FILENAME"` "$FILENAME"
|
||||
) 200>"/var/run/qubes/prepare-volatile-img.lock"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
[Unit]
|
||||
Description=Qubes NetVM startup
|
||||
After=qubes-core.service qubes-qmemman.service libvirtd.service
|
||||
Before=plymouth-quit.service
|
||||
Before=systemd-user-sessions.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
[Unit]
|
||||
Description=Start Qubes VM %i
|
||||
After=qubes-netvm.service
|
||||
Before=plymouth-quit.service
|
||||
Before=systemd-user-sessions.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Environment=DISPLAY=:0
|
||||
ExecStart=/usr/bin/qvm-start --no-guid %i
|
||||
ExecStart=/usr/bin/qvm-start --no-guid --skip-if-running %i
|
||||
Group=qubes
|
||||
RemainAfterExit=yes
|
||||
|
||||
|
||||
@ -37,7 +37,9 @@ slow_memset_react_msg="VM didn't give back all requested memory"
|
||||
class DomainState:
|
||||
def __init__(self, id):
|
||||
self.meminfo = None #dictionary of memory info read from client
|
||||
self.memory_actual = None #the current memory size
|
||||
self.memory_current = 0 # the current memory size
|
||||
self.memory_actual = None # the current memory allocation (what VM
|
||||
# is using or can use at any time)
|
||||
self.memory_maximum = None #the maximum memory size
|
||||
self.mem_used = None #used memory, computed based on meminfo
|
||||
self.id = id #domain id
|
||||
@ -45,6 +47,9 @@ class DomainState:
|
||||
self.no_progress = False #no react to memset
|
||||
self.slow_memset_react = False #slow react to memset (after few tries still above target)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__dict__.__repr__()
|
||||
|
||||
class SystemState(object):
|
||||
def __init__(self):
|
||||
self.log = logging.getLogger('qmemman.systemstate')
|
||||
@ -65,25 +70,53 @@ class SystemState(object):
|
||||
def add_domain(self, id):
|
||||
self.log.debug('add_domain(id={!r})'.format(id))
|
||||
self.domdict[id] = DomainState(id)
|
||||
# TODO: move to DomainState.__init__
|
||||
target_str = self.xs.read('', '/local/domain/' + id + '/memory/target')
|
||||
if target_str:
|
||||
self.domdict[id].last_target = int(target_str) * 1024
|
||||
|
||||
def del_domain(self, id):
|
||||
self.log.debug('del_domain(id={!r})'.format(id))
|
||||
self.domdict.pop(id)
|
||||
|
||||
def get_free_xen_memory(self):
|
||||
return int(self.xc.physinfo()['free_memory']*1024 * self.MEM_OVERHEAD_FACTOR)
|
||||
# hosts = self.xend_session.session.xenapi.host.get_all()
|
||||
# host_record = self.xend_session.session.xenapi.host.get_record(hosts[0])
|
||||
# host_metrics_record = self.xend_session.session.xenapi.host_metrics.get_record(host_record["metrics"])
|
||||
# ret = host_metrics_record["memory_free"]
|
||||
# return long(ret)
|
||||
xen_free = int(self.xc.physinfo()['free_memory']*1024 *
|
||||
self.MEM_OVERHEAD_FACTOR)
|
||||
# now check for domains which have assigned more memory than really
|
||||
# used - do not count it as "free", because domain is free to use it
|
||||
# at any time
|
||||
# assumption: self.refresh_memactual was called before
|
||||
# (so domdict[id].memory_actual is up to date)
|
||||
assigned_but_unused = reduce(
|
||||
lambda acc, dom: acc + max(0, dom.last_target-dom.memory_current),
|
||||
self.domdict.values(),
|
||||
0
|
||||
)
|
||||
# If, at any time, Xen have less memory than XEN_FREE_MEM_MIN,
|
||||
# it is a failure of qmemman. Collect as much data as possible to
|
||||
# debug it
|
||||
if xen_free < self.XEN_FREE_MEM_MIN:
|
||||
self.log.error("Xen free = {!r} below acceptable value! "
|
||||
"assigned_but_unused={!r}, domdict={!r}".format(
|
||||
xen_free, assigned_but_unused, self.domdict))
|
||||
elif xen_free < assigned_but_unused+self.XEN_FREE_MEM_MIN:
|
||||
self.log.error("Xen free = {!r} too small for satisfy assignments! "
|
||||
"assigned_but_unused={!r}, domdict={!r}".format(
|
||||
xen_free, assigned_but_unused, self.domdict))
|
||||
return xen_free - assigned_but_unused
|
||||
|
||||
#refresh information on memory assigned to all domains
|
||||
def refresh_memactual(self):
|
||||
for domain in self.xc.domain_getinfo():
|
||||
id = str(domain['domid'])
|
||||
if self.domdict.has_key(id):
|
||||
self.domdict[id].memory_actual = domain['mem_kb']*1024
|
||||
# real memory usage
|
||||
self.domdict[id].memory_current = domain['mem_kb']*1024
|
||||
# what VM is using or can use
|
||||
self.domdict[id].memory_actual = max(
|
||||
self.domdict[id].memory_current,
|
||||
self.domdict[id].last_target
|
||||
)
|
||||
self.domdict[id].memory_maximum = self.xs.read('', '/local/domain/%s/memory/static-max' % str(id))
|
||||
if self.domdict[id].memory_maximum:
|
||||
self.domdict[id].memory_maximum = int(self.domdict[id].memory_maximum)*1024
|
||||
@ -272,11 +305,11 @@ class SystemState(object):
|
||||
self.log.debug('do_balance dom={!r} sleeping ntries={}'.format(
|
||||
dom, ntries))
|
||||
time.sleep(self.BALOON_DELAY)
|
||||
self.refresh_memactual()
|
||||
ntries -= 1
|
||||
if ntries <= 0:
|
||||
# Waiting haven't helped; Find which domain get stuck and
|
||||
# abort balance (after distributing what we have)
|
||||
self.refresh_memactual()
|
||||
for rq2 in memset_reqs:
|
||||
dom2, mem2 = rq2
|
||||
if dom2 == dom:
|
||||
|
||||
@ -42,6 +42,15 @@ LOG_PATH='/var/log/qubes/qmemman.log'
|
||||
|
||||
system_state = SystemState()
|
||||
global_lock = thread.allocate_lock()
|
||||
# If XS_Watcher will
|
||||
# handle meminfo event before @introduceDomain, it will use
|
||||
# incomplete domain list for that and may redistribute memory
|
||||
# allocated to some VM, but not yet used (see #1389).
|
||||
# To fix that, system_state should be updated (refresh domain
|
||||
# list) before processing other changes, every time some process requested
|
||||
# memory for a new VM, before releasing the lock. Then XS_Watcher will check
|
||||
# this flag before processing other event.
|
||||
force_refresh_domain_list = False
|
||||
|
||||
def only_in_first_list(l1, l2):
|
||||
ret=[]
|
||||
@ -65,41 +74,65 @@ class XS_Watcher:
|
||||
self.log.debug('XS_Watcher()')
|
||||
|
||||
self.handle = xen.lowlevel.xs.xs()
|
||||
self.handle.watch('@introduceDomain', WatchType(XS_Watcher.domain_list_changed, None))
|
||||
self.handle.watch('@releaseDomain', WatchType(XS_Watcher.domain_list_changed, None))
|
||||
self.handle.watch('@introduceDomain', WatchType(
|
||||
XS_Watcher.domain_list_changed, False))
|
||||
self.handle.watch('@releaseDomain', WatchType(
|
||||
XS_Watcher.domain_list_changed, False))
|
||||
self.watch_token_dict = {}
|
||||
|
||||
def domain_list_changed(self, refresh_only=False):
|
||||
"""
|
||||
Check if any domain was created/destroyed. If it was, update
|
||||
appropriate list. Then redistribute memory.
|
||||
|
||||
def domain_list_changed(self, param):
|
||||
self.log.debug('domain_list_changed(param={!r})'.format(param))
|
||||
:param refresh_only If True, only refresh domain list, do not
|
||||
redistribute memory. In this mode, caller must already hold
|
||||
global_lock.
|
||||
"""
|
||||
self.log.debug('domain_list_changed(only_refresh={!r})'.format(
|
||||
refresh_only))
|
||||
|
||||
curr = self.handle.ls('', '/local/domain')
|
||||
self.log.debug('curr={!r}'.format(curr))
|
||||
got_lock = False
|
||||
if not refresh_only:
|
||||
self.log.debug('acquiring global_lock')
|
||||
global_lock.acquire()
|
||||
got_lock = True
|
||||
self.log.debug('global_lock acquired')
|
||||
try:
|
||||
curr = self.handle.ls('', '/local/domain')
|
||||
if curr is None:
|
||||
return
|
||||
|
||||
if curr == None:
|
||||
return
|
||||
# check if domain is really there, it may happen that some empty
|
||||
# directories are left in xenstore
|
||||
curr = filter(
|
||||
lambda x:
|
||||
self.handle.read('',
|
||||
'/local/domain/{}/domid'.format(x)
|
||||
) is not None,
|
||||
curr
|
||||
)
|
||||
self.log.debug('curr={!r}'.format(curr))
|
||||
|
||||
self.log.debug('acquiring global_lock')
|
||||
global_lock.acquire()
|
||||
self.log.debug('global_lock acquired')
|
||||
for i in only_in_first_list(curr, self.watch_token_dict.keys()):
|
||||
# new domain has been created
|
||||
watch = WatchType(XS_Watcher.meminfo_changed, i)
|
||||
self.watch_token_dict[i] = watch
|
||||
self.handle.watch(get_domain_meminfo_key(i), watch)
|
||||
system_state.add_domain(i)
|
||||
|
||||
for i in only_in_first_list(curr, self.watch_token_dict.keys()):
|
||||
#new domain has been created
|
||||
watch = WatchType(XS_Watcher.meminfo_changed, i)
|
||||
self.watch_token_dict[i] = watch
|
||||
self.handle.watch(get_domain_meminfo_key(i), watch)
|
||||
system_state.add_domain(i)
|
||||
for i in only_in_first_list(self.watch_token_dict.keys(), curr):
|
||||
# domain destroyed
|
||||
self.handle.unwatch(get_domain_meminfo_key(i), self.watch_token_dict[i])
|
||||
self.watch_token_dict.pop(i)
|
||||
system_state.del_domain(i)
|
||||
finally:
|
||||
if got_lock:
|
||||
global_lock.release()
|
||||
self.log.debug('global_lock released')
|
||||
|
||||
for i in only_in_first_list(self.watch_token_dict.keys(), curr):
|
||||
#domain destroyed
|
||||
self.handle.unwatch(get_domain_meminfo_key(i), self.watch_token_dict[i])
|
||||
self.watch_token_dict.pop(i)
|
||||
system_state.del_domain(i)
|
||||
|
||||
global_lock.release()
|
||||
self.log.debug('global_lock released')
|
||||
|
||||
system_state.do_balance()
|
||||
if not refresh_only:
|
||||
system_state.do_balance()
|
||||
|
||||
|
||||
def meminfo_changed(self, domain_id):
|
||||
@ -111,6 +144,8 @@ class XS_Watcher:
|
||||
self.log.debug('acquiring global_lock')
|
||||
global_lock.acquire()
|
||||
self.log.debug('global_lock acquired')
|
||||
if force_refresh_domain_list:
|
||||
self.domain_list_changed(refresh_only=True)
|
||||
|
||||
system_state.refresh_meminfo(domain_id, untrusted_meminfo_key)
|
||||
|
||||
@ -140,35 +175,41 @@ class QMemmanReqHandler(SocketServer.BaseRequestHandler):
|
||||
self.log = logging.getLogger('qmemman.daemon.reqhandler')
|
||||
|
||||
got_lock = False
|
||||
# self.request is the TCP socket connected to the client
|
||||
while True:
|
||||
self.data = self.request.recv(1024).strip()
|
||||
self.log.debug('data={!r}'.format(self.data))
|
||||
if len(self.data) == 0:
|
||||
self.log.info('EOF')
|
||||
try:
|
||||
# self.request is the TCP socket connected to the client
|
||||
while True:
|
||||
self.data = self.request.recv(1024).strip()
|
||||
self.log.debug('data={!r}'.format(self.data))
|
||||
if len(self.data) == 0:
|
||||
self.log.info('EOF')
|
||||
if got_lock:
|
||||
global force_refresh_domain_list
|
||||
force_refresh_domain_list = True
|
||||
return
|
||||
|
||||
# XXX something is wrong here: return without release?
|
||||
if got_lock:
|
||||
global_lock.release()
|
||||
self.log.debug('global_lock released')
|
||||
return
|
||||
self.log.warning('Second request over qmemman.sock?')
|
||||
return
|
||||
|
||||
# XXX something is wrong here: return without release?
|
||||
self.log.debug('acquiring global_lock')
|
||||
global_lock.acquire()
|
||||
self.log.debug('global_lock acquired')
|
||||
|
||||
got_lock = True
|
||||
if system_state.do_balloon(int(self.data)):
|
||||
resp = "OK\n"
|
||||
else:
|
||||
resp = "FAIL\n"
|
||||
self.log.debug('resp={!r}'.format(resp))
|
||||
self.request.send(resp)
|
||||
except BaseException as e:
|
||||
self.log.exception(
|
||||
"exception while handling request: {!r}".format(e))
|
||||
finally:
|
||||
if got_lock:
|
||||
self.log.warning('Second request over qmemman.sock?')
|
||||
return
|
||||
|
||||
self.log.debug('acquiring global_lock')
|
||||
global_lock.acquire()
|
||||
self.log.debug('global_lock acquired')
|
||||
|
||||
got_lock = True
|
||||
if system_state.do_balloon(int(self.data)):
|
||||
resp = "OK\n"
|
||||
else:
|
||||
resp = "FAIL\n"
|
||||
self.log.debug('resp={!r}'.format(resp))
|
||||
self.request.send(resp)
|
||||
|
||||
# XXX no release of lock?
|
||||
global_lock.release()
|
||||
self.log.debug('global_lock released')
|
||||
|
||||
|
||||
def start_server(server):
|
||||
@ -194,8 +235,13 @@ class QMemmanServer:
|
||||
usage = "usage: %prog [options]"
|
||||
parser = OptionParser(usage)
|
||||
parser.add_option("-c", "--config", action="store", dest="config", default=config_path)
|
||||
parser.add_option("-d", "--debug", action="store_true", dest="debug",
|
||||
default=False, help="Enable debugging")
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
if options.debug:
|
||||
logging.root.setLevel(logging.DEBUG)
|
||||
|
||||
# close io
|
||||
sys.stdin.close()
|
||||
sys.stdout.close()
|
||||
|
||||
6
qubes-rpc-policy/qubes.GetRandomizedTime.policy
Normal file
6
qubes-rpc-policy/qubes.GetRandomizedTime.policy
Normal file
@ -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
|
||||
@ -3,5 +3,8 @@
|
||||
|
||||
## Please use a single # to start your custom comments
|
||||
|
||||
sys-whonix anon-whonix allow
|
||||
whonix-gw anon-whonix allow
|
||||
whonix-ws anon-whonix allow
|
||||
$anyvm $dispvm allow
|
||||
$anyvm $anyvm ask
|
||||
|
||||
10
qubes-rpc-policy/qubes.OpenURL.policy
Normal file
10
qubes-rpc-policy/qubes.OpenURL.policy
Normal file
@ -0,0 +1,10 @@
|
||||
## 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
|
||||
|
||||
sys-whonix anon-whonix allow
|
||||
whonix-gw anon-whonix allow
|
||||
whonix-ws anon-whonix allow
|
||||
$anyvm $dispvm allow
|
||||
$anyvm $anyvm ask
|
||||
77
qubes-rpc/qubes.GetRandomizedTime
Executable file
77
qubes-rpc/qubes.GetRandomizedTime
Executable file
@ -0,0 +1,77 @@
|
||||
#!/bin/bash
|
||||
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2016 Patrick Schleizer <adrelanos@riseup.net>
|
||||
#
|
||||
# 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.
|
||||
|
||||
## Similar code as Boot Clock Randomization.
|
||||
## https://www.whonix.org/wiki/Boot_Clock_Randomization
|
||||
|
||||
set -e
|
||||
|
||||
## Get a random 0 or 1.
|
||||
## Will use this to decide to use plus or minus.
|
||||
ZERO_OR_ONE="$(shuf -i0-1 -n1 --random-source=/dev/random)"
|
||||
|
||||
## Create a random number between 0 and 180.
|
||||
DELAY="$(shuf -i0-180 -n1 --random-source=/dev/random)"
|
||||
|
||||
## Create a random number between 0 and 999999999.
|
||||
##
|
||||
## Thanks to
|
||||
## https://stackoverflow.com/questions/22887891/how-can-i-get-a-random-dev-random-number-between-0-and-999999999-in-bash
|
||||
NANOSECONDS="$(shuf -i0-999999999 -n1 --random-source=/dev/random)"
|
||||
|
||||
## Examples NANOSECONDS:
|
||||
## 117752805
|
||||
## 38653957
|
||||
|
||||
## Add leading zeros, because `date` expects 9 digits.
|
||||
NANOSECONDS="$(printf '%0*d\n' 9 "$NANOSECONDS")"
|
||||
|
||||
## Using
|
||||
## printf '%0*d\n' 9 "38653957"
|
||||
## 38653957
|
||||
## becomes
|
||||
## 038653957
|
||||
|
||||
## Examples NANOSECONDS:
|
||||
## 117752805
|
||||
## 038653957
|
||||
|
||||
if [ "$ZERO_OR_ONE" = "0" ]; then
|
||||
PLUS_OR_MINUS="-"
|
||||
elif [ "$ZERO_OR_ONE" = "1" ]; then
|
||||
PLUS_OR_MINUS="+"
|
||||
else
|
||||
exit 2
|
||||
fi
|
||||
|
||||
#OLD_TIME="$(date)"
|
||||
#OLD_TIME_NANOSECONDS="$(date +%s.%N)"
|
||||
|
||||
OLD_UNIXTIME="$(date +%s)"
|
||||
|
||||
NEW_TIME="$(( $OLD_UNIXTIME $PLUS_OR_MINUS $DELAY ))"
|
||||
|
||||
NEW_TIME_NANOSECONDS="$NEW_TIME.$NANOSECONDS"
|
||||
|
||||
echo "$NEW_TIME_NANOSECONDS"
|
||||
|
||||
## Testing the `date` syntax:
|
||||
## date --date @1396733199.112834496
|
||||
## date --date "@$NEW_TIME_NANOSECONDS"
|
||||
228
qvm-tools/qubes-bug-report
Executable file
228
qvm-tools/qubes-bug-report
Executable file
@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import subprocess
|
||||
import argparse
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
|
||||
from os.path import expanduser
|
||||
|
||||
#the term qube refers to a qubes vm
|
||||
|
||||
def is_program_installed_in_qube( program, qube_name ):
|
||||
is_installed = True
|
||||
|
||||
try:
|
||||
command = 'command -v ' + program
|
||||
subprocess.check_call([ 'qvm-run', qube_name, '--pass-io', '--no-color-output', command ], stdout = open( os.devnull, 'w' ) )
|
||||
|
||||
except subprocess.CalledProcessError:
|
||||
is_installed = False
|
||||
|
||||
return is_installed
|
||||
|
||||
|
||||
#this function requires virsh
|
||||
#domstate only works for Xen domU (guests)
|
||||
def is_qube_running( qube_name ):
|
||||
runs = False
|
||||
|
||||
out = subprocess.check_output([ "virsh", "-c", "xen:///", "domstate", qube_name ])
|
||||
out = out.decode('utf-8').replace('\n', '')
|
||||
|
||||
if 'running' == out:
|
||||
runs = True
|
||||
|
||||
return runs
|
||||
|
||||
|
||||
def get_qube_packages( qube_name ):
|
||||
content = "## Qubes Packages\n\n"
|
||||
|
||||
#a qube can have more than one package manager installed (only one is functional)
|
||||
pkg_cmd = { 'dpkg' : 'dpkg -l qubes-*', 'pacman' : 'pacman -Qs qubes', 'rpm' : 'rpm -qa qubes-*' }
|
||||
|
||||
if is_qube_running( qube_name ):
|
||||
|
||||
for package_manager in pkg_cmd.keys():
|
||||
if is_program_installed_in_qube( package_manager, qube_name ):
|
||||
pkg_list_cmd = pkg_cmd[package_manager]
|
||||
|
||||
try:
|
||||
out = subprocess.check_output([ 'qvm-run', qube_name, '--pass-io', '--no-color-output', pkg_list_cmd ], stderr = open( os.devnull, 'w' ) )
|
||||
out = out.decode('utf-8')
|
||||
content += create_heading( ( "Package Manager: " + package_manager ), 3 )
|
||||
content += wrap_code( out )
|
||||
except subprocess.CalledProcessError:
|
||||
pass #do nothing
|
||||
|
||||
else:
|
||||
content += "**No packages listed, because Qube " + qube_name + " was not running**\n\n"
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def get_dom0_packages():
|
||||
content = create_heading( "Dom0 Packages", 2 )
|
||||
out = subprocess.check_output([ "rpm", "-qa", "qubes-*" ])
|
||||
out = out.decode('utf-8')
|
||||
content += wrap_code( out )
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def wrap_code( text ):
|
||||
code = "~~~\n" + text + "~~~\n\n"
|
||||
|
||||
return code
|
||||
|
||||
def create_heading( heading, level ):
|
||||
heading = heading + "\n\n"
|
||||
|
||||
if 1 == level:
|
||||
heading = "# " + heading
|
||||
elif 2 == level:
|
||||
heading = "## " + heading
|
||||
else:
|
||||
heading = "### " + heading
|
||||
|
||||
return heading
|
||||
|
||||
|
||||
|
||||
def get_log_file_content( qube_name ):
|
||||
content = "## Log Files\n\n"
|
||||
qubes_os_log = "/var/log/qubes/"
|
||||
ext = ".log"
|
||||
|
||||
log_prefix = [ "guid", "pacat", "qubesdb", "qrexec" ]
|
||||
|
||||
#constructs for each log file prefix the full path and reads the log file
|
||||
for prefix in log_prefix:
|
||||
log_file = prefix + "." + qube_name + ext
|
||||
content += create_heading( ( "Log File: " + log_file ), 3 )
|
||||
content += wrap_code( get_log_file( qubes_os_log + log_file ) )
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def get_qube_prefs( qube_name ):
|
||||
qube_prefs = subprocess.check_output([ "qvm-prefs", qube_name ])
|
||||
qube_prefs = qube_prefs.decode('utf-8')
|
||||
|
||||
content = create_heading( "Qube Prefs", 2 )
|
||||
content += wrap_code( qube_prefs )
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def report( qube_name ):
|
||||
template = '''{title}
|
||||
{content}
|
||||
'''
|
||||
|
||||
title_text = create_heading( "Bug report: " + qube_name, 1 )
|
||||
|
||||
content_text = get_qube_prefs( qube_name )
|
||||
content_text += get_dom0_packages()
|
||||
content_text += get_log_file_content( qube_name )
|
||||
content_text += get_qube_packages( qube_name )
|
||||
|
||||
|
||||
report = template.format( title=title_text, content=content_text )
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def write_report( report_content, file_path ):
|
||||
with open( file_path, 'w' ) as report_file:
|
||||
report_file.write( report_content )
|
||||
|
||||
|
||||
def send_report( dest_qube, file_path):
|
||||
#if dest_qube is not running -> start dest_qube
|
||||
if not is_qube_running( dest_qube ):
|
||||
try:
|
||||
subprocess.check_call([ "qvm-start", dest_qube ])
|
||||
except subprocess.CalledProcessError:
|
||||
print( "Error while starting: " + dest_qube, file = sys.stderr )
|
||||
|
||||
try:
|
||||
subprocess.check_call([ "qvm-move-to-vm", dest_qube, file_path ])
|
||||
except subprocess.calledProcessError:
|
||||
print( "Moving file bug-report failed", file = sys.stderr )
|
||||
|
||||
|
||||
def get_log_file( log_file ):
|
||||
data = ""
|
||||
|
||||
#open and close the file
|
||||
with open( log_file ) as log:
|
||||
data = log.read()
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def qube_exist( qube_name ):
|
||||
exists = True
|
||||
|
||||
try:
|
||||
#calls: qvm-check --quiet vmanme
|
||||
subprocess.check_call([ "qvm-check", "--quiet", qube_name ])
|
||||
|
||||
except subprocess.CalledProcessError:
|
||||
exists = False
|
||||
|
||||
return exists
|
||||
|
||||
|
||||
def get_report_file_path( qube_name ):
|
||||
#exapanduser -> works corss platform
|
||||
home_dir = expanduser("~")
|
||||
date = time.strftime("%H%M%S")
|
||||
file_path = home_dir + "/" + qube_name + "_bug-report_" + date + ".md"
|
||||
|
||||
return file_path
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser( description = 'Generates a bug report for a specific qube (Qubes VM)' )
|
||||
parser.add_argument( 'vmname', metavar = '<vmanme>', type = str )
|
||||
parser.add_argument( '-d', '--dest-vm', metavar = '<dest-vm>', dest = "destvm", type = str, default = 'dom0', help = "send the report to the destination VM" )
|
||||
parser.add_argument( '-p', '--print-report', action = 'store_const', const = "print_report", required = False, help = "prints the report without writing it or sending it to a destination VM" )
|
||||
args = parser.parse_args()
|
||||
|
||||
if qube_exist( args.vmname ):
|
||||
|
||||
if qube_exist( args.destvm ):
|
||||
#get the report
|
||||
report_content = report( args.vmname )
|
||||
|
||||
#if -p or --print-report is an argument print the report
|
||||
if args.print_report:
|
||||
print( report_content )
|
||||
|
||||
#write and send the report
|
||||
else:
|
||||
file_path = get_report_file_path( args.vmname )
|
||||
write_report( report_content, file_path )
|
||||
print( "Report written to: " + file_path )
|
||||
|
||||
if 'dom0' != args.destvm:
|
||||
send_report( args.destvm, file_path )
|
||||
print( "Report send to VM: " + args.destvm )
|
||||
|
||||
exit(0)
|
||||
|
||||
else:
|
||||
print ( "Destination VM does not exist" )
|
||||
exit(1)
|
||||
|
||||
else:
|
||||
print( "VM does not exist" )
|
||||
exit(1)
|
||||
|
||||
|
||||
#calls the main function -> program start point
|
||||
main()
|
||||
@ -18,7 +18,7 @@
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
VERSION=2.4
|
||||
VERSION=2.5
|
||||
COPY2VM="dom0"
|
||||
SUPPORT_FILES=0
|
||||
|
||||
@ -101,9 +101,10 @@ XEN_EXTRA=`cat $TEMP_DIR/xl-info |grep xen_extra |cut -d: -f2 |tr -d ' '`
|
||||
QUBES=`cat $TEMP_DIR/qubes-release |cut -d '(' -f2 |cut -d ')' -f1`
|
||||
XL_VTX=`cat $TEMP_DIR/xl-info |grep xen_caps | grep hvm`
|
||||
XL_VTD=`cat $TEMP_DIR/xl-info |grep virt_caps |grep hvm_directio`
|
||||
XL_HAP=`cat $TEMP_DIR/xl-dmesg |grep 'HVM: Hardware Assisted Paging (HAP) detected'`
|
||||
PCRS=`find /sys/devices/ -name pcrs`
|
||||
|
||||
FILENAME="Qubes-HCL-${BRAND// /_}-${PRODUCT// /_}-$DATE"
|
||||
FILENAME="Qubes-HCL-${BRAND//[^[:alnum:]]/_}-${PRODUCT//[^[:alnum:]]/_}-$DATE"
|
||||
|
||||
if [[ $XL_VTX ]]
|
||||
then
|
||||
@ -127,6 +128,12 @@ if [[ $XL_VTD ]]
|
||||
|
||||
fi
|
||||
|
||||
if [ -n "$XL_HAP" ]; then
|
||||
HAP="yes"
|
||||
else
|
||||
HAP="no"
|
||||
fi
|
||||
|
||||
if [[ $PCRS ]]
|
||||
then
|
||||
# try tu run tcsd and: grep the logs, try get version info.
|
||||
@ -152,6 +159,7 @@ echo -e "Net:\n$NET\n"
|
||||
echo -e "SCSI:\n$SCSI\n"
|
||||
echo -e "HVM:\t\t$VTX"
|
||||
echo -e "I/O MMU:\t$VTD"
|
||||
echo -e "HAP/SLAT:\t${HAP^}"
|
||||
echo -e "TPM:\t\t$TPM"
|
||||
echo
|
||||
|
||||
@ -164,6 +172,8 @@ hvm:
|
||||
'$HVM'
|
||||
iommu:
|
||||
'$IOMMU'
|
||||
slat:
|
||||
'$HAP'
|
||||
tpm:
|
||||
'$TPM_s'
|
||||
brand: |
|
||||
@ -209,7 +219,7 @@ versions:
|
||||
FIXLINK
|
||||
|
||||
---
|
||||
" >> $HOME/$FILENAME.yml
|
||||
" >> "$HOME/$FILENAME.yml"
|
||||
|
||||
|
||||
if [[ "$SUPPORT_FILES" == 1 ]]
|
||||
@ -217,7 +227,7 @@ if [[ "$SUPPORT_FILES" == 1 ]]
|
||||
|
||||
# cpio
|
||||
cd $TEMP_DIR
|
||||
find -print0 |cpio --quiet -o -H crc --null |gzip >$HOME/$FILENAME.cpio.gz
|
||||
find -print0 | cpio --quiet -o -H crc --null | gzip > "$HOME/$FILENAME.cpio.gz"
|
||||
cd
|
||||
fi
|
||||
|
||||
@ -225,16 +235,16 @@ fi
|
||||
if [[ "$COPY2VM" != "dom0" ]]
|
||||
then
|
||||
# Copy to VM
|
||||
qvm-start -q $COPY2VM 2>/dev/null
|
||||
qvm-start -q $COPY2VM 2> /dev/null
|
||||
|
||||
if [[ -f "$HOME/$FILENAME.cpio.gz" ]]
|
||||
then
|
||||
cat $HOME/$FILENAME.cpio.gz | qvm-run -a -q --pass-io $COPY2VM "cat >/home/user/$FILENAME.cpio.gz"
|
||||
cat "$HOME/$FILENAME.cpio.gz" | qvm-run -a -q --pass-io $COPY2VM "cat > \"/home/user/$FILENAME.cpio.gz\""
|
||||
fi
|
||||
|
||||
if [[ -f "$HOME/$FILENAME.yml" ]]
|
||||
then
|
||||
cat $HOME/$FILENAME.yml | qvm-run -a -q --pass-io $COPY2VM "cat >/home/user/$FILENAME.yml"
|
||||
cat "$HOME/$FILENAME.yml" | qvm-run -a -q --pass-io $COPY2VM "cat > \"/home/user/$FILENAME.yml\""
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
@ -30,6 +30,7 @@ import qubes.backup
|
||||
import os
|
||||
import sys
|
||||
import getpass
|
||||
from locale import getpreferredencoding
|
||||
|
||||
def print_progress(progress):
|
||||
print >> sys.stderr, "\r-> Backing up files: {0}%...".format (progress),
|
||||
@ -40,46 +41,64 @@ def main():
|
||||
|
||||
parser.add_option ("-x", "--exclude", action="append",
|
||||
dest="exclude_list", default=[],
|
||||
help="Exclude the specified VM from backup (may be "
|
||||
help="Exclude the specified VM from the backup (may be "
|
||||
"repeated)")
|
||||
parser.add_option ("--force-root", action="store_true", dest="force_root", default=False,
|
||||
help="Force to run, even with root privileges")
|
||||
help="Force to run with root privileges")
|
||||
parser.add_option ("-d", "--dest-vm", action="store", dest="appvm",
|
||||
help="The AppVM to send backups to (implies -e)")
|
||||
help="Specify the destination VM to which the backup "
|
||||
"will be sent (implies -e)")
|
||||
parser.add_option ("-e", "--encrypt", action="store_true", dest="encrypt", default=False,
|
||||
help="Encrypts the backup")
|
||||
help="Encrypt the backup")
|
||||
parser.add_option ("--no-encrypt", action="store_true",
|
||||
dest="no_encrypt", default=False,
|
||||
help="Skip encryption even if sending the backup to VM")
|
||||
help="Skip encryption even if sending the backup to a "
|
||||
"VM")
|
||||
parser.add_option ("-p", "--passphrase-file", action="store",
|
||||
dest="pass_file", default=None,
|
||||
help="Read passphrase from a file, or use '-' to read "
|
||||
"from stdin")
|
||||
parser.add_option ("-E", "--enc-algo", action="store",
|
||||
dest="crypto_algorithm", default=None,
|
||||
help="Specify non-default encryption algorithm. For "
|
||||
"list of supported algos execute 'openssl "
|
||||
help="Specify a non-default encryption algorithm. For a "
|
||||
"list of supported algorithms, execute 'openssl "
|
||||
"list-cipher-algorithms' (implies -e)")
|
||||
parser.add_option ("-H", "--hmac-algo", action="store",
|
||||
dest="hmac_algorithm", default=None,
|
||||
help="Specify non-default hmac algorithm. For list of "
|
||||
"supported algos execute 'openssl "
|
||||
help="Specify a non-default HMAC algorithm. For a list "
|
||||
"of supported algorithms, execute 'openssl "
|
||||
"list-message-digest-algorithms'")
|
||||
parser.add_option ("-z", "--compress", action="store_true", dest="compress", default=False,
|
||||
help="Compress the backup")
|
||||
parser.add_option ("-Z", "--compress-filter", action="store",
|
||||
dest="compress_filter", default=False,
|
||||
help="Specify a non-default compression filter program "
|
||||
"(default: gzip)")
|
||||
parser.add_option("--tmpdir", action="store", dest="tmpdir", default=None,
|
||||
help="Specify a temporary directory (if you have at least "
|
||||
"1GB free RAM in dom0, use of /tmp is advised) ("
|
||||
"default: /var/tmp)")
|
||||
parser.add_option ("--debug", action="store_true", dest="debug",
|
||||
default=False, help="Enable (a lot of) debug output")
|
||||
|
||||
(options, args) = parser.parse_args ()
|
||||
|
||||
if (len (args) < 1):
|
||||
print >> sys.stderr, "You must specify the target backup directory (e.g. /mnt/backup)"
|
||||
print >> sys.stderr, "qvm-backup will create a subdirectory there for each individual backup."
|
||||
print >> sys.stderr, "You must specify the target backup directory "\
|
||||
" (e.g. /mnt/backup)."
|
||||
print >> sys.stderr, "qvm-backup will create a subdirectory there for "\
|
||||
" each individual backup."
|
||||
exit (0)
|
||||
|
||||
base_backup_dir = args[0]
|
||||
|
||||
if hasattr(os, "geteuid") and os.geteuid() == 0:
|
||||
if not options.force_root:
|
||||
print >> sys.stderr, "*** Running this tool as root is strongly discouraged, this will lead you in permissions problems."
|
||||
print >> sys.stderr, "Retry as unprivileged user."
|
||||
print >> sys.stderr, "... or use --force-root to continue anyway."
|
||||
print >> sys.stderr, "*** Running this tool as root is strongly "\
|
||||
"discouraged. This will lead to permissions "\
|
||||
"problems."
|
||||
print >> sys.stderr, "Retry as an unprivileged user, or use "\
|
||||
"--force-root to continue anyway."
|
||||
exit(1)
|
||||
|
||||
# Only for locking
|
||||
@ -123,14 +142,15 @@ def main():
|
||||
backup_fs_free_sz = stat.f_bsize * stat.f_bavail
|
||||
print
|
||||
if (total_backup_sz > backup_fs_free_sz):
|
||||
print >>sys.stderr, "ERROR: Not enough space available on the backup filesystem!"
|
||||
print >>sys.stderr, "ERROR: Not enough space available on the "\
|
||||
"backup filesystem!"
|
||||
exit(1)
|
||||
|
||||
print "-> Available space: {0}".format(size_to_human(backup_fs_free_sz))
|
||||
else:
|
||||
appvm = qvm_collection.get_vm_by_name(options.appvm)
|
||||
if appvm is None:
|
||||
print >>sys.stderr, "ERROR: VM {0} does not exist".format(options.appvm)
|
||||
print >>sys.stderr, "ERROR: VM {0} does not exist!".format(options.appvm)
|
||||
exit(1)
|
||||
|
||||
stat = os.statvfs('/var/tmp')
|
||||
@ -138,50 +158,54 @@ def main():
|
||||
print
|
||||
if (backup_fs_free_sz < 1000000000):
|
||||
print >>sys.stderr, "ERROR: Not enough space available " \
|
||||
"on the local filesystem (needs 1GB for temporary files)!"
|
||||
"on the local filesystem (1GB required for temporary files)!"
|
||||
exit(1)
|
||||
|
||||
if not appvm.is_running():
|
||||
appvm.start(verbose=True)
|
||||
|
||||
if options.appvm:
|
||||
print >>sys.stderr, ("WARNING: VM {} excluded because it's used to "
|
||||
"store the backup.").format(options.appvm)
|
||||
print >>sys.stderr, ("NOTE: VM {} will be excluded because it is "
|
||||
"the backup destination.").format(options.appvm)
|
||||
options.exclude_list.append(options.appvm)
|
||||
|
||||
if not options.encrypt:
|
||||
print >>sys.stderr, "WARNING: encryption will not be used"
|
||||
print >>sys.stderr, "WARNING: The backup will NOT be encrypted!"
|
||||
|
||||
prompt = raw_input ("Do you want to proceed? [y/N] ")
|
||||
if not (prompt == "y" or prompt == "Y"):
|
||||
exit (0)
|
||||
if options.pass_file is not None:
|
||||
f = open(options.pass_file) if options.pass_file != "-" else sys.stdin
|
||||
passphrase = f.readline().rstrip()
|
||||
if f is not sys.stdin:
|
||||
f.close()
|
||||
|
||||
if options.encrypt:
|
||||
passphrase = getpass.getpass("Please enter the pass phrase that will "
|
||||
"be used to encrypt and verify the "
|
||||
"backup: ")
|
||||
else:
|
||||
passphrase = getpass.getpass("Please enter the pass phrase that will "
|
||||
"be used to verify the backup: ")
|
||||
if raw_input("Do you want to proceed? [y/N] ").upper() != "Y":
|
||||
exit(0)
|
||||
|
||||
passphrase2 = getpass.getpass("Enter again for verification: ")
|
||||
if passphrase != passphrase2:
|
||||
print >>sys.stderr, "ERROR: Password mismatch"
|
||||
exit(1)
|
||||
s = ("Please enter the passphrase that will be used to {}verify "
|
||||
"the backup: ").format('encrypt and ' if options.encrypt else '')
|
||||
passphrase = getpass.getpass(s)
|
||||
|
||||
passphrase = passphrase.decode(sys.stdin.encoding)
|
||||
if getpass.getpass("Enter again for verification: ") != passphrase:
|
||||
print >>sys.stderr, "ERROR: Passphrase mismatch!"
|
||||
exit(1)
|
||||
|
||||
encoding = sys.stdin.encoding or getpreferredencoding()
|
||||
passphrase = passphrase.decode(encoding)
|
||||
|
||||
kwargs = {}
|
||||
if options.hmac_algorithm:
|
||||
kwargs['hmac_algorithm'] = options.hmac_algorithm
|
||||
if options.crypto_algorithm:
|
||||
kwargs['crypto_algorithm'] = options.crypto_algorithm
|
||||
if options.tmpdir:
|
||||
kwargs['tmpdir'] = options.tmpdir
|
||||
|
||||
try:
|
||||
backup_do(base_backup_dir, files_to_backup, passphrase,
|
||||
progress_callback=print_progress,
|
||||
encrypted=options.encrypt,
|
||||
compressed=options.compress,
|
||||
compressed=options.compress_filter or options.compress,
|
||||
appvm=appvm, **kwargs)
|
||||
except QubesException as e:
|
||||
print >>sys.stderr, "ERROR: %s" % str(e)
|
||||
|
||||
@ -31,6 +31,7 @@ from qubes.backup import backup_restore_do
|
||||
import qubes.backup
|
||||
import sys
|
||||
from optparse import OptionParser
|
||||
from locale import getpreferredencoding
|
||||
|
||||
import os
|
||||
import sys
|
||||
@ -42,23 +43,31 @@ def main():
|
||||
|
||||
parser.add_option ("--verify-only", action="store_true",
|
||||
dest="verify_only", default=False,
|
||||
help="Do not restore the data, only verify backup "
|
||||
"integrify.")
|
||||
help="Verify backup integrity without restoring any "
|
||||
"data")
|
||||
|
||||
parser.add_option ("--skip-broken", action="store_true", dest="skip_broken", default=False,
|
||||
help="Do not restore VMs that have missing templates or netvms")
|
||||
help="Do not restore VMs that have missing TemplateVMs "
|
||||
"or NetVMs")
|
||||
|
||||
parser.add_option ("--ignore-missing", action="store_true", dest="ignore_missing", default=False,
|
||||
help="Ignore missing templates or netvms, restore VMs anyway")
|
||||
help="Restore VMs even if their associated TemplateVMs "
|
||||
"and NetVMs are missing")
|
||||
|
||||
parser.add_option ("--skip-conflicting", action="store_true", dest="skip_conflicting", default=False,
|
||||
help="Do not restore VMs that are already present on the host")
|
||||
help="Do not restore VMs that are already present on "
|
||||
"the host")
|
||||
|
||||
parser.add_option ("--rename-conflicting", action="store_true",
|
||||
dest="rename_conflicting", default=False,
|
||||
help="Restore VMs that are already present on the host "
|
||||
"under different names")
|
||||
|
||||
parser.add_option ("--force-root", action="store_true", dest="force_root", default=False,
|
||||
help="Force to run, even with root privileges")
|
||||
help="Force to run with root privileges")
|
||||
|
||||
parser.add_option ("--replace-template", action="append", dest="replace_template", default=[],
|
||||
help="Restore VMs using another template, syntax: "
|
||||
help="Restore VMs using another TemplateVM; syntax: "
|
||||
"old-template-name:new-template-name (may be "
|
||||
"repeated)")
|
||||
|
||||
@ -66,17 +75,22 @@ def main():
|
||||
help="Skip restore of specified VM (may be repeated)")
|
||||
|
||||
parser.add_option ("--skip-dom0-home", action="store_false", dest="dom0_home", default=True,
|
||||
help="Do not restore dom0 user home dir")
|
||||
help="Do not restore dom0 user home directory")
|
||||
|
||||
parser.add_option ("--ignore-username-mismatch", action="store_true", dest="ignore_username_mismatch", default=False,
|
||||
help="Ignore dom0 username mismatch while restoring homedir")
|
||||
help="Ignore dom0 username mismatch when restoring home "
|
||||
"directory")
|
||||
|
||||
parser.add_option ("-d", "--dest-vm", action="store", dest="appvm",
|
||||
help="The AppVM to send backups to")
|
||||
help="Specify VM containing the backup to be restored")
|
||||
|
||||
parser.add_option ("-e", "--encrypted", action="store_true", dest="decrypt", default=False,
|
||||
help="The backup is encrypted")
|
||||
|
||||
parser.add_option ("-p", "--passphrase-file", action="store",
|
||||
dest="pass_file", default=None,
|
||||
help="Read passphrase from file, or use '-' to read from stdin")
|
||||
|
||||
parser.add_option ("-z", "--compressed", action="store_true", dest="compressed", default=False,
|
||||
help="The backup is compressed")
|
||||
|
||||
@ -86,7 +100,8 @@ def main():
|
||||
(options, args) = parser.parse_args ()
|
||||
|
||||
if (len (args) < 1):
|
||||
print >> sys.stderr, "You must specify the backup directory (e.g. /mnt/backup/qubes-2010-12-01-235959)"
|
||||
print >> sys.stderr, "You must specify the backup directory "\
|
||||
"(e.g. /mnt/backup/qubes-2010-12-01-235959)"
|
||||
exit (0)
|
||||
|
||||
backup_dir = args[0]
|
||||
@ -106,6 +121,8 @@ def main():
|
||||
restore_options['use-default-netvm'] = True
|
||||
if options.replace_template:
|
||||
restore_options['replace-template'] = options.replace_template
|
||||
if options.rename_conflicting:
|
||||
restore_options['rename-conflicting'] = True
|
||||
if not options.dom0_home:
|
||||
restore_options['dom0-home'] = False
|
||||
if options.ignore_username_mismatch:
|
||||
@ -124,8 +141,17 @@ def main():
|
||||
print >>sys.stderr, "ERROR: VM {0} does not exist".format(options.appvm)
|
||||
exit(1)
|
||||
|
||||
passphrase = getpass.getpass("Please enter the pass phrase that will be used to decrypt/verify the backup: ")
|
||||
passphrase = passphrase.decode(sys.stdin.encoding)
|
||||
if options.pass_file is not None:
|
||||
f = open(options.pass_file) if options.pass_file != "-" else sys.stdin
|
||||
passphrase = f.readline().rstrip()
|
||||
if f is not sys.stdin:
|
||||
f.close()
|
||||
else:
|
||||
passphrase = getpass.getpass("Please enter the passphrase to verify "
|
||||
"and (if encrypted) decrypt the backup: ")
|
||||
|
||||
encoding = sys.stdin.encoding or getpreferredencoding()
|
||||
passphrase = passphrase.decode(encoding)
|
||||
|
||||
print >> sys.stderr, "Checking backup content..."
|
||||
|
||||
@ -178,69 +204,97 @@ def main():
|
||||
print
|
||||
|
||||
if hasattr(os, "geteuid") and os.geteuid() == 0:
|
||||
print >> sys.stderr, "*** Running this tool as root is strongly discouraged, this will lead you in permissions problems."
|
||||
print >> sys.stderr, "*** Running this tool as root is strongly "\
|
||||
"discouraged. This will lead to permissions "\
|
||||
"problems."
|
||||
if options.force_root:
|
||||
print >> sys.stderr, "Continuing as commanded. You have been warned."
|
||||
print >> sys.stderr, "Continuing as commanded. You have been "\
|
||||
"warned."
|
||||
else:
|
||||
print >> sys.stderr, "Retry as unprivileged user."
|
||||
print >> sys.stderr, "... or use --force-root to continue anyway."
|
||||
print >> sys.stderr, "Retry as an unprivileged user, or use "\
|
||||
"--force-root to continue anyway."
|
||||
exit(1)
|
||||
|
||||
if there_are_conflicting_vms:
|
||||
print >> sys.stderr, "*** There VMs with conflicting names on the host! ***"
|
||||
print >> sys.stderr, "*** There are VMs with conflicting names on the "\
|
||||
"host! ***"
|
||||
if options.skip_conflicting:
|
||||
print >> sys.stderr, "Those VMs will not be restored, the host VMs will not be overwritten!"
|
||||
print >> sys.stderr, "Those VMs will not be restored. The host "\
|
||||
"VMs will NOT be overwritten."
|
||||
else:
|
||||
print >> sys.stderr, "Remove VMs with conflicting names from the host before proceeding."
|
||||
print >> sys.stderr, "... or use --skip-conflicting to restore only those VMs that do not exist on the host."
|
||||
print >> sys.stderr, "Remove VMs with conflicting names from the "\
|
||||
"host before proceeding."
|
||||
print >> sys.stderr, "Or use --skip-conflicting to restore only "\
|
||||
"those VMs that do not exist on the host."
|
||||
print >> sys.stderr, "Or use --rename-conflicting to restore " \
|
||||
"those VMs under modified names (with "\
|
||||
"numbers at the end)."
|
||||
exit (1)
|
||||
|
||||
print "The above VMs will be copied and added to your system."
|
||||
print "Exisiting VMs will not be removed."
|
||||
print "Exisiting VMs will NOT be removed."
|
||||
|
||||
if there_are_missing_templates:
|
||||
print >> sys.stderr, "*** One or more template VM is missing on the host! ***"
|
||||
print >> sys.stderr, "*** One or more TemplateVMs are missing on the"\
|
||||
"host! ***"
|
||||
if not (options.skip_broken or options.ignore_missing):
|
||||
print >> sys.stderr, "Install it first, before proceeding with backup restore."
|
||||
print >> sys.stderr, "Or pass: --skip-broken or --ignore-missing switch."
|
||||
print >> sys.stderr, "Install them before proceeding with the "\
|
||||
"restore."
|
||||
print >> sys.stderr, "Or pass: --skip-broken or --ignore-missing."
|
||||
exit (1)
|
||||
elif options.skip_broken:
|
||||
print >> sys.stderr, "... VMs that depend on it will not be restored (--skip-broken used)"
|
||||
print >> sys.stderr, "Skipping broken entries: VMs that depend on "\
|
||||
"missing TemplateVMs will NOT be restored."
|
||||
elif options.ignore_missing:
|
||||
print >> sys.stderr, "... VMs that depend on it will be restored anyway (--ignore-missing used)"
|
||||
print >> sys.stderr, "Ignoring missing entries: VMs that depend "\
|
||||
"on missing TemplateVMs will NOT be restored."
|
||||
else:
|
||||
print >> sys.stderr, "INTERNAL ERROR?!"
|
||||
print >> sys.stderr, "INTERNAL ERROR! Please report this to the "\
|
||||
"Qubes OS team!"
|
||||
exit (1)
|
||||
|
||||
if there_are_missing_netvms:
|
||||
print >> sys.stderr, "*** One or more network VM is missing on the host! ***"
|
||||
print >> sys.stderr, "*** One or more NetVMs are missing on the "\
|
||||
"host! ***"
|
||||
if not (options.skip_broken or options.ignore_missing):
|
||||
print >> sys.stderr, "Install it first, before proceeding with backup restore."
|
||||
print >> sys.stderr, "Or pass: --skip_broken or --ignore_missing switch."
|
||||
print >> sys.stderr, "Install them before proceeding with the "\
|
||||
"restore."
|
||||
print >> sys.stderr, "Or pass: --skip-broken or --ignore-missing."
|
||||
exit (1)
|
||||
elif options.skip_broken:
|
||||
print >> sys.stderr, "... VMs that depend on it will not be restored (--skip-broken used)"
|
||||
print >> sys.stderr, "Skipping broken entries: VMs that depend on "\
|
||||
"missing NetVMs will NOT be restored."
|
||||
elif options.ignore_missing:
|
||||
print >> sys.stderr, "... VMs that depend on it be restored anyway (--ignore-missing used)"
|
||||
print >> sys.stderr, "Ignoring missing entries: VMs that depend "\
|
||||
"on missing NetVMs will NOT be restored."
|
||||
else:
|
||||
print >> sys.stderr, "INTERNAL ERROR?!"
|
||||
print >> sys.stderr, "INTERNAL ERROR! Please report this to the "\
|
||||
"Qubes OS team!"
|
||||
exit (1)
|
||||
|
||||
if 'dom0' in restore_info.keys() and options.dom0_home:
|
||||
if dom0_username_mismatch:
|
||||
print >> sys.stderr, "*** Dom0 username mismatch! This can break some settings ***"
|
||||
print >> sys.stderr, "*** Dom0 username mismatch! This can break "\
|
||||
"some settings! ***"
|
||||
if not options.ignore_username_mismatch:
|
||||
print >> sys.stderr, "Skip dom0 home restore (--skip-dom0-home)"
|
||||
print >> sys.stderr, "Or pass: --ignore-username-mismatch to continue anyway"
|
||||
print >> sys.stderr, "Skip restoring the dom0 home directory "\
|
||||
"(--skip-dom0-home), or pass "\
|
||||
"--ignore-username-mismatch to continue "\
|
||||
"anyway."
|
||||
exit(1)
|
||||
else:
|
||||
print >> sys.stderr, "Continuing as directed"
|
||||
print >> sys.stderr, "While restoring user homedir, existing files/dirs will be backed up in 'home-pre-restore-<current-time>' dir"
|
||||
|
||||
prompt = raw_input ("Do you want to proceed? [y/N] ")
|
||||
if not (prompt == "y" or prompt == "Y"):
|
||||
exit (0)
|
||||
print >> sys.stderr, "Continuing as directed."
|
||||
print >> sys.stderr, "NOTE: Before restoring the dom0 home directory, "\
|
||||
"a new directory named "\
|
||||
"'home-pre-restore-<current-time>' will be "\
|
||||
"created inside the dom0 home directory. If any "\
|
||||
"restored files conflict with existing files, "\
|
||||
"the existing files will be moved to this new "\
|
||||
"directory."
|
||||
|
||||
if options.pass_file is None:
|
||||
if raw_input("Do you want to proceed? [y/N] ").upper() != "Y":
|
||||
exit(0)
|
||||
|
||||
try:
|
||||
backup_restore_do(restore_info,
|
||||
|
||||
@ -27,11 +27,18 @@ import sys
|
||||
import time
|
||||
|
||||
def main():
|
||||
usage = "usage: %prog [options] <vm-name>"
|
||||
usage = """usage: %prog [options] <vm-name>\n
|
||||
Specify no state options to check if VM exists"""
|
||||
parser = OptionParser (usage)
|
||||
|
||||
parser.add_option ("-q", "--quiet", action="store_false", dest="verbose", default=True)
|
||||
|
||||
parser.add_option ("--running", action="store_true", dest="running", default=False,
|
||||
help="Determine if VM is running")
|
||||
parser.add_option ("--paused", action="store_true", dest="paused", default=False,
|
||||
help="Determine if VM is paused")
|
||||
parser.add_option ("--template", action="store_true", dest="template", default=False,
|
||||
help="Determine if VM is a template")
|
||||
|
||||
(options, args) = parser.parse_args ()
|
||||
if (len (args) != 1):
|
||||
parser.error ("You must specify VM name!")
|
||||
@ -48,8 +55,26 @@ def main():
|
||||
print >> sys.stdout, "A VM with the name '{0}' does not exist in the system!".format(vmname)
|
||||
exit(1)
|
||||
|
||||
elif options.running:
|
||||
vm_state = not vm.is_running()
|
||||
if options.verbose:
|
||||
print >> sys.stdout, "A VM with the name {0} is {1}running.".format(vmname, "not " * vm_state)
|
||||
exit(vm_state)
|
||||
|
||||
elif options.paused:
|
||||
vm_state = not vm.is_paused()
|
||||
if options.verbose:
|
||||
print >> sys.stdout, "A VM with the name {0} is {1}paused.".format(vmname, "not " * vm_state)
|
||||
exit(vm_state)
|
||||
|
||||
elif options.template:
|
||||
vm_state = not vm.is_template()
|
||||
if options.verbose:
|
||||
print >> sys.stdout, "A VM with the name {0} is {1}a template.".format(vmname, "not " * vm_state)
|
||||
exit(vm_state)
|
||||
|
||||
else:
|
||||
if options.verbose:
|
||||
if options.verbose:
|
||||
print >> sys.stdout, "A VM with the name '{0}' does exist.".format(vmname)
|
||||
exit(0)
|
||||
|
||||
|
||||
@ -17,37 +17,43 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
#
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
|
||||
# USA.
|
||||
|
||||
import os
|
||||
import sys
|
||||
from optparse import OptionParser
|
||||
|
||||
from qubes.qubes import QubesVmCollection
|
||||
from qubes.qubes import QubesAppVm, QubesTemplateVm, QubesHVm
|
||||
from qubes.qubes import QubesException
|
||||
from optparse import OptionParser;
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
def main():
|
||||
usage = "usage: %prog [options] <src-name> <new-name>\n"\
|
||||
"Clones an existing VM by copying all its disk files"
|
||||
|
||||
parser = OptionParser (usage)
|
||||
parser.add_option ("-q", "--quiet", action="store_false", dest="verbose", default=True)
|
||||
parser.add_option ("-p", "--path", dest="dir_path",
|
||||
help="Specify path to the template directory")
|
||||
parser.add_option ("--force-root", action="store_true", dest="force_root", default=False,
|
||||
help="Force to run, even with root privileges")
|
||||
|
||||
(options, args) = parser.parse_args ()
|
||||
if (len (args) != 2):
|
||||
parser.error ("You must specify at least the src and dst TemplateVM names!")
|
||||
parser = OptionParser(usage)
|
||||
parser.add_option("-q", "--quiet", action="store_false", dest="verbose",
|
||||
default=True)
|
||||
parser.add_option("-p", "--path", dest="dir_path",
|
||||
help="Specify path to the template directory")
|
||||
parser.add_option("--force-root", action="store_true", dest="force_root",
|
||||
default=False,
|
||||
help="Force to run, even with root privileges")
|
||||
parser.add_option("-P", "--pool", dest="pool_name",
|
||||
help="Specify in to which storage pool to clone")
|
||||
|
||||
(options, args) = parser.parse_args()
|
||||
if (len(args) != 2):
|
||||
parser.error(
|
||||
"You must specify at least the src and dst TemplateVM names!")
|
||||
srcname = args[0]
|
||||
dstname = args[1]
|
||||
|
||||
if hasattr(os, "geteuid") and os.geteuid() == 0:
|
||||
if not options.force_root:
|
||||
print >> sys.stderr, "*** Running this tool as root is strongly discouraged, this will lead you in permissions problems."
|
||||
print >> sys.stderr, "*** Running this tool as root is" + \
|
||||
" strongly discouraged, this will lead you in permissions" + \
|
||||
"problems."
|
||||
print >> sys.stderr, "Retry as unprivileged user."
|
||||
print >> sys.stderr, "... or use --force-root to continue anyway."
|
||||
exit(1)
|
||||
@ -57,12 +63,21 @@ def main():
|
||||
qvm_collection.load()
|
||||
|
||||
src_vm = qvm_collection.get_vm_by_name(srcname)
|
||||
if src_vm is None:
|
||||
print >> sys.stderr, "ERROR: A VM with the name '{0}' does not exist in the system.".format(srcname)
|
||||
if src_vm is None:
|
||||
print >> sys.stderr, \
|
||||
"ERROR: A VM with the name '{0}' does not exist in the system." \
|
||||
.format(srcname)
|
||||
exit(1)
|
||||
|
||||
if options.pool_name is None:
|
||||
pool_name = src_vm.pool_name
|
||||
else:
|
||||
pool_name = options.pool_name
|
||||
|
||||
if qvm_collection.get_vm_by_name(dstname) is not None:
|
||||
print >> sys.stderr, "ERROR: A VM with the name '{0}' already exists in the system.".format(dstname)
|
||||
print >> sys.stderr, \
|
||||
"ERROR: A VM with the name '{0}' already exists in the system." \
|
||||
.format(dstname)
|
||||
exit(1)
|
||||
|
||||
if src_vm.is_disposablevm():
|
||||
@ -70,19 +85,21 @@ def main():
|
||||
exit(1)
|
||||
|
||||
dst_vm = qvm_collection.add_new_vm(src_vm.__class__.__name__,
|
||||
name=dstname, template=src_vm.template,
|
||||
dir_path=options.dir_path, installed_by_rpm=False)
|
||||
name=dstname, template=src_vm.template,
|
||||
pool_name=pool_name,
|
||||
dir_path=options.dir_path,
|
||||
installed_by_rpm=False)
|
||||
|
||||
try:
|
||||
dst_vm.clone_attrs(src_vm)
|
||||
dst_vm.clone_disk_files (src_vm=src_vm, verbose=options.verbose)
|
||||
dst_vm.clone_disk_files(src_vm=src_vm, verbose=options.verbose)
|
||||
except (IOError, OSError) as err:
|
||||
print >> sys.stderr, "ERROR: {0}".format(err)
|
||||
qvm_collection.pop(dst_vm.qid)
|
||||
dst_vm.remove_from_disk()
|
||||
exit (1)
|
||||
exit(1)
|
||||
|
||||
qvm_collection.save()
|
||||
qvm_collection.unlock_db()
|
||||
|
||||
|
||||
main()
|
||||
|
||||
@ -41,6 +41,8 @@ def main():
|
||||
help="Specify the label to use for the new VM (e.g. red, yellow, green, ...)")
|
||||
parser.add_option ("-p", "--proxy", action="store_true", dest="proxyvm", default=False,
|
||||
help="Create ProxyVM")
|
||||
parser.add_option ("-P", "--pool", dest="pool_name",
|
||||
help="Specify which storage pool to use")
|
||||
parser.add_option ("-H", "--hvm", action="store_true", dest="hvm", default=False,
|
||||
help="Create HVM (standalone unless --template option used)")
|
||||
parser.add_option ("--hvm-template", action="store_true", dest="hvm_template", default=False,
|
||||
@ -71,6 +73,11 @@ def main():
|
||||
parser.error ("You must specify VM name!")
|
||||
vmname = args[0]
|
||||
|
||||
if options.pool_name is None:
|
||||
pool_name = "default"
|
||||
else :
|
||||
pool_name = options.pool_name
|
||||
|
||||
if (options.netvm + options.proxyvm + options.hvm + options.hvm_template) > 1:
|
||||
parser.error ("You must specify at most one VM type switch")
|
||||
|
||||
@ -123,6 +130,10 @@ def main():
|
||||
if options.offline_mode:
|
||||
vmm.offline_mode = True
|
||||
|
||||
if re.match('^disp\d+$', vmname):
|
||||
print >> sys.stderr, 'The name "{0}" is reserved for internal use.'.format(vmname)
|
||||
exit(1)
|
||||
|
||||
qvm_collection = QubesVmCollection()
|
||||
qvm_collection.lock_db_for_writing()
|
||||
qvm_collection.load()
|
||||
@ -170,7 +181,9 @@ def main():
|
||||
vmtype = "QubesAppVm"
|
||||
|
||||
try:
|
||||
vm = qvm_collection.add_new_vm(vmtype, name=vmname, template=new_vm_template, label = label)
|
||||
vm=qvm_collection.add_new_vm(vmtype, name=vmname,
|
||||
template=new_vm_template, label=label,
|
||||
pool_name=pool_name)
|
||||
except QubesException as err:
|
||||
print >> sys.stderr, "ERROR: {0}".format(err)
|
||||
exit (1)
|
||||
|
||||
@ -34,6 +34,10 @@ def main():
|
||||
usage = "usage: %prog <vm-name> <size>"
|
||||
parser = OptionParser (usage)
|
||||
|
||||
parser.add_option("--allow-start", action="store_true",
|
||||
dest="allow_start", default=False,
|
||||
help="Allow VM to be started to complete the operation")
|
||||
|
||||
(options, args) = parser.parse_args ()
|
||||
if (len (args) != 2):
|
||||
parser.error ("You must specify VM name and new size!")
|
||||
@ -57,7 +61,7 @@ def main():
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
vm.resize_root_img(size_bytes)
|
||||
vm.resize_root_img(size_bytes, allow_start=options.allow_start)
|
||||
except (IOError, OSError, QubesException) as err:
|
||||
print >> sys.stderr, "ERROR: {0}".format(err)
|
||||
exit (1)
|
||||
|
||||
195
qvm-tools/qvm-ls
195
qvm-tools/qvm-ls
@ -24,7 +24,7 @@
|
||||
from qubes.qubes import QubesVmCollection
|
||||
from qubes.qubes import QubesHost
|
||||
from qubes.qubes import QubesException
|
||||
from optparse import OptionParser
|
||||
from argparse import ArgumentParser
|
||||
import sys
|
||||
|
||||
|
||||
@ -91,119 +91,140 @@ fields = {
|
||||
|
||||
|
||||
def main():
|
||||
usage = "usage: %prog [options] <vm-name>"
|
||||
parser = OptionParser (usage)
|
||||
usage = "%(prog)s [options]"
|
||||
parser = ArgumentParser ()
|
||||
|
||||
parser.add_option ("-n", "--network", dest="network",
|
||||
parser.add_argument ("VMs", action="store", nargs="*",
|
||||
help="Specify VMs to be queried")
|
||||
|
||||
parser.add_argument ("-n", "--network", dest="network",
|
||||
action="store_true", default=False,
|
||||
help="Show network addresses assigned to VMs")
|
||||
|
||||
parser.add_option ("-c", "--cpu", dest="cpu",
|
||||
parser.add_argument ("-c", "--cpu", dest="cpu",
|
||||
action="store_true", default=False,
|
||||
help="Show CPU load")
|
||||
|
||||
parser.add_option ("-m", "--mem", dest="mem",
|
||||
parser.add_argument ("-m", "--mem", dest="mem",
|
||||
action="store_true", default=False,
|
||||
help="Show memory usage")
|
||||
|
||||
parser.add_option ("-d", "--disk", dest="disk",
|
||||
parser.add_argument ("-d", "--disk", dest="disk",
|
||||
action="store_true", default=False,
|
||||
help="Show VM disk utilization statistics")
|
||||
|
||||
parser.add_option ("-k", "--kernel", dest="kernel",
|
||||
parser.add_argument ("-k", "--kernel", dest="kernel",
|
||||
action="store_true", default=False,
|
||||
help="Show VM kernel options")
|
||||
help="Show VM kernel arguments")
|
||||
|
||||
parser.add_option ("-i", "--ids", dest="ids",
|
||||
parser.add_argument ("-i", "--ids", dest="ids",
|
||||
action="store_true", default=False,
|
||||
help="Show Qubes and Xen id#s")
|
||||
|
||||
parser.add_option("-b", "--last-backup", dest="backup",
|
||||
parser.add_argument("-b", "--last-backup", dest="backup",
|
||||
action="store_true", default=False,
|
||||
help="Show date of last VM backup")
|
||||
|
||||
parser.add_option("--raw-list", dest="raw_list",
|
||||
parser.add_argument("--raw-list", dest="raw_list",
|
||||
action="store_true", default=False,
|
||||
help="List only VM names one per line")
|
||||
|
||||
parser.add_argument("--raw-data", dest="raw_data",
|
||||
action="store", nargs="+",
|
||||
help="Display specify data of specified VMs.\
|
||||
Intended for bash-parsing.")
|
||||
|
||||
(options, args) = parser.parse_args ()
|
||||
|
||||
arguments = parser.parse_args ()
|
||||
|
||||
qvm_collection = QubesVmCollection()
|
||||
qvm_collection.lock_db_for_reading()
|
||||
qvm_collection.load()
|
||||
qvm_collection.unlock_db()
|
||||
|
||||
if options.raw_list:
|
||||
if arguments.raw_list:
|
||||
for vm in qvm_collection.values():
|
||||
print vm.name
|
||||
return
|
||||
|
||||
fields_to_display = ["name", "on", "state", "updbl", "type", "template", "netvm", "label" ]
|
||||
|
||||
cpu_usages = None
|
||||
|
||||
if (options.ids):
|
||||
fields_to_display += ["qid", "xid"]
|
||||
if arguments.raw_data:
|
||||
fields_to_display = arguments.raw_data
|
||||
if 'cpu' in arguments.raw_data:
|
||||
qhost = QubesHost()
|
||||
(measure_time, cpu_usages) = qhost.measure_cpu_usage(qvm_collection)
|
||||
else:
|
||||
fields_to_display = ["name", "on", "state", "updbl", "type", "template", "netvm", "label" ]
|
||||
|
||||
if (options.cpu):
|
||||
qhost = QubesHost()
|
||||
(measure_time, cpu_usages) = qhost.measure_cpu_usage()
|
||||
fields_to_display += ["cpu"]
|
||||
if (arguments.ids):
|
||||
fields_to_display += ["qid", "xid"]
|
||||
|
||||
if (options.mem):
|
||||
fields_to_display += ["mem"]
|
||||
if (arguments.cpu):
|
||||
qhost = QubesHost()
|
||||
(measure_time, cpu_usages) = qhost.measure_cpu_usage(qvm_collection)
|
||||
fields_to_display += ["cpu"]
|
||||
|
||||
if options.backup:
|
||||
fields_to_display += ["last backup"]
|
||||
if (arguments.mem):
|
||||
fields_to_display += ["mem"]
|
||||
|
||||
if (options.network):
|
||||
if 'template' in fields_to_display:
|
||||
fields_to_display.remove ("template")
|
||||
fields_to_display += ["ip", "ip back", "gateway/DNS"]
|
||||
if arguments.backup:
|
||||
fields_to_display += ["last backup"]
|
||||
|
||||
if (options.disk):
|
||||
if 'template' in fields_to_display:
|
||||
fields_to_display.remove ("template")
|
||||
if 'netvm' in fields_to_display:
|
||||
fields_to_display.remove ("netvm")
|
||||
fields_to_display += ["priv-curr", "priv-max", "root-curr", "root-max", "disk" ]
|
||||
if (arguments.network):
|
||||
if 'template' in fields_to_display:
|
||||
fields_to_display.remove ("template")
|
||||
fields_to_display += ["ip", "ip back", "gateway/DNS"]
|
||||
|
||||
if (options.kernel):
|
||||
fields_to_display += ["kernel", "kernelopts" ]
|
||||
if (arguments.disk):
|
||||
if 'template' in fields_to_display:
|
||||
fields_to_display.remove ("template")
|
||||
if 'netvm' in fields_to_display:
|
||||
fields_to_display.remove ("netvm")
|
||||
fields_to_display += ["priv-curr", "priv-max", "root-curr", "root-max", "disk" ]
|
||||
|
||||
if (arguments.kernel):
|
||||
fields_to_display += ["kernel", "kernelopts" ]
|
||||
|
||||
|
||||
vms_list = [vm for vm in qvm_collection.values()]
|
||||
if len(args) > 0:
|
||||
vms_list = [vm for vm in vms_list if vm.name in args]
|
||||
no_vms = len (vms_list)
|
||||
vms_to_display = []
|
||||
# Frist, the NetVMs...
|
||||
for netvm in vms_list:
|
||||
if netvm.is_netvm():
|
||||
vms_to_display.append (netvm)
|
||||
#assume VMs are presented in desired order:
|
||||
if len(arguments.VMs) > 0:
|
||||
vms_to_display = [vm for vm in vms_list if vm.name in arguments.VMs]
|
||||
#otherwise, format them accordingly:
|
||||
else:
|
||||
no_vms = len (vms_list)
|
||||
vms_to_display = []
|
||||
# Frist, the NetVMs...
|
||||
for netvm in vms_list:
|
||||
if netvm.is_netvm():
|
||||
vms_to_display.append (netvm)
|
||||
|
||||
# Now, the AppVMs without template (or with template not included in the list)...
|
||||
for appvm in vms_list:
|
||||
if appvm.is_appvm() and not appvm.is_template() and \
|
||||
(appvm.template is None or appvm.template not in vms_list):
|
||||
vms_to_display.append (appvm)
|
||||
# Now, the AppVMs without template (or with template not included in the list)...
|
||||
for appvm in vms_list:
|
||||
if appvm.is_appvm() and not appvm.is_template() and \
|
||||
(appvm.template is None or appvm.template not in vms_list):
|
||||
vms_to_display.append (appvm)
|
||||
|
||||
# Now, the template, and all its AppVMs...
|
||||
for tvm in vms_list:
|
||||
if tvm.is_template():
|
||||
vms_to_display.append (tvm)
|
||||
for vm in vms_list:
|
||||
if (vm.is_appvm() or vm.is_disposablevm()) and \
|
||||
vm.template and vm.template.qid == tvm.qid:
|
||||
vms_to_display.append(vm)
|
||||
# Now, the template, and all its AppVMs...
|
||||
for tvm in vms_list:
|
||||
if tvm.is_template():
|
||||
vms_to_display.append (tvm)
|
||||
for vm in vms_list:
|
||||
if (vm.is_appvm() or vm.is_disposablevm()) and \
|
||||
vm.template and vm.template.qid == tvm.qid:
|
||||
vms_to_display.append(vm)
|
||||
|
||||
assert len(vms_to_display) == no_vms
|
||||
assert len(vms_to_display) == no_vms
|
||||
|
||||
#We DON'T NEED a max_width if we devide output by pipes!
|
||||
|
||||
# First calculate the maximum width of each field we want to display
|
||||
# also collect data to display
|
||||
for f in fields_to_display:
|
||||
fields[f]["max_width"] = len(f)
|
||||
|
||||
|
||||
data_to_display = []
|
||||
for vm in vms_to_display:
|
||||
data_row = {}
|
||||
@ -213,42 +234,44 @@ def main():
|
||||
else:
|
||||
data_row[f] = str(eval(fields[f]["func"]))
|
||||
l = len(data_row[f])
|
||||
if l > fields[f]["max_width"]:
|
||||
if 'max_width' in fields[f] and l > fields[f]["max_width"]:
|
||||
fields[f]["max_width"] = l
|
||||
data_to_display.append(data_row)
|
||||
try:
|
||||
vm.verify_files()
|
||||
except QubesException as err:
|
||||
print >> sys.stderr, "WARNING: VM '{0}' has corrupted files!".format(vm.name)
|
||||
|
||||
# XXX: For what?
|
||||
total_width = 0;
|
||||
for f in fields_to_display:
|
||||
total_width += fields[f]["max_width"]
|
||||
|
||||
# Display the header
|
||||
s = ""
|
||||
for f in fields_to_display:
|
||||
fmt="{{0:-^{0}}}-+".format(fields[f]["max_width"] + 1)
|
||||
s += fmt.format('-')
|
||||
print s
|
||||
s = ""
|
||||
for f in fields_to_display:
|
||||
fmt="{{0:>{0}}} |".format(fields[f]["max_width"] + 1)
|
||||
s += fmt.format(f)
|
||||
print s
|
||||
s = ""
|
||||
for f in fields_to_display:
|
||||
fmt="{{0:-^{0}}}-+".format(fields[f]["max_width"] + 1)
|
||||
s += fmt.format('-')
|
||||
print s
|
||||
|
||||
# ... and the actual data
|
||||
for row in data_to_display:
|
||||
#Nicely formatted header only needed for humans
|
||||
if not arguments.raw_data:
|
||||
# Display the header
|
||||
s = ""
|
||||
for f in fields_to_display:
|
||||
fmt="{{0:-^{0}}}-+".format(fields[f]["max_width"] + 1)
|
||||
s += fmt.format('-')
|
||||
print s
|
||||
s = ""
|
||||
for f in fields_to_display:
|
||||
fmt="{{0:>{0}}} |".format(fields[f]["max_width"] + 1)
|
||||
s += fmt.format(row[f])
|
||||
s += fmt.format(f)
|
||||
print s
|
||||
s = ""
|
||||
for f in fields_to_display:
|
||||
fmt="{{0:-^{0}}}-+".format(fields[f]["max_width"] + 1)
|
||||
s += fmt.format('-')
|
||||
print s
|
||||
|
||||
# ... and the actual data
|
||||
for row in data_to_display:
|
||||
s = ""
|
||||
for f in fields_to_display:
|
||||
fmt="{{0:>{0}}} |".format(fields[f]["max_width"] + 1)
|
||||
s += fmt.format(row[f])
|
||||
print s
|
||||
|
||||
#won't look pretty, but is easy to parse!
|
||||
else:
|
||||
for row in data_to_display:
|
||||
print '|'.join([row[f] for f in fields_to_display])
|
||||
|
||||
main()
|
||||
|
||||
@ -27,6 +27,25 @@ import subprocess
|
||||
import os
|
||||
import sys
|
||||
from qubes.qubes import vmm
|
||||
import re
|
||||
|
||||
|
||||
def find_devices_of_class(klass):
|
||||
p = subprocess.Popen(["/sbin/lspci", "-mm", "-n"], stdout=subprocess.PIPE)
|
||||
result = p.communicate()
|
||||
retcode = p.returncode
|
||||
if retcode != 0:
|
||||
print "ERROR when executing lspci!"
|
||||
raise IOError
|
||||
|
||||
rx_netdev = re.compile(r"^([0-9a-f]{2}:[0-9a-f]{2}.[0-9a-f]) \"" +
|
||||
klass)
|
||||
for dev in str(result[0]).splitlines():
|
||||
match = rx_netdev.match(dev)
|
||||
if match is not None:
|
||||
dev_bdf = match.group(1)
|
||||
assert dev_bdf is not None
|
||||
yield dev_bdf
|
||||
|
||||
|
||||
def main():
|
||||
@ -39,6 +58,9 @@ def main():
|
||||
parser.add_option ("-l", "--list", action="store_true", dest="do_list", default=False)
|
||||
parser.add_option ("-a", "--add", action="store_true", dest="do_add", default=False)
|
||||
parser.add_option ("-d", "--delete", action="store_true", dest="do_delete", default=False)
|
||||
parser.add_option("-C", "--add-class", action="store_true",
|
||||
dest="do_add_class", default=False,
|
||||
help="Add all devices of given class (net, usb)")
|
||||
parser.add_option ("--offline-mode", dest="offline_mode",
|
||||
action="store_true", default=False,
|
||||
help="Offline mode")
|
||||
@ -49,14 +71,15 @@ def main():
|
||||
|
||||
vmname = args[0]
|
||||
|
||||
if options.do_list + options.do_add + options.do_delete > 1:
|
||||
print >> sys.stderr, "Only one of -l -a -d is allowed!"
|
||||
exit (1)
|
||||
if options.do_list + options.do_add + options.do_delete + \
|
||||
options.do_add_class > 1:
|
||||
print >> sys.stderr, "Only one of -l -a -d -C is allowed!"
|
||||
exit(1)
|
||||
|
||||
if options.offline_mode:
|
||||
vmm.offline_mode = True
|
||||
|
||||
if options.do_add or options.do_delete:
|
||||
if options.do_add or options.do_delete or options.do_add_class:
|
||||
qvm_collection = QubesVmCollection()
|
||||
qvm_collection.lock_db_for_writing()
|
||||
qvm_collection.load()
|
||||
@ -81,6 +104,26 @@ def main():
|
||||
qvm_collection.save()
|
||||
qvm_collection.unlock_db()
|
||||
|
||||
elif options.do_add_class:
|
||||
if len(args) < 2:
|
||||
print >> sys.stderr, "You must specify the PCI device class to add"
|
||||
exit(1)
|
||||
|
||||
klass = args[1]
|
||||
|
||||
if klass == 'net':
|
||||
devs = find_devices_of_class("02")
|
||||
elif klass == 'usb':
|
||||
devs = find_devices_of_class("0c03")
|
||||
else:
|
||||
print >> sys.stderr, "Supported classes: net, usb"
|
||||
exit(1)
|
||||
|
||||
for dev in devs:
|
||||
vm.pci_add(dev)
|
||||
qvm_collection.save()
|
||||
qvm_collection.unlock_db()
|
||||
|
||||
elif options.do_delete:
|
||||
if len (args) < 2:
|
||||
print >> sys.stderr, "You must specify the PCI device to delete"
|
||||
|
||||
@ -58,6 +58,7 @@ def do_list(vm):
|
||||
print fmt.format ("config", vm.conf_file)
|
||||
print fmt.format ("pcidevs", vm.pcidevs)
|
||||
print fmt.format ("pci_strictreset", vm.pci_strictreset)
|
||||
print fmt.format ("pci_e820_host", vm.pci_e820_host)
|
||||
if vm.template is None:
|
||||
print fmt.format ("root_img", vm.root_img)
|
||||
if hasattr(vm, "rootcow_img") and vm.rootcow_img is not None:
|
||||
@ -228,6 +229,14 @@ def set_pci_strictreset(vms, vm, args):
|
||||
vm.pci_strictreset = bool(eval(args[0].capitalize()))
|
||||
return True
|
||||
|
||||
def set_pci_e820_host(vms, vm, args):
|
||||
if len (args) != 1:
|
||||
print >> sys.stderr, "Missing value (True/False)!"
|
||||
return False
|
||||
|
||||
vm.pci_e820_host = bool(eval(args[0].capitalize()))
|
||||
return True
|
||||
|
||||
def set_netvm(vms, vm, args):
|
||||
if len (args) != 1:
|
||||
print >> sys.stderr, "Missing netvm name argument!"
|
||||
@ -485,6 +494,7 @@ properties = {
|
||||
"include_in_backups": set_include_in_backups,
|
||||
"pcidevs": set_pcidevs,
|
||||
"pci_strictreset": set_pci_strictreset,
|
||||
"pci_e820_host": set_pci_e820_host,
|
||||
"label" : set_label,
|
||||
"netvm" : set_netvm,
|
||||
"dispvm_netvm" : set_dispvm_netvm,
|
||||
@ -536,7 +546,7 @@ def main():
|
||||
default=False)
|
||||
parser.add_option("-s", "--set", action="store_true", dest="do_set",
|
||||
default=False)
|
||||
parser.add_option ("-g", "--gry", action="store_true", dest="do_get",
|
||||
parser.add_option ("-g", "--get", action="store_true", dest="do_get",
|
||||
default=False)
|
||||
parser.add_option("--force-root", action="store_true", dest="force_root",
|
||||
default=False,
|
||||
|
||||
@ -79,6 +79,10 @@ def main():
|
||||
exit (1)
|
||||
|
||||
try:
|
||||
if options.remove_from_db_only:
|
||||
# normally it is done by vm.remove_from_disk(), but it isn't
|
||||
# called in this case
|
||||
vm.libvirt_domain.undefine()
|
||||
if vm.installed_by_rpm:
|
||||
if options.verbose:
|
||||
print >> sys.stderr, "--> VM installed by RPM, leaving all the files on disk"
|
||||
|
||||
@ -47,7 +47,9 @@ def vm_run_cmd(vm, cmd, options):
|
||||
if options.verbose:
|
||||
print >> sys.stderr, "Running command on VM: '{0}'...".format(vm.name)
|
||||
if options.passio and options.color_output is not None:
|
||||
print "\033[0;%dm" % options.color_output,
|
||||
sys.stdout.write("\033[0;{}m".format(options.color_output))
|
||||
if options.passio and options.color_stderr is not None:
|
||||
sys.stderr.write("\033[0;{}m".format(options.color_stderr))
|
||||
|
||||
try:
|
||||
def tray_notify_generic(level, str):
|
||||
@ -65,6 +67,8 @@ def vm_run_cmd(vm, cmd, options):
|
||||
except QubesException as err:
|
||||
if options.passio and options.color_output is not None:
|
||||
sys.stdout.write("\033[0m")
|
||||
if options.passio and options.color_stderr is not None:
|
||||
sys.stderr.write("\033[0m")
|
||||
if options.tray:
|
||||
tray_notify_error(str(err))
|
||||
notify_error_qubes_manager(vm.name, str(err))
|
||||
@ -73,6 +77,8 @@ def vm_run_cmd(vm, cmd, options):
|
||||
finally:
|
||||
if options.passio and options.color_output is not None:
|
||||
sys.stdout.write("\033[0m")
|
||||
if options.passio and options.color_stderr is not None:
|
||||
sys.stderr.write("\033[0m")
|
||||
|
||||
def main():
|
||||
usage = "usage: %prog [options] [<vm-name>] [<cmd>]"
|
||||
@ -122,25 +128,44 @@ def main():
|
||||
dest="color_output", default=None,
|
||||
help="Disable marking VM output with red color")
|
||||
|
||||
parser.add_option("--no-color-stderr", action="store_false",
|
||||
dest="color_stderr", default=None,
|
||||
help="Disable marking VM stderr with red color")
|
||||
|
||||
parser.add_option("--color-output", action="store", type="int",
|
||||
dest="color_output",
|
||||
help="Force marking VM output with given ANSI style ("
|
||||
"use 31 for red)")
|
||||
|
||||
parser.add_option("--color-stderr", action="store", type="int",
|
||||
dest="color_stderr",
|
||||
help="Force marking VM stderr with given ANSI style ("
|
||||
"use 31 for red)")
|
||||
|
||||
(options, args) = parser.parse_args ()
|
||||
|
||||
if options.passio and options.run_on_all_running:
|
||||
if (options.passio and not options.localcmd) and options.run_on_all_running:
|
||||
parser.error ("Options --all and --pass-io cannot be used together")
|
||||
|
||||
if options.localcmd and not options.passio:
|
||||
print >> sys.stderr, "WARNING: option --localcmd have no effect " \
|
||||
"without --pass-io"
|
||||
|
||||
if options.passio:
|
||||
options.verbose = False
|
||||
|
||||
if options.color_output is None:
|
||||
if os.isatty(sys.stdout.fileno()):
|
||||
if os.isatty(sys.stdout.fileno()) and not options.localcmd:
|
||||
options.color_output = 31
|
||||
elif options.color_output is False:
|
||||
options.color_output = None
|
||||
|
||||
if options.color_stderr is None:
|
||||
if os.isatty(sys.stderr.fileno()) and not options.localcmd:
|
||||
options.color_stderr = 31
|
||||
elif options.color_stderr is False:
|
||||
options.color_stderr = None
|
||||
|
||||
if (options.pause or options.unpause):
|
||||
takes_cmd_argument = False
|
||||
else:
|
||||
@ -180,8 +205,6 @@ def main():
|
||||
continue
|
||||
if (options.unpause and vm.is_paused()) or (not options.unpause and vm.is_running()):
|
||||
vms_list.append (vm)
|
||||
# disable options incompatible with --all
|
||||
options.passio = False
|
||||
else:
|
||||
vm = qvm_collection.get_vm_by_name(vmname)
|
||||
if vm is None:
|
||||
|
||||
@ -36,6 +36,10 @@ def main():
|
||||
help="Force operation, even if may damage other VMs (eg shutdown of NetVM)")
|
||||
parser.add_option ("--wait", action="store_true", dest="wait_for_shutdown", default=False,
|
||||
help="Wait for the VM(s) to shutdown")
|
||||
parser.add_option("--wait-time", action="store", dest="wait_time",
|
||||
default=defaults["shutdown_counter_max"],
|
||||
help="Timout after which VM will be killed when --wait "
|
||||
"is used")
|
||||
parser.add_option ("--all", action="store_true", dest="shutdown_all", default=False,
|
||||
help="Shutdown all running VMs")
|
||||
parser.add_option ("--exclude", action="append", dest="exclude_list",
|
||||
@ -107,7 +111,7 @@ def main():
|
||||
continue
|
||||
else:
|
||||
halting_vms.append(vm)
|
||||
if shutdown_counter > defaults["shutdown_counter_max"]:
|
||||
if shutdown_counter > int(options.wait_time):
|
||||
# kill the VM
|
||||
if options.verbose:
|
||||
print >> sys.stderr, "Killing the (apparently hanging) VM '{0}'...".format(vm.name)
|
||||
|
||||
@ -57,6 +57,9 @@ def main():
|
||||
help="Do actions necessary when preparing DVM image")
|
||||
parser.add_option ("--custom-config", action="store", dest="custom_config", default=None,
|
||||
help="Use custom Xen config instead of Qubes-generated one")
|
||||
parser.add_option("--skip-if-running", action="store_true",
|
||||
dest="skip_if_running", default=False,
|
||||
help="Do not fail if the VM is already running")
|
||||
parser.add_option ("--debug", action="store_true", dest="debug", default=False,
|
||||
help="Enable debug mode for this VM (until its shutdown)")
|
||||
|
||||
@ -83,7 +86,12 @@ def main():
|
||||
exit(1)
|
||||
|
||||
if options.install_windows_tools:
|
||||
options.drive = 'cdrom:dom0:/usr/lib/qubes/qubes-windows-tools.iso'
|
||||
windows_tools_path = '/usr/lib/qubes/qubes-windows-tools.iso'
|
||||
if not os.path.exists(windows_tools_path):
|
||||
print >> sys.stderr, "You need to install 'qubes-windows-tools' " \
|
||||
"package in dom0 first"
|
||||
exit(1)
|
||||
options.drive = 'cdrom:dom0:{}'.format(windows_tools_path)
|
||||
|
||||
if options.drive_hd:
|
||||
options.drive = 'hd:' + options.drive_hd
|
||||
@ -104,6 +112,9 @@ def main():
|
||||
if options.debug:
|
||||
vm.debug = True
|
||||
|
||||
if options.skip_if_running and vm.is_running():
|
||||
return
|
||||
|
||||
try:
|
||||
vm.verify_files()
|
||||
xid = vm.start(verbose=options.verbose, preparing_dvm=options.preparing_dvm, start_guid=not options.noguid, notify_function=tray_notify_generic if options.tray else None)
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
#
|
||||
import fcntl
|
||||
|
||||
from optparse import OptionParser
|
||||
from qubes.qubes import QubesVmCollection
|
||||
import os.path
|
||||
import os
|
||||
@ -41,9 +42,11 @@ def get_netvm_of_vm(vm):
|
||||
return netvm
|
||||
|
||||
def main():
|
||||
verbose = False
|
||||
if len(sys.argv) > 1 and sys.argv[1] in [ '--verbose', '-v' ]:
|
||||
verbose = True
|
||||
parser = OptionParser()
|
||||
parser.add_option ("-v", "--verbose", action="store_true", dest="verbose", default=False)
|
||||
parser.add_option ("-f", "--force", action="store_true", dest="force", default=False)
|
||||
|
||||
(options, args) = parser.parse_args ()
|
||||
|
||||
lockfile_name = "/var/run/qubes/qvm-sync-clock.lock"
|
||||
if os.path.exists(lockfile_name):
|
||||
@ -74,43 +77,47 @@ def main():
|
||||
sys.exit(1)
|
||||
|
||||
net_vm = get_netvm_of_vm(clock_vm)
|
||||
if verbose:
|
||||
if options.verbose:
|
||||
print >> sys.stderr, '--> Waiting for network for ClockVM.'
|
||||
|
||||
# Ignore retcode, try even if nm-online failed - user can setup network manually
|
||||
# on-online has timeout 30sec by default
|
||||
net_vm.run('nm-online -x', verbose=verbose, gui=False, wait=True,
|
||||
net_vm.run('nm-online -x', verbose=options.verbose, gui=False, wait=True,
|
||||
ignore_stderr=True)
|
||||
|
||||
# Sync clock
|
||||
if clock_vm.run('QUBESRPC qubes.SyncNtpClock dom0', user="root",
|
||||
verbose=verbose, gui=False, wait=True, ignore_stderr=True) \
|
||||
verbose=options.verbose, gui=False, wait=True, ignore_stderr=True) \
|
||||
!= 0:
|
||||
print >> sys.stderr, 'Time sync failed, aborting!'
|
||||
sys.exit(1)
|
||||
if options.force:
|
||||
print >> sys.stderr, 'Time sync failed! - Syncing with dom0 ' \
|
||||
'anyway as requested'
|
||||
else:
|
||||
print >> sys.stderr, 'Time sync failed! - Exiting'
|
||||
sys.exit(1)
|
||||
else:
|
||||
# Use the date format based on RFC2822 to avoid localisation issues
|
||||
p = clock_vm.run('date -u -Iseconds', verbose=options.verbose,
|
||||
gui=False, passio_popen=True, ignore_stderr=True)
|
||||
date_out = p.stdout.read(100)
|
||||
date_out = date_out.strip()
|
||||
if not re.match(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+00:?00$', date_out):
|
||||
print >> sys.stderr, 'Invalid date output, aborting!'
|
||||
sys.exit(1)
|
||||
|
||||
# Use the date format based on RFC2822 to avoid localisation issues
|
||||
p = clock_vm.run('date -u -Iseconds', verbose=verbose,
|
||||
gui=False, passio_popen=True, ignore_stderr=True)
|
||||
date_out = p.stdout.read(100)
|
||||
date_out = date_out.strip()
|
||||
if not re.match(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+0000$', date_out):
|
||||
print >> sys.stderr, 'Invalid date output, aborting!'
|
||||
sys.exit(1)
|
||||
# Sync dom0 time
|
||||
if options.verbose:
|
||||
print >> sys.stderr, '--> Syncing dom0 clock.'
|
||||
|
||||
# Sync dom0 time
|
||||
if verbose:
|
||||
print >> sys.stderr, '--> Syncing dom0 clock.'
|
||||
|
||||
subprocess.check_call(['sudo', 'date', '-u', '-Iseconds', '-s', date_out],
|
||||
stdout=None if verbose else open(os.devnull, 'w'))
|
||||
subprocess.check_call(['sudo', 'hwclock', '--systohc'],
|
||||
stdout=None if verbose else open(os.devnull, 'w'))
|
||||
subprocess.check_call(['sudo', 'date', '-u', '-Iseconds', '-s', date_out],
|
||||
stdout=None if options.verbose else open(os.devnull, 'w'))
|
||||
subprocess.check_call(['sudo', 'hwclock', '--systohc'],
|
||||
stdout=None if options.verbose else open(os.devnull, 'w'))
|
||||
|
||||
# Sync other VMs clock
|
||||
for vm in qvm_collection.values():
|
||||
if vm.is_running() and vm.qid != 0 and vm.qid != clock_vm.qid:
|
||||
if verbose:
|
||||
if options.verbose:
|
||||
print >> sys.stderr, '--> Syncing \'%s\' clock.' % vm.name
|
||||
try:
|
||||
vm.run_service("qubes.SetDateTime", user="root",
|
||||
|
||||
124
qvm-tools/qvm-top
Executable file
124
qvm-tools/qvm-top
Executable file
@ -0,0 +1,124 @@
|
||||
#!/usr/bin/python2
|
||||
# -*- encoding: utf8 -*-
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2010 Joanna Rutkowska <joanna@invisiblethingslab.com>
|
||||
#
|
||||
# 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 QubesVmCollection
|
||||
from qubes.qubes import QubesHost
|
||||
from qubes.qubes import QubesException
|
||||
from optparse import OptionParser
|
||||
import sys
|
||||
|
||||
def main():
|
||||
usage = "usage: %prog [options]"
|
||||
parser = OptionParser (usage)
|
||||
|
||||
parser.add_option("--list", dest="list_top",
|
||||
action="store_true", default=False,
|
||||
help="n m : One line summary of top n vms with more than m cpu_time %")
|
||||
|
||||
(options, args) = parser.parse_args ()
|
||||
|
||||
qvm_collection = QubesVmCollection()
|
||||
qvm_collection.lock_db_for_reading()
|
||||
qvm_collection.load()
|
||||
qvm_collection.unlock_db()
|
||||
|
||||
fields_to_display = ["name", "cpu", "mem"]
|
||||
|
||||
cpu_usages = None
|
||||
qhost = QubesHost()
|
||||
(measure_time, cpu_usages) = qhost.measure_cpu_usage(qvm_collection)
|
||||
|
||||
vms_list = [vm for vm in qvm_collection.values() if vm.is_running()]
|
||||
vms_list = sorted(vms_list, key= lambda vm: 1-cpu_usages[vm.get_xid()]['cpu_usage'])
|
||||
|
||||
no_vms = len (vms_list)
|
||||
vms_to_display = vms_list
|
||||
|
||||
if options.list_top:
|
||||
any_shown = False
|
||||
ndisp = 3
|
||||
cputh = 0
|
||||
if len(args) > 0:
|
||||
ndisp = int(args[0])
|
||||
if len(args) > 1:
|
||||
cputh = int(args[1])
|
||||
|
||||
for vm in vms_to_display[:ndisp]:
|
||||
cpu = cpu_usages[vm.get_xid()]['cpu_usage']
|
||||
if cpu > cputh:
|
||||
any_shown = True
|
||||
sys.stdout.write("%d %s, " % (cpu, vm.name))
|
||||
|
||||
if any_shown:
|
||||
sys.stdout.write(" ... | ")
|
||||
|
||||
totalMem = 0
|
||||
dom0mem = 0
|
||||
for vm in vms_to_display:
|
||||
if not vm.name == "dom0":
|
||||
totalMem += vm.get_mem()
|
||||
else:
|
||||
dom0mem = vm.get_mem()
|
||||
totalMem /= 1024.0 * 1024.0
|
||||
dom0mem /= 1024.0 * 1024.0
|
||||
sys.stdout.write("%.1f G + %.1f G" % (totalMem, dom0mem))
|
||||
return
|
||||
|
||||
max_width = { 'name': 0, 'cpu': 0, 'mem': 0 }
|
||||
data_to_display = []
|
||||
for vm in vms_to_display:
|
||||
data_row = {}
|
||||
data_row['name'] = vm.name
|
||||
max_width['name'] = max(max_width['name'], len(data_row['name']))
|
||||
data_row['cpu'] = "%.1f" % (cpu_usages[vm.get_xid()]['cpu_usage'])
|
||||
max_width['cpu'] = max(max_width['cpu'], len(data_row['cpu']))
|
||||
data_row['mem'] = "%d" % (vm.get_mem() / (1024.0))
|
||||
max_width['mem'] = max(max_width['mem'], len(data_row['mem']))
|
||||
data_to_display.append(data_row)
|
||||
|
||||
# Display the header
|
||||
s = ""
|
||||
for f in fields_to_display:
|
||||
fmt="{{0:-^{0}}}-+".format(max_width[f] + 1)
|
||||
s += fmt.format('-')
|
||||
print s
|
||||
s = ""
|
||||
for f in fields_to_display:
|
||||
fmt="{{0:>{0}}} |".format(max_width[f] + 1)
|
||||
s += fmt.format(f)
|
||||
print s
|
||||
s = ""
|
||||
for f in fields_to_display:
|
||||
fmt="{{0:-^{0}}}-+".format(max_width[f] + 1)
|
||||
s += fmt.format('-')
|
||||
print s
|
||||
|
||||
# ... and the actual data
|
||||
for row in data_to_display:
|
||||
s = ""
|
||||
for f in fields_to_display:
|
||||
fmt="{{0:>{0}}} |".format(max_width[f] + 1)
|
||||
s += fmt.format(row[f])
|
||||
print s
|
||||
|
||||
main()
|
||||
@ -92,10 +92,21 @@ def main():
|
||||
touch_dvm_savefile = is_dvm_up_to_date(tvm, dvm_tmpl)
|
||||
|
||||
print >> sys.stderr, "Creating temporary VM..."
|
||||
fstrim_vm = qvm_collection.add_new_vm("QubesAppVm",
|
||||
template=tvm,
|
||||
name="{}-fstrim".format(tvm_name),
|
||||
netvm=None,
|
||||
trim_vmname = "trim-{}".format(tvm_name[:31 - len('trim-')])
|
||||
fstrim_vm = qvm_collection.get_vm_by_name(trim_vmname)
|
||||
if fstrim_vm is not None:
|
||||
if not fstrim_vm.internal:
|
||||
print >>sys.stderr, \
|
||||
"ERROR: VM '{}' already exists and is not marked as internal. " \
|
||||
"Remove it manually."
|
||||
fstrim_vm.remove_from_disk()
|
||||
qvm_collection.pop(fstrim_vm.qid)
|
||||
fstrim_vm = qvm_collection.add_new_vm(
|
||||
"QubesAppVm",
|
||||
template=tvm,
|
||||
name=trim_vmname,
|
||||
netvm=None,
|
||||
internal=True,
|
||||
)
|
||||
if not fstrim_vm:
|
||||
print >> sys.stderr, "ERROR: Failed to create new VM"
|
||||
|
||||
@ -27,8 +27,6 @@ from optparse import OptionParser
|
||||
import sys
|
||||
import os
|
||||
|
||||
pvusb_enable_flagfile = '/var/lib/qubes/pvusb-enable.flag'
|
||||
|
||||
def main():
|
||||
usage = "usage: %prog -l [options]\n"\
|
||||
"usage: %prog -a [options] <vm-name> <device-vm-name>:<device>\n"\
|
||||
@ -49,24 +47,6 @@ def main():
|
||||
|
||||
(options, args) = parser.parse_args ()
|
||||
|
||||
if not os.path.exists(pvusb_enable_flagfile):
|
||||
print >> sys.stderr, ""
|
||||
print >> sys.stderr, "******* WARNING *** WARNING *** WARNING *** WARNING *******"
|
||||
print >> sys.stderr, "*** ***"
|
||||
print >> sys.stderr, "*** PVUSB passthrough kernel support is still unstable. ***"
|
||||
print >> sys.stderr, "*** It can CRASH your VMs. ***"
|
||||
print >> sys.stderr, "*** ***"
|
||||
print >> sys.stderr, "***********************************************************"
|
||||
print >> sys.stderr, ""
|
||||
print >> sys.stderr, "To use it, you need install kernel from \"unstable\" repository"
|
||||
print >> sys.stderr, "If you still want to enable it, type capital YES"
|
||||
print >> sys.stderr, ""
|
||||
prompt = raw_input ("Do you want enable PV USB support? ")
|
||||
if prompt == "YES":
|
||||
open(pvusb_enable_flagfile, "w").close()
|
||||
else:
|
||||
exit(1)
|
||||
|
||||
if hasattr(os, "geteuid") and os.geteuid() == 0:
|
||||
if not options.force_root:
|
||||
print >> sys.stderr, "*** Running this tool as root is strongly discouraged, this will lead you in permissions problems."
|
||||
@ -78,11 +58,10 @@ def main():
|
||||
print >> sys.stderr, "Only one of -l -a -d is allowed!"
|
||||
exit (1)
|
||||
|
||||
if options.do_attach or options.do_detach:
|
||||
qvm_collection = QubesVmCollection()
|
||||
qvm_collection.lock_db_for_reading()
|
||||
qvm_collection.load()
|
||||
qvm_collection.unlock_db()
|
||||
qvm_collection = QubesVmCollection()
|
||||
qvm_collection.lock_db_for_reading()
|
||||
qvm_collection.load()
|
||||
qvm_collection.unlock_db()
|
||||
|
||||
if options.do_attach:
|
||||
if (len (args) != 2):
|
||||
@ -91,14 +70,17 @@ def main():
|
||||
if vm is None:
|
||||
parser.error ("Invalid VM name: %s" % args[0])
|
||||
|
||||
# FIXME: here we assume that device is always in form "domain:dev", which can be changed in the future
|
||||
# FIXME: here we assume that device is always in form "domain:dev",
|
||||
# which can be changed in the future
|
||||
if args[1].find(":") < 0:
|
||||
parser.error ("Invalid device syntax: %s" % args[1])
|
||||
dev_list = usb_list()
|
||||
parser.error("Invalid device syntax: %s" % args[1])
|
||||
backend_vm = qvm_collection.get_vm_by_name(args[1].split(":")[0])
|
||||
if backend_vm is None:
|
||||
parser.error("No such VM: {}".format(args[1].split(":")[0]))
|
||||
dev_list = usb_list(qvm_collection, vm=backend_vm)
|
||||
if not args[1] in dev_list.keys():
|
||||
parser.error ("Invalid device name: %s" % args[1])
|
||||
parser.error("Invalid device name: %s" % args[1])
|
||||
dev = dev_list[args[1]]
|
||||
backend_vm = qvm_collection.get_vm_by_name(dev['vm'])
|
||||
assert backend_vm is not None
|
||||
|
||||
kwargs = {}
|
||||
@ -106,14 +88,14 @@ def main():
|
||||
# kwargs['frontend'] = options.frontend
|
||||
kwargs['auto_detach'] = options.auto_detach
|
||||
try:
|
||||
usb_attach(vm, backend_vm, dev['device'], **kwargs)
|
||||
usb_attach(qvm_collection, vm, dev, **kwargs)
|
||||
except QubesException as e:
|
||||
print >> sys.stderr, "ERROR: %s" % str(e)
|
||||
sys.exit(1)
|
||||
elif options.do_detach:
|
||||
if (len (args) < 1):
|
||||
parser.error ("You must provide device or vm name!")
|
||||
if len(args) > 1:
|
||||
if len(args) > 1:
|
||||
parser.error ("Too many parameters")
|
||||
# Check if provided name is VM
|
||||
vm = qvm_collection.get_vm_by_name(args[0])
|
||||
@ -123,36 +105,37 @@ def main():
|
||||
# kwargs['frontend'] = options.frontend
|
||||
# usb_detach(vm, **kwargs)
|
||||
#else:
|
||||
usb_detach_all(vm)
|
||||
usb_detach_all(qvm_collection, vm)
|
||||
else:
|
||||
# Maybe usbvm:device?
|
||||
|
||||
# FIXME: nasty copy-paste from attach code half a page above
|
||||
# FIXME: here we assume that device is always in form "domain:dev", which can be changed in the future
|
||||
# FIXME: nasty copy-paste from attach code half a page above
|
||||
# FIXME: here we assume that device is always in form "domain:dev",
|
||||
# which can be changed in the future
|
||||
if args[0].find(":") < 0:
|
||||
parser.error ("Invalid device syntax: %s" % args[0])
|
||||
dev_list = usb_list()
|
||||
parser.error("Invalid device syntax: %s" % args[0])
|
||||
backend_vm = qvm_collection.get_vm_by_name(args[0].split(":")[0])
|
||||
if backend_vm is None:
|
||||
parser.error("No such VM: {}".format(args[0].split(":")[0]))
|
||||
dev_list = usb_list(qvm_collection, vm=backend_vm)
|
||||
if not args[0] in dev_list.keys():
|
||||
parser.error ("Invalid device name: %s" % args[0])
|
||||
parser.error("Invalid device name: %s" % args[0])
|
||||
dev = dev_list[args[0]]
|
||||
backend_vm = qvm_collection.get_vm_by_name(dev['vm'])
|
||||
assert backend_vm is not None
|
||||
|
||||
attached_to = usb_check_attached('', backend_vm.xid, dev['device'])
|
||||
attached_to = usb_check_attached(qvm_collection, dev)
|
||||
if attached_to is None:
|
||||
print >> sys.stderr, "WARNING: Device not connected to any VM"
|
||||
exit(0)
|
||||
usb_detach(backend_vm, attached_to)
|
||||
usb_detach(qvm_collection, attached_to, dev)
|
||||
else:
|
||||
if len(args) > 0:
|
||||
parser.error ("Too many parameters")
|
||||
if len(args) > 0:
|
||||
parser.error("Too many parameters")
|
||||
# do_list
|
||||
for dev in usb_list().values():
|
||||
attached_to = usb_check_attached('', dev['xid'], dev['device'])
|
||||
for dev in usb_list(qvm_collection).values():
|
||||
attached_to = dev['connected-to']
|
||||
attached_to_str = ""
|
||||
if attached_to:
|
||||
attached_to_str = " (attached to %s:%s)" % (attached_to['vm'], attached_to['frontend'])
|
||||
print "%s\t%s%s (USBv%s)" % (dev['name'], dev['desc'], attached_to_str, dev['usb_ver'])
|
||||
attached_to_str = " (attached to %s)" % (attached_to.name)
|
||||
print "%s\t%s%s" % (dev['name'], dev['desc'], attached_to_str)
|
||||
exit (0)
|
||||
|
||||
main()
|
||||
|
||||
@ -27,6 +27,9 @@
|
||||
|
||||
%{!?version: %define version %(cat version)}
|
||||
|
||||
# debug_package hack should be removed when BuildArch:noarch is enabled below
|
||||
%define debug_package %{nil}
|
||||
|
||||
%define _dracutmoddir /usr/lib/dracut/modules.d
|
||||
%if %{fedora} < 17
|
||||
%define _dracutmoddir /usr/share/dracut/modules.d
|
||||
@ -43,11 +46,14 @@ License: GPL
|
||||
URL: http://www.qubes-os.org
|
||||
BuildRequires: ImageMagick
|
||||
BuildRequires: systemd-units
|
||||
# FIXME: Enable this and disable debug_package
|
||||
#BuildArch: noarch
|
||||
Requires(post): systemd-units
|
||||
Requires(preun): systemd-units
|
||||
Requires(postun): systemd-units
|
||||
Requires: python, pciutils, python-inotify, python-daemon
|
||||
Requires: qubes-core-dom0-linux >= 2.0.24
|
||||
Requires: qubes-core-dom0-linux >= 3.1.8
|
||||
Requires: qubes-core-dom0-doc
|
||||
Requires: qubes-db-dom0
|
||||
Requires: python-lxml
|
||||
Requires: python-psutil
|
||||
@ -57,7 +63,7 @@ Requires: libvirt-python
|
||||
%if x%{?backend_vmm} == xxen
|
||||
Requires: xen-runtime
|
||||
Requires: xen-hvm
|
||||
Requires: libvirt-daemon-xen >= 1.2.12-3
|
||||
Requires: libvirt-daemon-xen >= 1.2.20-6
|
||||
%endif
|
||||
Requires: createrepo
|
||||
Requires: gnome-packagekit
|
||||
@ -154,7 +160,6 @@ if ! grep -q ^qubes: /etc/group ; then
|
||||
fi
|
||||
|
||||
%triggerin -- xen-runtime
|
||||
sed -i 's/\/block /\/block.qubes /' /etc/udev/rules.d/xen-backend.rules
|
||||
/usr/lib/qubes/fix-dir-perms.sh
|
||||
|
||||
%preun
|
||||
@ -170,12 +175,12 @@ if [ "$1" = 0 ] ; then
|
||||
chgrp root /etc/xen
|
||||
chmod 700 /etc/xen
|
||||
groupdel qubes
|
||||
sed -i 's/\/block.qubes /\/block /' /etc/udev/rules.d/xen-backend.rules
|
||||
fi
|
||||
|
||||
%files
|
||||
%defattr(-,root,root,-)
|
||||
%config(noreplace) %attr(0664,root,qubes) %{_sysconfdir}/qubes/qmemman.conf
|
||||
%config(noreplace) %attr(0664,root,qubes) %{_sysconfdir}/qubes/storage.conf
|
||||
/usr/bin/qvm-*
|
||||
/usr/bin/qubes-*
|
||||
%dir %{python_sitearch}/qubes
|
||||
@ -245,11 +250,14 @@ fi
|
||||
%attr(0664,root,qubes) %config(noreplace) /etc/qubes-rpc/policy/qubes.Filecopy
|
||||
%attr(0664,root,qubes) %config(noreplace) /etc/qubes-rpc/policy/qubes.GetImageRGBA
|
||||
%attr(0664,root,qubes) %config(noreplace) /etc/qubes-rpc/policy/qubes.OpenInVM
|
||||
%attr(0664,root,qubes) %config(noreplace) /etc/qubes-rpc/policy/qubes.OpenURL
|
||||
%attr(0664,root,qubes) %config(noreplace) /etc/qubes-rpc/policy/qubes.NotifyTools
|
||||
%attr(0664,root,qubes) %config(noreplace) /etc/qubes-rpc/policy/qubes.NotifyUpdates
|
||||
%attr(0664,root,qubes) %config(noreplace) /etc/qubes-rpc/policy/qubes.VMShell
|
||||
%attr(0664,root,qubes) %config(noreplace) /etc/qubes-rpc/policy/qubes.GetRandomizedTime
|
||||
/etc/qubes-rpc/qubes.NotifyTools
|
||||
/etc/qubes-rpc/qubes.NotifyUpdates
|
||||
/etc/qubes-rpc/qubes.GetRandomizedTime
|
||||
%attr(2770,root,qubes) %dir /var/log/qubes
|
||||
%attr(0770,root,qubes) %dir /var/run/qubes
|
||||
/etc/xdg/autostart/qubes-guid.desktop
|
||||
|
||||
@ -17,13 +17,31 @@ endif
|
||||
cp backupcompatibility.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp basic.py $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp basic.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp block.py $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp block.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp dispvm.py $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp dispvm.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp dom0_update.py $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp dom0_update.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp extra.py $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp extra.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp hardware.py $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp hardware.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp hvm.py $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp hvm.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp mime.py $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp mime.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp network.py $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp network.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp vm_qrexec_gui.py $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp vm_qrexec_gui.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp pvgrub.py $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp pvgrub.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp regressions.py $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp regressions.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp run.py $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp run.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp storage.py $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp storage.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp storage_xen.py $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp storage_xen.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp vm_qrexec_gui.py $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
cp vm_qrexec_gui.py[co] $(DESTDIR)$(PYTHON_TESTSPATH)
|
||||
|
||||
@ -23,21 +23,35 @@
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
#
|
||||
|
||||
"""
|
||||
.. warning::
|
||||
The test suite hereby claims any domain whose name starts with
|
||||
:py:data:`VMPREFIX` as fair game. This is needed to enforce sane
|
||||
test executing environment. If you have domains named ``test-*``,
|
||||
don't run the tests.
|
||||
"""
|
||||
from distutils import spawn
|
||||
import functools
|
||||
|
||||
import multiprocessing
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
import unittest.case
|
||||
|
||||
import lxml.etree
|
||||
import sys
|
||||
import pkg_resources
|
||||
|
||||
import qubes.backup
|
||||
import qubes.qubes
|
||||
import time
|
||||
|
||||
VMPREFIX = 'test-'
|
||||
VMPREFIX = 'test-inst-'
|
||||
CLSVMPREFIX = 'test-cls-'
|
||||
|
||||
|
||||
#: :py:obj:`True` if running in dom0, :py:obj:`False` otherwise
|
||||
@ -85,6 +99,32 @@ def skipUnlessGit(test_item):
|
||||
|
||||
return unittest.skipUnless(in_git, 'outside git tree')(test_item)
|
||||
|
||||
def expectedFailureIfTemplate(templates):
|
||||
"""
|
||||
Decorator for marking specific test as expected to fail only for some
|
||||
templates. Template name is compared as substring, so 'whonix' will
|
||||
handle both 'whonix-ws' and 'whonix-gw'.
|
||||
templates can be either a single string, or an iterable
|
||||
"""
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
template = self.template
|
||||
if isinstance(templates, basestring):
|
||||
should_expect_fail = template in templates
|
||||
else:
|
||||
should_expect_fail = any([template in x for x in templates])
|
||||
if should_expect_fail:
|
||||
try:
|
||||
func(self, *args, **kwargs)
|
||||
except Exception:
|
||||
raise unittest.case._ExpectedFailure(sys.exc_info())
|
||||
raise unittest.case._UnexpectedSuccess()
|
||||
else:
|
||||
# Call directly:
|
||||
func(self, *args, **kwargs)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
class _AssertNotRaisesContext(object):
|
||||
"""A context manager used to implement TestCase.assertNotRaises methods.
|
||||
@ -133,14 +173,12 @@ class QubesTestCase(unittest.TestCase):
|
||||
self.__class__.__name__,
|
||||
self._testMethodName))
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return '{}/{}/{}'.format(
|
||||
'.'.join(self.__class__.__module__.split('.')[2:]),
|
||||
self.__class__.__module__,
|
||||
self.__class__.__name__,
|
||||
self._testMethodName)
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
super(QubesTestCase, self).tearDown()
|
||||
|
||||
@ -153,7 +191,6 @@ class QubesTestCase(unittest.TestCase):
|
||||
and filter((lambda (tc, exc): tc is self), l):
|
||||
raise BeforeCleanExit()
|
||||
|
||||
|
||||
def assertNotRaises(self, excClass, callableObj=None, *args, **kwargs):
|
||||
"""Fail if an exception of class excClass is raised
|
||||
by callableObj when invoked with arguments args and keyword
|
||||
@ -183,15 +220,14 @@ class QubesTestCase(unittest.TestCase):
|
||||
with context:
|
||||
callableObj(*args, **kwargs)
|
||||
|
||||
|
||||
def assertXMLEqual(self, xml1, xml2):
|
||||
'''Check for equality of two XML objects.
|
||||
"""Check for equality of two XML objects.
|
||||
|
||||
:param xml1: first element
|
||||
:param xml2: second element
|
||||
:type xml1: :py:class:`lxml.etree._Element`
|
||||
:type xml2: :py:class:`lxml.etree._Element`
|
||||
''' # pylint: disable=invalid-name
|
||||
""" # pylint: disable=invalid-name
|
||||
|
||||
self.assertEqual(xml1.tag, xml2.tag)
|
||||
self.assertEqual(xml1.text, xml2.text)
|
||||
@ -202,13 +238,13 @@ class QubesTestCase(unittest.TestCase):
|
||||
|
||||
class SystemTestsMixin(object):
|
||||
def setUp(self):
|
||||
'''Set up the test.
|
||||
"""Set up the test.
|
||||
|
||||
.. warning::
|
||||
This method instantiates QubesVmCollection acquires write lock for
|
||||
it. You can use is as :py:attr:`qc`. You can (and probably
|
||||
should) release the lock at the end of setUp in subclass
|
||||
'''
|
||||
"""
|
||||
|
||||
super(SystemTestsMixin, self).setUp()
|
||||
|
||||
@ -218,17 +254,24 @@ class SystemTestsMixin(object):
|
||||
|
||||
self.conn = libvirt.open(qubes.qubes.defaults['libvirt_uri'])
|
||||
|
||||
self.remove_test_vms()
|
||||
|
||||
self._remove_test_vms(self.qc, self.conn)
|
||||
|
||||
def tearDown(self):
|
||||
super(SystemTestsMixin, self).tearDown()
|
||||
|
||||
try: self.qc.lock_db_for_writing()
|
||||
except qubes.qubes.QubesException: pass
|
||||
# release the lock, because we have no way to check whether it was
|
||||
# read or write lock
|
||||
try:
|
||||
self.qc.unlock_db()
|
||||
except qubes.qubes.QubesException:
|
||||
pass
|
||||
|
||||
self._kill_test_vms(self.qc)
|
||||
|
||||
self.qc.lock_db_for_writing()
|
||||
self.qc.load()
|
||||
|
||||
self.remove_test_vms()
|
||||
self._remove_test_vms(self.qc, self.conn)
|
||||
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
@ -236,9 +279,36 @@ class SystemTestsMixin(object):
|
||||
|
||||
self.conn.close()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(SystemTestsMixin, cls).tearDownClass()
|
||||
|
||||
def make_vm_name(self, name):
|
||||
return VMPREFIX + name
|
||||
qc = qubes.qubes.QubesVmCollection()
|
||||
qc.lock_db_for_reading()
|
||||
qc.load()
|
||||
qc.unlock_db()
|
||||
|
||||
conn = libvirt.open(qubes.qubes.defaults['libvirt_uri'])
|
||||
|
||||
cls._kill_test_vms(qc, prefix=CLSVMPREFIX)
|
||||
|
||||
qc.lock_db_for_writing()
|
||||
qc.load()
|
||||
|
||||
cls._remove_test_vms(qc, conn, prefix=CLSVMPREFIX)
|
||||
|
||||
qc.save()
|
||||
qc.unlock_db()
|
||||
del qc
|
||||
|
||||
conn.close()
|
||||
|
||||
@staticmethod
|
||||
def make_vm_name(name, class_teardown=False):
|
||||
if class_teardown:
|
||||
return CLSVMPREFIX + name
|
||||
else:
|
||||
return VMPREFIX + name
|
||||
|
||||
def save_and_reload_db(self):
|
||||
self.qc.save()
|
||||
@ -246,44 +316,64 @@ class SystemTestsMixin(object):
|
||||
self.qc.lock_db_for_writing()
|
||||
self.qc.load()
|
||||
|
||||
def _remove_vm_qubes(self, vm):
|
||||
@staticmethod
|
||||
def _kill_test_vms(qc, prefix=VMPREFIX):
|
||||
# do not keep write lock while killing VMs, because that may cause a
|
||||
# deadlock with disk hotplug scripts (namely qvm-template-commit
|
||||
# called when shutting down TemplateVm)
|
||||
qc.lock_db_for_reading()
|
||||
qc.load()
|
||||
qc.unlock_db()
|
||||
for vm in qc.values():
|
||||
if vm.name.startswith(prefix):
|
||||
if vm.is_running():
|
||||
vm.force_shutdown()
|
||||
|
||||
@classmethod
|
||||
def _remove_vm_qubes(cls, qc, conn, vm):
|
||||
vmname = vm.name
|
||||
|
||||
try:
|
||||
# XXX .is_running() may throw libvirtError if undefined
|
||||
if vm.is_running():
|
||||
vm.force_shutdown()
|
||||
except: pass
|
||||
except:
|
||||
pass
|
||||
|
||||
try: vm.remove_from_disk()
|
||||
except: pass
|
||||
try:
|
||||
vm.remove_from_disk()
|
||||
except:
|
||||
pass
|
||||
|
||||
try: vm.libvirt_domain.undefine()
|
||||
except libvirt.libvirtError: pass
|
||||
try:
|
||||
vm.libvirt_domain.undefine()
|
||||
except libvirt.libvirtError:
|
||||
pass
|
||||
|
||||
self.qc.pop(vm.qid)
|
||||
qc.pop(vm.qid)
|
||||
del vm
|
||||
|
||||
# Now ensure it really went away. This may not have happened,
|
||||
# for example if vm.libvirtDomain malfunctioned.
|
||||
try:
|
||||
dom = self.conn.lookupByName(vmname)
|
||||
except: pass
|
||||
dom = conn.lookupByName(vmname)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
self._remove_vm_libvirt(dom)
|
||||
cls._remove_vm_libvirt(dom)
|
||||
|
||||
self._remove_vm_disk(vmname)
|
||||
cls._remove_vm_disk(vmname)
|
||||
|
||||
|
||||
def _remove_vm_libvirt(self, dom):
|
||||
@staticmethod
|
||||
def _remove_vm_libvirt(dom):
|
||||
try:
|
||||
dom.destroy()
|
||||
except libvirt.libvirtError: # not running
|
||||
pass
|
||||
dom.undefine()
|
||||
|
||||
|
||||
def _remove_vm_disk(self, vmname):
|
||||
@staticmethod
|
||||
def _remove_vm_disk(vmname):
|
||||
for dirspec in (
|
||||
'qubes_appvms_dir',
|
||||
'qubes_servicevms_dir',
|
||||
@ -296,35 +386,33 @@ class SystemTestsMixin(object):
|
||||
else:
|
||||
os.unlink(dirpath)
|
||||
|
||||
|
||||
def remove_vms(self, vms):
|
||||
for vm in vms: self._remove_vm_qubes(vm)
|
||||
for vm in vms:
|
||||
self._remove_vm_qubes(self.qc, self.conn, vm)
|
||||
self.save_and_reload_db()
|
||||
|
||||
@classmethod
|
||||
def _remove_test_vms(cls, qc, conn, prefix=VMPREFIX):
|
||||
"""Aggresively remove any domain that has name in testing namespace.
|
||||
|
||||
def remove_test_vms(self):
|
||||
'''Aggresively remove any domain that has name in testing namespace.
|
||||
|
||||
.. warning::
|
||||
The test suite hereby claims any domain whose name starts with
|
||||
:py:data:`VMPREFIX` as fair game. This is needed to enforce sane
|
||||
test executing environment. If you have domains named ``test-*``,
|
||||
don't run the tests.
|
||||
'''
|
||||
"""
|
||||
|
||||
# first, remove them Qubes-way
|
||||
something_removed = False
|
||||
for vm in self.qc.values():
|
||||
if vm.name.startswith(VMPREFIX):
|
||||
self._remove_vm_qubes(vm)
|
||||
for vm in qc.values():
|
||||
if vm.name.startswith(prefix):
|
||||
cls._remove_vm_qubes(qc, conn, vm)
|
||||
something_removed = True
|
||||
if something_removed:
|
||||
self.save_and_reload_db()
|
||||
qc.save()
|
||||
qc.unlock_db()
|
||||
qc.lock_db_for_writing()
|
||||
qc.load()
|
||||
|
||||
# now remove what was only in libvirt
|
||||
for dom in self.conn.listAllDomains():
|
||||
if dom.name().startswith(VMPREFIX):
|
||||
self._remove_vm_libvirt(dom)
|
||||
for dom in conn.listAllDomains():
|
||||
if dom.name().startswith(prefix):
|
||||
cls._remove_vm_libvirt(dom)
|
||||
|
||||
# finally remove anything that is left on disk
|
||||
vmnames = set()
|
||||
@ -335,10 +423,34 @@ class SystemTestsMixin(object):
|
||||
dirpath = os.path.join(qubes.qubes.system_path['qubes_base_dir'],
|
||||
qubes.qubes.system_path[dirspec])
|
||||
for name in os.listdir(dirpath):
|
||||
if name.startswith(VMPREFIX):
|
||||
if name.startswith(prefix):
|
||||
vmnames.add(name)
|
||||
for vmname in vmnames:
|
||||
self._remove_vm_disk(vmname)
|
||||
cls._remove_vm_disk(vmname)
|
||||
|
||||
def qrexec_policy(self, service, source, destination, allow=True):
|
||||
"""
|
||||
Allow qrexec calls for duration of the test
|
||||
:param service: service name
|
||||
:param source: source VM name
|
||||
:param destination: destination VM name
|
||||
:return:
|
||||
"""
|
||||
|
||||
def add_remove_rule(add=True):
|
||||
with open('/etc/qubes-rpc/policy/{}'.format(service), 'r+') as policy:
|
||||
policy_rules = policy.readlines()
|
||||
rule = "{} {} {}\n".format(source, destination,
|
||||
'allow' if allow else 'deny')
|
||||
if add:
|
||||
policy_rules.insert(0, rule)
|
||||
else:
|
||||
policy_rules.remove(rule)
|
||||
policy.truncate(0)
|
||||
policy.seek(0)
|
||||
policy.write(''.join(policy_rules))
|
||||
add_remove_rule(add=True)
|
||||
self.addCleanup(add_remove_rule, add=False)
|
||||
|
||||
def wait_for_window(self, title, timeout=30, show=True):
|
||||
"""
|
||||
@ -355,7 +467,7 @@ class SystemTestsMixin(object):
|
||||
wait_count = 0
|
||||
while subprocess.call(['xdotool', 'search', '--name', title],
|
||||
stdout=open(os.path.devnull, 'w'),
|
||||
stderr=subprocess.STDOUT) == 0:
|
||||
stderr=subprocess.STDOUT) == int(show):
|
||||
wait_count += 1
|
||||
if wait_count > timeout*10:
|
||||
self.fail("Timeout while waiting for {} window to {}".format(
|
||||
@ -363,7 +475,31 @@ class SystemTestsMixin(object):
|
||||
)
|
||||
time.sleep(0.1)
|
||||
|
||||
def enter_keys_in_window(self, title, keys):
|
||||
"""
|
||||
Search for window with given title, then enter listed keys there.
|
||||
The function will wait for said window to appear.
|
||||
|
||||
:param title: title of window
|
||||
:param keys: list of keys to enter, as for `xdotool key`
|
||||
:return: None
|
||||
"""
|
||||
|
||||
# 'xdotool search --sync' sometimes crashes on some race when
|
||||
# accessing window properties
|
||||
self.wait_for_window(title)
|
||||
command = ['xdotool', 'search', '--name', title,
|
||||
'windowactivate', '--sync',
|
||||
'key'] + keys
|
||||
subprocess.check_call(command)
|
||||
|
||||
def shutdown_and_wait(self, vm, timeout=60):
|
||||
"""
|
||||
|
||||
:param vm: VM object
|
||||
:param timeout: timeout after which fail the test
|
||||
:return:
|
||||
"""
|
||||
vm.shutdown()
|
||||
while timeout > 0:
|
||||
if not vm.is_running():
|
||||
@ -372,6 +508,77 @@ class SystemTestsMixin(object):
|
||||
timeout -= 1
|
||||
self.fail("Timeout while waiting for VM {} shutdown".format(vm.name))
|
||||
|
||||
def prepare_hvm_system_linux(self, vm, init_script, extra_files=None):
|
||||
if not os.path.exists('/usr/lib/grub/i386-pc'):
|
||||
self.skipTest('grub2 not installed')
|
||||
if not spawn.find_executable('grub2-install'):
|
||||
self.skipTest('grub2-tools not installed')
|
||||
if not spawn.find_executable('dracut'):
|
||||
self.skipTest('dracut not installed')
|
||||
# create a single partition
|
||||
p = subprocess.Popen(['sfdisk', '-q', '-L', vm.storage.root_img],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=open(os.devnull, 'w'),
|
||||
stderr=subprocess.STDOUT)
|
||||
p.communicate('2048,\n')
|
||||
assert p.returncode == 0, 'sfdisk failed'
|
||||
# TODO: check if root_img is really file, not already block device
|
||||
p = subprocess.Popen(['sudo', 'losetup', '-f', '-P', '--show',
|
||||
vm.storage.root_img], stdout=subprocess.PIPE)
|
||||
(loopdev, _) = p.communicate()
|
||||
loopdev = loopdev.strip()
|
||||
looppart = loopdev + 'p1'
|
||||
assert p.returncode == 0, 'losetup failed'
|
||||
subprocess.check_call(['sudo', 'mkfs.ext2', '-q', '-F', looppart])
|
||||
mountpoint = tempfile.mkdtemp()
|
||||
subprocess.check_call(['sudo', 'mount', looppart, mountpoint])
|
||||
try:
|
||||
subprocess.check_call(['sudo', 'grub2-install',
|
||||
'--target', 'i386-pc',
|
||||
'--modules', 'part_msdos ext2',
|
||||
'--boot-directory', mountpoint, loopdev],
|
||||
stderr=open(os.devnull, 'w')
|
||||
)
|
||||
grub_cfg = '{}/grub2/grub.cfg'.format(mountpoint)
|
||||
subprocess.check_call(
|
||||
['sudo', 'chown', '-R', os.getlogin(), mountpoint])
|
||||
with open(grub_cfg, 'w') as f:
|
||||
f.write(
|
||||
"set timeout=1\n"
|
||||
"menuentry 'Default' {\n"
|
||||
" linux /vmlinuz root=/dev/xvda1 "
|
||||
"rd.driver.blacklist=bochs_drm "
|
||||
"rd.driver.blacklist=uhci_hcd\n"
|
||||
" initrd /initrd\n"
|
||||
"}"
|
||||
)
|
||||
p = subprocess.Popen(['uname', '-r'], stdout=subprocess.PIPE)
|
||||
(kernel_version, _) = p.communicate()
|
||||
kernel_version = kernel_version.strip()
|
||||
kernel = '/boot/vmlinuz-{}'.format(kernel_version)
|
||||
shutil.copy(kernel, os.path.join(mountpoint, 'vmlinuz'))
|
||||
init_path = os.path.join(mountpoint, 'init')
|
||||
with open(init_path, 'w') as f:
|
||||
f.write(init_script)
|
||||
os.chmod(init_path, 0755)
|
||||
dracut_args = [
|
||||
'--kver', kernel_version,
|
||||
'--include', init_path,
|
||||
'/usr/lib/dracut/hooks/pre-pivot/initscript.sh',
|
||||
'--no-hostonly', '--nolvmconf', '--nomdadmconf',
|
||||
]
|
||||
if extra_files:
|
||||
dracut_args += ['--install', ' '.join(extra_files)]
|
||||
subprocess.check_call(
|
||||
['dracut'] + dracut_args + [os.path.join(mountpoint,
|
||||
'initrd')],
|
||||
stderr=open(os.devnull, 'w')
|
||||
)
|
||||
finally:
|
||||
subprocess.check_call(['sudo', 'umount', mountpoint])
|
||||
shutil.rmtree(mountpoint)
|
||||
subprocess.check_call(['sudo', 'losetup', '-d', loopdev])
|
||||
|
||||
class BackupTestsMixin(SystemTestsMixin):
|
||||
def setUp(self):
|
||||
super(BackupTestsMixin, self).setUp()
|
||||
@ -381,39 +588,28 @@ class BackupTestsMixin(SystemTestsMixin):
|
||||
if self.verbose:
|
||||
print >>sys.stderr, "-> Creating backupvm"
|
||||
|
||||
# TODO: allow non-default template
|
||||
self.backupvm = self.qc.add_new_vm("QubesAppVm",
|
||||
name=self.make_vm_name('backupvm'),
|
||||
template=self.qc.get_default_template())
|
||||
self.backupvm.create_on_disk(verbose=self.verbose)
|
||||
|
||||
self.backupdir = os.path.join(os.environ["HOME"], "test-backup")
|
||||
if os.path.exists(self.backupdir):
|
||||
shutil.rmtree(self.backupdir)
|
||||
os.mkdir(self.backupdir)
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
super(BackupTestsMixin, self).tearDown()
|
||||
shutil.rmtree(self.backupdir)
|
||||
|
||||
|
||||
def print_progress(self, progress):
|
||||
if self.verbose:
|
||||
print >> sys.stderr, "\r-> Backing up files: {0}%...".format(progress)
|
||||
|
||||
|
||||
def error_callback(self, message):
|
||||
self.error_detected.put(message)
|
||||
if self.verbose:
|
||||
print >> sys.stderr, "ERROR: {0}".format(message)
|
||||
|
||||
|
||||
def print_callback(self, msg):
|
||||
if self.verbose:
|
||||
print msg
|
||||
|
||||
|
||||
def fill_image(self, path, size=None, sparse=False):
|
||||
block_size = 4096
|
||||
|
||||
@ -432,17 +628,28 @@ class BackupTestsMixin(SystemTestsMixin):
|
||||
|
||||
f.close()
|
||||
|
||||
|
||||
# NOTE: this was create_basic_vms
|
||||
def create_backup_vms(self):
|
||||
template=self.qc.get_default_template()
|
||||
|
||||
vms = []
|
||||
vmname = self.make_vm_name('test-net')
|
||||
if self.verbose:
|
||||
print >>sys.stderr, "-> Creating %s" % vmname
|
||||
testnet = self.qc.add_new_vm('QubesNetVm',
|
||||
name=vmname, template=template)
|
||||
testnet.create_on_disk(verbose=self.verbose)
|
||||
testnet.services['ntpd'] = True
|
||||
vms.append(testnet)
|
||||
self.fill_image(testnet.private_img, 20*1024*1024)
|
||||
|
||||
vmname = self.make_vm_name('test1')
|
||||
if self.verbose:
|
||||
print >>sys.stderr, "-> Creating %s" % vmname
|
||||
testvm1 = self.qc.add_new_vm('QubesAppVm',
|
||||
name=vmname, template=template)
|
||||
testvm1.uses_default_netvm = False
|
||||
testvm1.netvm = testnet
|
||||
testvm1.create_on_disk(verbose=self.verbose)
|
||||
vms.append(testvm1)
|
||||
self.fill_image(testvm1.private_img, 100*1024*1024)
|
||||
@ -451,6 +658,8 @@ class BackupTestsMixin(SystemTestsMixin):
|
||||
if self.verbose:
|
||||
print >>sys.stderr, "-> Creating %s" % vmname
|
||||
testvm2 = self.qc.add_new_vm('QubesHVm', name=vmname)
|
||||
# fixup - uses_default_netvm=True anyway
|
||||
testvm2.netvm = self.qc.get_default_netvm()
|
||||
testvm2.create_on_disk(verbose=self.verbose)
|
||||
self.fill_image(testvm2.root_img, 1024*1024*1024, True)
|
||||
vms.append(testvm2)
|
||||
@ -459,9 +668,8 @@ class BackupTestsMixin(SystemTestsMixin):
|
||||
|
||||
return vms
|
||||
|
||||
|
||||
def make_backup(self, vms, prepare_kwargs=dict(), do_kwargs=dict(),
|
||||
target=None):
|
||||
target=None, expect_failure=False):
|
||||
# XXX: bakup_prepare and backup_do don't support host_collection
|
||||
self.qc.unlock_db()
|
||||
if target is None:
|
||||
@ -472,19 +680,24 @@ class BackupTestsMixin(SystemTestsMixin):
|
||||
print_callback=self.print_callback,
|
||||
**prepare_kwargs)
|
||||
except qubes.qubes.QubesException as e:
|
||||
self.fail("QubesException during backup_prepare: %s" % str(e))
|
||||
if not expect_failure:
|
||||
self.fail("QubesException during backup_prepare: %s" % str(e))
|
||||
else:
|
||||
raise
|
||||
|
||||
try:
|
||||
qubes.backup.backup_do(target, files_to_backup, "qubes",
|
||||
progress_callback=self.print_progress,
|
||||
**do_kwargs)
|
||||
except qubes.qubes.QubesException as e:
|
||||
self.fail("QubesException during backup_do: %s" % str(e))
|
||||
if not expect_failure:
|
||||
self.fail("QubesException during backup_do: %s" % str(e))
|
||||
else:
|
||||
raise
|
||||
|
||||
self.qc.lock_db_for_writing()
|
||||
self.qc.load()
|
||||
|
||||
|
||||
def restore_backup(self, source=None, appvm=None, options=None,
|
||||
expect_errors=None):
|
||||
if source is None:
|
||||
@ -528,7 +741,6 @@ class BackupTestsMixin(SystemTestsMixin):
|
||||
if not appvm and not os.path.isdir(backupfile):
|
||||
os.unlink(backupfile)
|
||||
|
||||
|
||||
def create_sparse(self, path, size):
|
||||
f = open(path, "w")
|
||||
f.truncate(size)
|
||||
@ -543,13 +755,21 @@ def load_tests(loader, tests, pattern):
|
||||
'qubes.tests.basic',
|
||||
'qubes.tests.dom0_update',
|
||||
'qubes.tests.network',
|
||||
'qubes.tests.dispvm',
|
||||
'qubes.tests.vm_qrexec_gui',
|
||||
'qubes.tests.mime',
|
||||
'qubes.tests.hvm',
|
||||
'qubes.tests.pvgrub',
|
||||
'qubes.tests.backup',
|
||||
'qubes.tests.backupcompatibility',
|
||||
'qubes.tests.regressions',
|
||||
'qubes.tests.storage',
|
||||
'qubes.tests.storage_xen',
|
||||
'qubes.tests.block',
|
||||
'qubes.tests.hardware',
|
||||
'qubes.tests.extra',
|
||||
):
|
||||
tests.addTests(loader.loadTestsFromName(modname))
|
||||
|
||||
return tests
|
||||
|
||||
|
||||
|
||||
160
tests/backup.py
160
tests/backup.py
@ -28,7 +28,7 @@ import os
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
|
||||
from qubes.qubes import QubesException, QubesTemplateVm
|
||||
import qubes.tests
|
||||
|
||||
class TC_00_Backup(qubes.tests.BackupTestsMixin, qubes.tests.QubesTestCase):
|
||||
@ -37,6 +37,32 @@ class TC_00_Backup(qubes.tests.BackupTestsMixin, qubes.tests.QubesTestCase):
|
||||
self.make_backup(vms)
|
||||
self.remove_vms(vms)
|
||||
self.restore_backup()
|
||||
for vm in vms:
|
||||
restored_vm = self.qc.get_vm_by_name(vm.name)
|
||||
for prop in ('name', 'kernel', 'uses_default_kernel',
|
||||
'uses_default_netvm', 'memory', 'maxmem', 'kernelopts',
|
||||
'uses_default_kernelopts', 'services', 'vcpus', 'pcidevs',
|
||||
'include_in_backups', 'default_user', 'qrexec_timeout',
|
||||
'autostart', 'pci_strictreset', 'pci_e820_host', 'debug',
|
||||
'internal'):
|
||||
if prop not in vm.get_attrs_config():
|
||||
continue
|
||||
self.assertEquals(
|
||||
getattr(vm, prop), getattr(restored_vm, prop),
|
||||
"VM {} - property {} not properly restored".format(
|
||||
vm.name, prop))
|
||||
for prop in ('netvm', 'template', 'label'):
|
||||
orig_value = getattr(vm, prop)
|
||||
restored_value = getattr(restored_vm, prop)
|
||||
if orig_value and restored_value:
|
||||
self.assertEquals(orig_value.name, restored_value.name,
|
||||
"VM {} - property {} not properly restored".format(
|
||||
vm.name, prop))
|
||||
else:
|
||||
self.assertEquals(orig_value, restored_value,
|
||||
"VM {} - property {} not properly restored".format(
|
||||
vm.name, prop))
|
||||
|
||||
self.remove_vms(vms)
|
||||
|
||||
def test_001_compressed_backup(self):
|
||||
@ -63,7 +89,6 @@ class TC_00_Backup(qubes.tests.BackupTestsMixin, qubes.tests.QubesTestCase):
|
||||
self.restore_backup()
|
||||
self.remove_vms(vms)
|
||||
|
||||
|
||||
def test_004_sparse_multipart(self):
|
||||
vms = []
|
||||
|
||||
@ -85,22 +110,17 @@ class TC_00_Backup(qubes.tests.BackupTestsMixin, qubes.tests.QubesTestCase):
|
||||
self.restore_backup()
|
||||
self.remove_vms(vms)
|
||||
|
||||
|
||||
# TODO: iterate over templates
|
||||
def test_100_send_to_vm(self):
|
||||
def test_005_compressed_custom(self):
|
||||
vms = self.create_backup_vms()
|
||||
self.backupvm.start()
|
||||
self.make_backup(vms,
|
||||
do_kwargs={
|
||||
'appvm': self.backupvm,
|
||||
'compressed': True,
|
||||
'encrypted': True},
|
||||
target='dd of=/var/tmp/backup-test')
|
||||
self.make_backup(vms, do_kwargs={'compressed': "bzip2"})
|
||||
self.remove_vms(vms)
|
||||
self.restore_backup(source='dd if=/var/tmp/backup-test',
|
||||
appvm=self.backupvm)
|
||||
self.restore_backup()
|
||||
self.remove_vms(vms)
|
||||
|
||||
def test_100_backup_dom0_no_restore(self):
|
||||
self.make_backup([self.qc[0]])
|
||||
# TODO: think of some safe way to test restore...
|
||||
|
||||
def test_200_restore_over_existing_directory(self):
|
||||
"""
|
||||
Regression test for #1386
|
||||
@ -119,3 +139,115 @@ class TC_00_Backup(qubes.tests.BackupTestsMixin, qubes.tests.QubesTestCase):
|
||||
test_dir)
|
||||
])
|
||||
self.remove_vms(vms)
|
||||
|
||||
def test_210_auto_rename(self):
|
||||
"""
|
||||
Test for #869
|
||||
:return:
|
||||
"""
|
||||
vms = self.create_backup_vms()
|
||||
self.make_backup(vms)
|
||||
self.restore_backup(options={
|
||||
'rename-conflicting': True
|
||||
})
|
||||
for vm in vms:
|
||||
self.assertIsNotNone(self.qc.get_vm_by_name(vm.name+'1'))
|
||||
restored_vm = self.qc.get_vm_by_name(vm.name+'1')
|
||||
if vm.netvm and not vm.uses_default_netvm:
|
||||
self.assertEqual(restored_vm.netvm.name, vm.netvm.name+'1')
|
||||
|
||||
self.remove_vms(vms)
|
||||
|
||||
class TC_10_BackupVMMixin(qubes.tests.BackupTestsMixin):
|
||||
def setUp(self):
|
||||
super(TC_10_BackupVMMixin, self).setUp()
|
||||
self.backupvm = self.qc.add_new_vm(
|
||||
"QubesAppVm",
|
||||
name=self.make_vm_name('backupvm'),
|
||||
template=self.qc.get_vm_by_name(self.template)
|
||||
)
|
||||
self.backupvm.create_on_disk(verbose=self.verbose)
|
||||
|
||||
def test_100_send_to_vm_file_with_spaces(self):
|
||||
vms = self.create_backup_vms()
|
||||
self.backupvm.start()
|
||||
self.backupvm.run("mkdir '/var/tmp/backup directory'", wait=True)
|
||||
self.make_backup(vms,
|
||||
do_kwargs={
|
||||
'appvm': self.backupvm,
|
||||
'compressed': True,
|
||||
'encrypted': True},
|
||||
target='/var/tmp/backup directory')
|
||||
self.remove_vms(vms)
|
||||
p = self.backupvm.run("ls /var/tmp/backup*/qubes-backup*",
|
||||
passio_popen=True)
|
||||
(backup_path, _) = p.communicate()
|
||||
backup_path = backup_path.strip()
|
||||
self.restore_backup(source=backup_path,
|
||||
appvm=self.backupvm)
|
||||
self.remove_vms(vms)
|
||||
|
||||
def test_110_send_to_vm_command(self):
|
||||
vms = self.create_backup_vms()
|
||||
self.backupvm.start()
|
||||
self.make_backup(vms,
|
||||
do_kwargs={
|
||||
'appvm': self.backupvm,
|
||||
'compressed': True,
|
||||
'encrypted': True},
|
||||
target='dd of=/var/tmp/backup-test')
|
||||
self.remove_vms(vms)
|
||||
self.restore_backup(source='dd if=/var/tmp/backup-test',
|
||||
appvm=self.backupvm)
|
||||
self.remove_vms(vms)
|
||||
|
||||
def test_110_send_to_vm_no_space(self):
|
||||
"""
|
||||
Check whether backup properly report failure when no enough space is
|
||||
available
|
||||
:return:
|
||||
"""
|
||||
vms = self.create_backup_vms()
|
||||
self.backupvm.start()
|
||||
retcode = self.backupvm.run(
|
||||
# Debian 7 has too old losetup to handle loop-control device
|
||||
"mknod /dev/loop0 b 7 0;"
|
||||
"truncate -s 50M /home/user/backup.img && "
|
||||
"mkfs.ext4 -F /home/user/backup.img && "
|
||||
"mkdir /home/user/backup && "
|
||||
"mount /home/user/backup.img /home/user/backup -o loop &&"
|
||||
"chmod 777 /home/user/backup",
|
||||
user="root", wait=True)
|
||||
if retcode != 0:
|
||||
raise RuntimeError("Failed to prepare backup directory")
|
||||
with self.assertRaises(QubesException):
|
||||
self.make_backup(vms,
|
||||
do_kwargs={
|
||||
'appvm': self.backupvm,
|
||||
'compressed': False,
|
||||
'encrypted': True},
|
||||
target='/home/user/backup',
|
||||
expect_failure=True)
|
||||
self.qc.lock_db_for_writing()
|
||||
self.qc.load()
|
||||
self.remove_vms(vms)
|
||||
|
||||
|
||||
def load_tests(loader, tests, pattern):
|
||||
try:
|
||||
qc = qubes.qubes.QubesVmCollection()
|
||||
qc.lock_db_for_reading()
|
||||
qc.load()
|
||||
qc.unlock_db()
|
||||
templates = [vm.name for vm in qc.values() if
|
||||
isinstance(vm, QubesTemplateVm)]
|
||||
except OSError:
|
||||
templates = []
|
||||
for template in templates:
|
||||
tests.addTests(loader.loadTestsFromTestCase(
|
||||
type(
|
||||
'TC_10_BackupVM_' + template,
|
||||
(TC_10_BackupVMMixin, qubes.tests.QubesTestCase),
|
||||
{'template': template})))
|
||||
|
||||
return tests
|
||||
|
||||
@ -146,6 +146,19 @@ compression-filter=gzip
|
||||
'''
|
||||
|
||||
class TC_00_BackupCompatibility(qubes.tests.BackupTestsMixin, qubes.tests.QubesTestCase):
|
||||
def tearDown(self):
|
||||
self.qc.unlock_db()
|
||||
self.qc.lock_db_for_writing()
|
||||
self.qc.load()
|
||||
|
||||
# Remove here as we use 'test-' prefix, instead of 'test-inst-'
|
||||
self._remove_test_vms(self.qc, self.conn, prefix="test-")
|
||||
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
|
||||
super(TC_00_BackupCompatibility, self).tearDown()
|
||||
|
||||
def create_whitelisted_appmenus(self, filename):
|
||||
f = open(filename, "w")
|
||||
f.write("gnome-terminal.desktop\n")
|
||||
@ -167,14 +180,20 @@ class TC_00_BackupCompatibility(qubes.tests.BackupTestsMixin, qubes.tests.QubesT
|
||||
|
||||
def create_volatile_img(self, filename):
|
||||
self.create_sparse(filename, 11.5*2**30)
|
||||
sfdisk_input="0,1024,S\n,10240,L\n"
|
||||
p = subprocess.Popen(["/usr/sbin/sfdisk", "--no-reread", "-u",
|
||||
"M",
|
||||
filename], stdout=open("/dev/null","w"),
|
||||
stderr=subprocess.STDOUT, stdin=subprocess.PIPE)
|
||||
p.communicate(input=sfdisk_input)
|
||||
self.assertEqual(p.returncode, 0, "sfdisk failed with code %d" % p
|
||||
.returncode)
|
||||
# here used to be sfdisk call with "0,1024,S\n,10240,L\n" input,
|
||||
# but since sfdisk folks like to change command arguments in
|
||||
# incompatible way, have an partition table verbatim here
|
||||
ptable = (
|
||||
'\x00\x00\x00\x00\x00\x00\x00\x00\xab\x39\xd5\xd4\x00\x00\x20\x00'
|
||||
'\x00\x21\xaa\x82\x82\x28\x08\x00\x00\x00\x00\x00\x00\x20\xaa\x00'
|
||||
'\x82\x29\x15\x83\x9c\x79\x08\x00\x00\x20\x00\x00\x01\x40\x00\x00'
|
||||
'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa\x55'
|
||||
)
|
||||
with open(filename, 'r+') as f:
|
||||
f.seek(0x1b0)
|
||||
f.write(ptable)
|
||||
|
||||
# TODO: mkswap
|
||||
|
||||
def fullpath(self, name):
|
||||
@ -424,6 +443,7 @@ class TC_00_BackupCompatibility(qubes.tests.BackupTestsMixin, qubes.tests.QubesT
|
||||
|
||||
self.restore_backup(self.backupdir, options={
|
||||
'use-default-template': True,
|
||||
'use-default-netvm': True,
|
||||
})
|
||||
self.assertIsNotNone(self.qc.get_vm_by_name("test-template-clone"))
|
||||
self.assertIsNotNone(self.qc.get_vm_by_name("test-testproxy"))
|
||||
|
||||
248
tests/basic.py
248
tests/basic.py
@ -22,15 +22,18 @@
|
||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
#
|
||||
from distutils import spawn
|
||||
|
||||
import multiprocessing
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
import unittest
|
||||
import time
|
||||
from qubes.qubes import QubesVmCollection, QubesException, system_path
|
||||
from qubes.qubes import QubesVmCollection, QubesException, system_path, vmm
|
||||
import libvirt
|
||||
|
||||
import qubes.qubes
|
||||
import qubes.tests
|
||||
@ -51,6 +54,26 @@ class TC_00_Basic(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
with self.assertNotRaises(qubes.qubes.QubesException):
|
||||
vm.verify_files()
|
||||
|
||||
def test_010_remove(self):
|
||||
vmname = self.make_vm_name('appvm')
|
||||
vm = self.qc.add_new_vm('QubesAppVm',
|
||||
name=vmname, template=self.qc.get_default_template())
|
||||
vm.create_on_disk(verbose=False)
|
||||
# check for QubesOS/qubes-issues#1930
|
||||
vm.autostart = True
|
||||
self.save_and_reload_db()
|
||||
vm = self.qc[vm.qid]
|
||||
vm.remove_from_disk()
|
||||
self.qc.pop(vm.qid)
|
||||
self.save_and_reload_db()
|
||||
self.assertNotIn(vm.qid, self.qc)
|
||||
self.assertFalse(os.path.exists(vm.dir_path))
|
||||
self.assertFalse(os.path.exists(
|
||||
'/etc/systemd/system/multi-user.target.wants/'
|
||||
'qubes-vm@{}.service'.format(vm.name)))
|
||||
with self.assertRaises(libvirt.libvirtError):
|
||||
vmm.libvirt_conn.lookupByName(vm.name)
|
||||
|
||||
|
||||
class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
def setUp(self):
|
||||
@ -113,6 +136,14 @@ class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
'/etc/systemd/system/multi-user.target.wants/'
|
||||
'qubes-vm@{}.service'.format(self.vmname)))
|
||||
|
||||
def test_001_rename_libvirt_undefined(self):
|
||||
self.vm.libvirt_domain.undefine()
|
||||
self.vm._libvirt_domain = None
|
||||
|
||||
newname = self.make_vm_name('newname')
|
||||
with self.assertNotRaises(libvirt.libvirtError):
|
||||
self.vm.set_name(newname)
|
||||
|
||||
def test_010_netvm(self):
|
||||
if self.qc.get_default_netvm() is None:
|
||||
self.skip("Set default NetVM before running this test")
|
||||
@ -258,6 +289,45 @@ class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
self.assertEquals(testvm1.get_firewall_conf(),
|
||||
testvm3.get_firewall_conf())
|
||||
|
||||
def test_020_name_conflict_app(self):
|
||||
with self.assertRaises(QubesException):
|
||||
self.vm2 = self.qc.add_new_vm('QubesAppVm',
|
||||
name=self.vmname, template=self.qc.get_default_template())
|
||||
self.vm2.create_on_disk(verbose=False)
|
||||
|
||||
def test_021_name_conflict_hvm(self):
|
||||
with self.assertRaises(QubesException):
|
||||
self.vm2 = self.qc.add_new_vm('QubesHVm',
|
||||
name=self.vmname, template=self.qc.get_default_template())
|
||||
self.vm2.create_on_disk(verbose=False)
|
||||
|
||||
def test_022_name_conflict_net(self):
|
||||
with self.assertRaises(QubesException):
|
||||
self.vm2 = self.qc.add_new_vm('QubesNetVm',
|
||||
name=self.vmname, template=self.qc.get_default_template())
|
||||
self.vm2.create_on_disk(verbose=False)
|
||||
|
||||
def test_030_rename_conflict_app(self):
|
||||
vm2name = self.make_vm_name('newname')
|
||||
|
||||
self.vm2 = self.qc.add_new_vm('QubesAppVm',
|
||||
name=vm2name, template=self.qc.get_default_template())
|
||||
self.vm2.create_on_disk(verbose=False)
|
||||
|
||||
with self.assertRaises(QubesException):
|
||||
self.vm2.set_name(self.vmname)
|
||||
|
||||
def test_031_rename_conflict_net(self):
|
||||
vm3name = self.make_vm_name('newname')
|
||||
|
||||
self.vm3 = self.qc.add_new_vm('QubesNetVm',
|
||||
name=vm3name, template=self.qc.get_default_template())
|
||||
self.vm3.create_on_disk(verbose=False)
|
||||
|
||||
with self.assertRaises(QubesException):
|
||||
self.vm3.set_name(self.vmname)
|
||||
|
||||
|
||||
class TC_02_QvmPrefs(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
def setup_appvm(self):
|
||||
self.testvm = self.qc.add_new_vm(
|
||||
@ -571,5 +641,181 @@ class TC_02_QvmPrefs(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
self.execute_tests('kernel', [('default', '', False)])
|
||||
self.execute_tests('kernelopts', [('default', '', False)])
|
||||
|
||||
class TC_03_QvmRevertTemplateChanges(qubes.tests.SystemTestsMixin,
|
||||
qubes.tests.QubesTestCase):
|
||||
|
||||
def setup_pv_template(self):
|
||||
self.test_template = self.qc.add_new_vm(
|
||||
"QubesTemplateVm",
|
||||
name=self.make_vm_name("pv-clone"),
|
||||
)
|
||||
self.test_template.clone_attrs(src_vm=self.qc.get_default_template())
|
||||
self.test_template.clone_disk_files(
|
||||
src_vm=self.qc.get_default_template(),
|
||||
verbose=False)
|
||||
self.save_and_reload_db()
|
||||
self.qc.unlock_db()
|
||||
|
||||
def setup_hvm_template(self):
|
||||
self.test_template = self.qc.add_new_vm(
|
||||
"QubesTemplateHVm",
|
||||
name=self.make_vm_name("hvm"),
|
||||
)
|
||||
self.test_template.create_on_disk(verbose=False)
|
||||
self.save_and_reload_db()
|
||||
self.qc.unlock_db()
|
||||
|
||||
def get_rootimg_checksum(self):
|
||||
p = subprocess.Popen(['sha1sum', self.test_template.root_img],
|
||||
stdout=subprocess.PIPE)
|
||||
return p.communicate()[0]
|
||||
|
||||
def _do_test(self):
|
||||
checksum_before = self.get_rootimg_checksum()
|
||||
self.test_template.start(verbose=False)
|
||||
self.shutdown_and_wait(self.test_template)
|
||||
checksum_changed = self.get_rootimg_checksum()
|
||||
if checksum_before == checksum_changed:
|
||||
self.log.warning("template not modified, test result will be "
|
||||
"unreliable")
|
||||
with self.assertNotRaises(subprocess.CalledProcessError):
|
||||
subprocess.check_call(['sudo', 'qvm-revert-template-changes',
|
||||
'--force', self.test_template.name])
|
||||
|
||||
checksum_after = self.get_rootimg_checksum()
|
||||
self.assertEquals(checksum_before, checksum_after)
|
||||
|
||||
def test_000_revert_pv(self):
|
||||
"""
|
||||
Test qvm-revert-template-changes for PV template
|
||||
"""
|
||||
self.setup_pv_template()
|
||||
self._do_test()
|
||||
|
||||
def test_000_revert_hvm(self):
|
||||
"""
|
||||
Test qvm-revert-template-changes for HVM template
|
||||
"""
|
||||
# TODO: have some system there, so the root.img will get modified
|
||||
self.setup_hvm_template()
|
||||
self._do_test()
|
||||
|
||||
|
||||
class TC_30_Gui_daemon(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
@unittest.skipUnless(spawn.find_executable('xdotool'),
|
||||
"xdotool not installed")
|
||||
def test_000_clipboard(self):
|
||||
testvm1 = self.qc.add_new_vm("QubesAppVm",
|
||||
name=self.make_vm_name('vm1'),
|
||||
template=self.qc.get_default_template())
|
||||
testvm1.create_on_disk(verbose=False)
|
||||
testvm2 = self.qc.add_new_vm("QubesAppVm",
|
||||
name=self.make_vm_name('vm2'),
|
||||
template=self.qc.get_default_template())
|
||||
testvm2.create_on_disk(verbose=False)
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
|
||||
testvm1.start()
|
||||
testvm2.start()
|
||||
|
||||
window_title = 'user@{}'.format(testvm1.name)
|
||||
testvm1.run('zenity --text-info --editable --title={}'.format(
|
||||
window_title))
|
||||
|
||||
self.wait_for_window(window_title)
|
||||
time.sleep(0.5)
|
||||
test_string = "test{}".format(testvm1.xid)
|
||||
|
||||
# Type and copy some text
|
||||
subprocess.check_call(['xdotool', 'search', '--name', window_title,
|
||||
'windowactivate', '--sync',
|
||||
'type', '{}'.format(test_string)])
|
||||
# second xdotool call because type --terminator do not work (SEGV)
|
||||
# additionally do not use search here, so window stack will be empty
|
||||
# and xdotool will use XTEST instead of generating events manually -
|
||||
# this will be much better - at least because events will have
|
||||
# correct timestamp (so gui-daemon would not drop the copy request)
|
||||
subprocess.check_call(['xdotool',
|
||||
'key', 'ctrl+a', 'ctrl+c', 'ctrl+shift+c',
|
||||
'Escape'])
|
||||
|
||||
clipboard_content = \
|
||||
open('/var/run/qubes/qubes-clipboard.bin', 'r').read().strip()
|
||||
self.assertEquals(clipboard_content, test_string,
|
||||
"Clipboard copy operation failed - content")
|
||||
clipboard_source = \
|
||||
open('/var/run/qubes/qubes-clipboard.bin.source',
|
||||
'r').read().strip()
|
||||
self.assertEquals(clipboard_source, testvm1.name,
|
||||
"Clipboard copy operation failed - owner")
|
||||
|
||||
# Then paste it to the other window
|
||||
window_title = 'user@{}'.format(testvm2.name)
|
||||
p = testvm2.run('zenity --entry --title={} > test.txt'.format(
|
||||
window_title), passio_popen=True)
|
||||
self.wait_for_window(window_title)
|
||||
|
||||
subprocess.check_call(['xdotool', 'key', '--delay', '100',
|
||||
'ctrl+shift+v', 'ctrl+v', 'Return'])
|
||||
p.wait()
|
||||
|
||||
# And compare the result
|
||||
(test_output, _) = testvm2.run('cat test.txt',
|
||||
passio_popen=True).communicate()
|
||||
self.assertEquals(test_string, test_output.strip())
|
||||
|
||||
clipboard_content = \
|
||||
open('/var/run/qubes/qubes-clipboard.bin', 'r').read().strip()
|
||||
self.assertEquals(clipboard_content, "",
|
||||
"Clipboard not wiped after paste - content")
|
||||
clipboard_source = \
|
||||
open('/var/run/qubes/qubes-clipboard.bin.source', 'r').read(
|
||||
|
||||
).strip()
|
||||
self.assertEquals(clipboard_source, "",
|
||||
"Clipboard not wiped after paste - owner")
|
||||
|
||||
class TC_05_StandaloneVM(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
def test_000_create_start(self):
|
||||
testvm1 = self.qc.add_new_vm("QubesAppVm",
|
||||
template=None,
|
||||
name=self.make_vm_name('vm1'))
|
||||
testvm1.create_on_disk(verbose=False,
|
||||
source_template=self.qc.get_default_template())
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
testvm1.start()
|
||||
self.assertEquals(testvm1.get_power_state(), "Running")
|
||||
|
||||
def test_100_resize_root_img(self):
|
||||
testvm1 = self.qc.add_new_vm("QubesAppVm",
|
||||
template=None,
|
||||
name=self.make_vm_name('vm1'))
|
||||
testvm1.create_on_disk(verbose=False,
|
||||
source_template=self.qc.get_default_template())
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
with self.assertRaises(QubesException):
|
||||
testvm1.resize_root_img(20*1024**3)
|
||||
testvm1.resize_root_img(20*1024**3, allow_start=True)
|
||||
timeout = 60
|
||||
while testvm1.is_running():
|
||||
time.sleep(1)
|
||||
timeout -= 1
|
||||
if timeout == 0:
|
||||
self.fail("Timeout while waiting for VM shutdown")
|
||||
self.assertEquals(testvm1.get_root_img_sz(), 20*1024**3)
|
||||
testvm1.start()
|
||||
p = testvm1.run('df --output=size /|tail -n 1',
|
||||
passio_popen=True)
|
||||
# new_size in 1k-blocks
|
||||
(new_size, _) = p.communicate()
|
||||
# some safety margin for FS metadata
|
||||
self.assertGreater(int(new_size.strip()), 19*1024**2)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# vim: ts=4 sw=4 et
|
||||
|
||||
310
tests/block.py
Normal file
310
tests/block.py
Normal file
@ -0,0 +1,310 @@
|
||||
# vim: fileencoding=utf-8
|
||||
#
|
||||
# The Qubes OS Project, https://www.qubes-os.org/
|
||||
#
|
||||
# Copyright (C) 2016
|
||||
# Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
|
||||
#
|
||||
# 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 qubes.tests
|
||||
import qubes.qubesutils
|
||||
import subprocess
|
||||
|
||||
# the same class for both dom0 and VMs
|
||||
class TC_00_List(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
template = None
|
||||
|
||||
def setUp(self):
|
||||
super(TC_00_List, self).setUp()
|
||||
self.img_path = '/tmp/test.img'
|
||||
self.mount_point = '/tmp/test-dir'
|
||||
if self.template is not None:
|
||||
self.vm = self.qc.add_new_vm(
|
||||
"QubesAppVm",
|
||||
name=self.make_vm_name("vm"),
|
||||
template=self.qc.get_vm_by_name(self.template))
|
||||
self.vm.create_on_disk(verbose=False)
|
||||
self.save_and_reload_db()
|
||||
self.qc.unlock_db()
|
||||
self.vm.start()
|
||||
else:
|
||||
self.qc.unlock_db()
|
||||
self.vm = self.qc[0]
|
||||
|
||||
def tearDown(self):
|
||||
super(TC_00_List, self).tearDown()
|
||||
if self.template is None:
|
||||
if os.path.exists(self.mount_point):
|
||||
subprocess.call(['sudo', 'umount', self.mount_point])
|
||||
subprocess.call(['sudo', 'rmdir', self.mount_point])
|
||||
subprocess.call(['sudo', 'dmsetup', 'remove', 'test-dm'])
|
||||
if os.path.exists(self.img_path):
|
||||
loopdev = subprocess.check_output(['losetup', '-j',
|
||||
self.img_path])
|
||||
for dev in loopdev.splitlines():
|
||||
subprocess.call(
|
||||
['sudo', 'losetup', '-d', dev.split(':')[0]])
|
||||
subprocess.call(['sudo', 'rm', '-f', self.img_path])
|
||||
|
||||
def run_script(self, script, user="user"):
|
||||
if self.template is None:
|
||||
if user == "user":
|
||||
subprocess.check_call(script, shell=True)
|
||||
elif user == "root":
|
||||
subprocess.check_call(['sudo', 'sh', '-c', script])
|
||||
else:
|
||||
retcode = self.vm.run(script, user=user, wait=True)
|
||||
if retcode != 0:
|
||||
raise subprocess.CalledProcessError
|
||||
|
||||
def test_000_list_loop(self):
|
||||
if self.template is None:
|
||||
self.skipTest('loop devices excluded in dom0')
|
||||
self.run_script(
|
||||
"set -e;"
|
||||
"truncate -s 128M {path}; "
|
||||
"losetup -f {path}; "
|
||||
"udevadm settle".format(path=self.img_path), user="root")
|
||||
|
||||
dev_list = qubes.qubesutils.block_list_vm(self.vm)
|
||||
found = False
|
||||
for dev in dev_list.keys():
|
||||
if dev_list[dev]['desc'] == self.img_path:
|
||||
self.assertTrue(dev.startswith(self.vm.name + ':loop'))
|
||||
self.assertEquals(dev_list[dev]['mode'], 'w')
|
||||
self.assertEquals(dev_list[dev]['size'], 1024 * 1024 * 128)
|
||||
self.assertEquals(
|
||||
dev_list[dev]['device'], '/dev/' + dev.split(':')[1])
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
self.fail("Device {} not found in {!r}".format(self.img_path, dev_list))
|
||||
|
||||
def test_001_list_loop_mounted(self):
|
||||
if self.template is None:
|
||||
self.skipTest('loop devices excluded in dom0')
|
||||
self.run_script(
|
||||
"set -e;"
|
||||
"truncate -s 128M {path}; "
|
||||
"mkfs.ext4 -q -F {path}; "
|
||||
"mkdir -p {mntdir}; "
|
||||
"mount {path} {mntdir} -o loop; "
|
||||
"udevadm settle".format(
|
||||
path=self.img_path,
|
||||
mntdir=self.mount_point),
|
||||
user="root")
|
||||
|
||||
dev_list = qubes.qubesutils.block_list_vm(self.vm)
|
||||
for dev in dev_list.keys():
|
||||
if dev_list[dev]['desc'] == self.img_path:
|
||||
self.fail(
|
||||
'Device {} ({}) should not be listed because is mounted'
|
||||
.format(dev, self.img_path))
|
||||
|
||||
def test_010_list_dm(self):
|
||||
self.run_script(
|
||||
"set -e;"
|
||||
"truncate -s 128M {path}; "
|
||||
"loopdev=`losetup -f`; "
|
||||
"losetup $loopdev {path}; "
|
||||
"dmsetup create test-dm --table \"0 262144 linear $(cat "
|
||||
"/sys/block/$(basename $loopdev)/dev) 0\";"
|
||||
"udevadm settle".format(path=self.img_path), user="root")
|
||||
|
||||
dev_list = qubes.qubesutils.block_list_vm(self.vm)
|
||||
found = False
|
||||
for dev in dev_list.keys():
|
||||
if dev.startswith(self.vm.name + ':loop'):
|
||||
self.assertNotEquals(dev_list[dev]['desc'], self.img_path,
|
||||
"Device {} ({}) should not be listed as it is used in "
|
||||
"device-mapper".format(dev, self.img_path)
|
||||
)
|
||||
elif dev_list[dev]['desc'] == 'test-dm':
|
||||
self.assertEquals(dev_list[dev]['mode'], 'w')
|
||||
self.assertEquals(dev_list[dev]['size'], 1024 * 1024 * 128)
|
||||
self.assertEquals(
|
||||
dev_list[dev]['device'], '/dev/' + dev.split(':')[1])
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
self.fail("Device {} not found in {!r}".format('test-dm', dev_list))
|
||||
|
||||
def test_011_list_dm_mounted(self):
|
||||
self.run_script(
|
||||
"set -e;"
|
||||
"truncate -s 128M {path}; "
|
||||
"loopdev=`losetup -f`; "
|
||||
"losetup $loopdev {path}; "
|
||||
"dmsetup create test-dm --table \"0 262144 linear $(cat "
|
||||
"/sys/block/$(basename $loopdev)/dev) 0\";"
|
||||
"mkfs.ext4 -q -F /dev/mapper/test-dm;"
|
||||
"mkdir -p {mntdir};"
|
||||
"mount /dev/mapper/test-dm {mntdir};"
|
||||
"udevadm settle".format(
|
||||
path=self.img_path,
|
||||
mntdir=self.mount_point),
|
||||
user="root")
|
||||
|
||||
dev_list = qubes.qubesutils.block_list_vm(self.vm)
|
||||
for dev in dev_list.keys():
|
||||
if dev.startswith(self.vm.name + ':loop'):
|
||||
self.assertNotEquals(dev_list[dev]['desc'], self.img_path,
|
||||
"Device {} ({}) should not be listed as it is used in "
|
||||
"device-mapper".format(dev, self.img_path)
|
||||
)
|
||||
else:
|
||||
self.assertNotEquals(dev_list[dev]['desc'], 'test-dm',
|
||||
"Device {} ({}) should not be listed as it is "
|
||||
"mounted".format(dev, 'test-dm')
|
||||
)
|
||||
|
||||
def test_012_list_dm_delayed(self):
|
||||
self.run_script(
|
||||
"set -e;"
|
||||
"truncate -s 128M {path}; "
|
||||
"loopdev=`losetup -f`; "
|
||||
"losetup $loopdev {path}; "
|
||||
"udevadm settle; "
|
||||
"dmsetup create test-dm --table \"0 262144 linear $(cat "
|
||||
"/sys/block/$(basename $loopdev)/dev) 0\";"
|
||||
"udevadm settle".format(path=self.img_path), user="root")
|
||||
|
||||
dev_list = qubes.qubesutils.block_list_vm(self.vm)
|
||||
found = False
|
||||
for dev in dev_list.keys():
|
||||
if dev.startswith(self.vm.name + ':loop'):
|
||||
self.assertNotEquals(dev_list[dev]['desc'], self.img_path,
|
||||
"Device {} ({}) should not be listed as it is used in "
|
||||
"device-mapper".format(dev, self.img_path)
|
||||
)
|
||||
elif dev_list[dev]['desc'] == 'test-dm':
|
||||
self.assertEquals(dev_list[dev]['mode'], 'w')
|
||||
self.assertEquals(dev_list[dev]['size'], 1024 * 1024 * 128)
|
||||
self.assertEquals(
|
||||
dev_list[dev]['device'], '/dev/' + dev.split(':')[1])
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
self.fail("Device {} not found in {!r}".format('test-dm', dev_list))
|
||||
|
||||
def test_013_list_dm_removed(self):
|
||||
if self.template is None:
|
||||
self.skipTest('test not supported in dom0 - loop devices excluded '
|
||||
'in dom0')
|
||||
self.run_script(
|
||||
"set -e;"
|
||||
"truncate -s 128M {path}; "
|
||||
"loopdev=`losetup -f`; "
|
||||
"losetup $loopdev {path}; "
|
||||
"dmsetup create test-dm --table \"0 262144 linear $(cat "
|
||||
"/sys/block/$(basename $loopdev)/dev) 0\";"
|
||||
"udevadm settle;"
|
||||
"dmsetup remove test-dm;"
|
||||
"udevadm settle".format(path=self.img_path), user="root")
|
||||
|
||||
dev_list = qubes.qubesutils.block_list_vm(self.vm)
|
||||
found = False
|
||||
for dev in dev_list.keys():
|
||||
if dev_list[dev]['desc'] == self.img_path:
|
||||
self.assertTrue(dev.startswith(self.vm.name + ':loop'))
|
||||
self.assertEquals(dev_list[dev]['mode'], 'w')
|
||||
self.assertEquals(dev_list[dev]['size'], 1024 * 1024 * 128)
|
||||
self.assertEquals(
|
||||
dev_list[dev]['device'], '/dev/' + dev.split(':')[1])
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
self.fail("Device {} not found in {!r}".format(self.img_path, dev_list))
|
||||
|
||||
def test_020_list_loop_partition(self):
|
||||
if self.template is None:
|
||||
self.skipTest('loop devices excluded in dom0')
|
||||
self.run_script(
|
||||
"set -e;"
|
||||
"truncate -s 128M {path}; "
|
||||
"echo ,,L | sfdisk {path};"
|
||||
"loopdev=`losetup -f`; "
|
||||
"losetup -P $loopdev {path}; "
|
||||
"blockdev --rereadpt $loopdev; "
|
||||
"udevadm settle".format(path=self.img_path), user="root")
|
||||
|
||||
dev_list = qubes.qubesutils.block_list_vm(self.vm)
|
||||
found = False
|
||||
for dev in dev_list.keys():
|
||||
if dev_list[dev]['desc'] == self.img_path:
|
||||
self.assertTrue(dev.startswith(self.vm.name + ':loop'))
|
||||
self.assertEquals(dev_list[dev]['mode'], 'w')
|
||||
self.assertEquals(dev_list[dev]['size'], 1024 * 1024 * 128)
|
||||
self.assertEquals(
|
||||
dev_list[dev]['device'], '/dev/' + dev.split(':')[1])
|
||||
self.assertIn(dev + 'p1', dev_list)
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
self.fail("Device {} not found in {!r}".format(self.img_path, dev_list))
|
||||
|
||||
def test_021_list_loop_partition_mounted(self):
|
||||
if self.template is None:
|
||||
self.skipTest('loop devices excluded in dom0')
|
||||
self.run_script(
|
||||
"set -e;"
|
||||
"truncate -s 128M {path}; "
|
||||
"echo ,,L | sfdisk {path};"
|
||||
"loopdev=`losetup -f`; "
|
||||
"losetup -P $loopdev {path}; "
|
||||
"blockdev --rereadpt $loopdev; "
|
||||
"mkfs.ext4 -q -F ${{loopdev}}p1; "
|
||||
"mkdir -p {mntdir}; "
|
||||
"mount ${{loopdev}}p1 {mntdir}; "
|
||||
"udevadm settle".format(
|
||||
path=self.img_path, mntdir=self.mount_point),
|
||||
user="root")
|
||||
|
||||
dev_list = qubes.qubesutils.block_list_vm(self.vm)
|
||||
for dev in dev_list.keys():
|
||||
if dev_list[dev]['desc'] == self.img_path:
|
||||
self.fail(
|
||||
'Device {} ({}) should not be listed because its '
|
||||
'partition is mounted'
|
||||
.format(dev, self.img_path))
|
||||
elif dev.startswith(self.vm.name + ':loop') and dev.endswith('p1'):
|
||||
# FIXME: risky assumption that only tests create partitioned
|
||||
# loop devices
|
||||
self.fail(
|
||||
'Device {} ({}) should not be listed because is mounted'
|
||||
.format(dev, self.img_path))
|
||||
|
||||
|
||||
def load_tests(loader, tests, pattern):
|
||||
try:
|
||||
qc = qubes.qubes.QubesVmCollection()
|
||||
qc.lock_db_for_reading()
|
||||
qc.load()
|
||||
qc.unlock_db()
|
||||
templates = [vm.name for vm in qc.values() if
|
||||
isinstance(vm, qubes.qubes.QubesTemplateVm)]
|
||||
except OSError:
|
||||
templates = []
|
||||
for template in templates:
|
||||
tests.addTests(loader.loadTestsFromTestCase(
|
||||
type(
|
||||
'TC_00_List_' + template,
|
||||
(TC_00_List, qubes.tests.QubesTestCase),
|
||||
{'template': template})))
|
||||
|
||||
return tests
|
||||
448
tests/dispvm.py
Normal file
448
tests/dispvm.py
Normal file
@ -0,0 +1,448 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2016 Marek Marczykowski-Górecki
|
||||
# <marmarek@invisiblethingslab.com>
|
||||
#
|
||||
# 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 distutils import spawn
|
||||
import qubes.tests
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
import os
|
||||
import time
|
||||
|
||||
class TC_04_DispVM(qubes.tests.SystemTestsMixin,
|
||||
qubes.tests.QubesTestCase):
|
||||
|
||||
@staticmethod
|
||||
def get_dispvm_template_name():
|
||||
vmdir = os.readlink('/var/lib/qubes/dvmdata/vmdir')
|
||||
return os.path.basename(vmdir)
|
||||
|
||||
def test_000_firewall_propagation(self):
|
||||
"""
|
||||
Check firewall propagation VM->DispVM, when VM have some firewall rules
|
||||
"""
|
||||
|
||||
# FIXME: currently qubes.xml doesn't contain this information...
|
||||
dispvm_template_name = self.get_dispvm_template_name()
|
||||
dispvm_template = self.qc.get_vm_by_name(dispvm_template_name)
|
||||
|
||||
testvm1 = self.qc.add_new_vm("QubesAppVm",
|
||||
name=self.make_vm_name('vm1'),
|
||||
template=self.qc.get_default_template())
|
||||
testvm1.create_on_disk(verbose=False)
|
||||
firewall = testvm1.get_firewall_conf()
|
||||
firewall['allowDns'] = False
|
||||
firewall['allowYumProxy'] = False
|
||||
firewall['rules'] = [{'address': '1.2.3.4',
|
||||
'netmask': 24,
|
||||
'proto': 'tcp',
|
||||
'portBegin': 22,
|
||||
'portEnd': 22,
|
||||
}]
|
||||
testvm1.write_firewall_conf(firewall)
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
|
||||
testvm1.start()
|
||||
|
||||
p = testvm1.run("qvm-run --dispvm 'qubesdb-read /name; echo ERROR;"
|
||||
" read x'",
|
||||
passio_popen=True)
|
||||
|
||||
dispvm_name = p.stdout.readline().strip()
|
||||
self.qc.lock_db_for_reading()
|
||||
self.qc.load()
|
||||
self.qc.unlock_db()
|
||||
dispvm = self.qc.get_vm_by_name(dispvm_name)
|
||||
self.assertIsNotNone(dispvm, "DispVM {} not found in qubes.xml".format(
|
||||
dispvm_name))
|
||||
# check if firewall was propagated to the DispVM
|
||||
self.assertEquals(testvm1.get_firewall_conf(),
|
||||
dispvm.get_firewall_conf())
|
||||
# and only there (#1608)
|
||||
self.assertNotEquals(dispvm_template.get_firewall_conf(),
|
||||
dispvm.get_firewall_conf())
|
||||
# then modify some rule
|
||||
firewall = dispvm.get_firewall_conf()
|
||||
firewall['rules'] = [{'address': '4.3.2.1',
|
||||
'netmask': 24,
|
||||
'proto': 'tcp',
|
||||
'portBegin': 22,
|
||||
'portEnd': 22,
|
||||
}]
|
||||
dispvm.write_firewall_conf(firewall)
|
||||
# and check again if wasn't saved anywhere else (#1608)
|
||||
self.assertNotEquals(dispvm_template.get_firewall_conf(),
|
||||
dispvm.get_firewall_conf())
|
||||
self.assertNotEquals(testvm1.get_firewall_conf(),
|
||||
dispvm.get_firewall_conf())
|
||||
p.stdin.write('\n')
|
||||
p.wait()
|
||||
|
||||
def test_001_firewall_propagation(self):
|
||||
"""
|
||||
Check firewall propagation VM->DispVM, when VM have no firewall rules
|
||||
"""
|
||||
testvm1 = self.qc.add_new_vm("QubesAppVm",
|
||||
name=self.make_vm_name('vm1'),
|
||||
template=self.qc.get_default_template())
|
||||
testvm1.create_on_disk(verbose=False)
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
|
||||
# FIXME: currently qubes.xml doesn't contain this information...
|
||||
dispvm_template_name = self.get_dispvm_template_name()
|
||||
dispvm_template = self.qc.get_vm_by_name(dispvm_template_name)
|
||||
original_firewall = None
|
||||
if os.path.exists(dispvm_template.firewall_conf):
|
||||
original_firewall = tempfile.TemporaryFile()
|
||||
with open(dispvm_template.firewall_conf) as f:
|
||||
original_firewall.write(f.read())
|
||||
try:
|
||||
|
||||
firewall = dispvm_template.get_firewall_conf()
|
||||
firewall['allowDns'] = False
|
||||
firewall['allowYumProxy'] = False
|
||||
firewall['rules'] = [{'address': '1.2.3.4',
|
||||
'netmask': 24,
|
||||
'proto': 'tcp',
|
||||
'portBegin': 22,
|
||||
'portEnd': 22,
|
||||
}]
|
||||
dispvm_template.write_firewall_conf(firewall)
|
||||
|
||||
testvm1.start()
|
||||
|
||||
p = testvm1.run("qvm-run --dispvm 'qubesdb-read /name; echo ERROR;"
|
||||
" read x'",
|
||||
passio_popen=True)
|
||||
|
||||
dispvm_name = p.stdout.readline().strip()
|
||||
self.qc.lock_db_for_reading()
|
||||
self.qc.load()
|
||||
self.qc.unlock_db()
|
||||
dispvm = self.qc.get_vm_by_name(dispvm_name)
|
||||
self.assertIsNotNone(dispvm, "DispVM {} not found in qubes.xml".format(
|
||||
dispvm_name))
|
||||
# check if firewall was propagated to the DispVM from the right VM
|
||||
self.assertEquals(testvm1.get_firewall_conf(),
|
||||
dispvm.get_firewall_conf())
|
||||
# and only there (#1608)
|
||||
self.assertNotEquals(dispvm_template.get_firewall_conf(),
|
||||
dispvm.get_firewall_conf())
|
||||
# then modify some rule
|
||||
firewall = dispvm.get_firewall_conf()
|
||||
firewall['rules'] = [{'address': '4.3.2.1',
|
||||
'netmask': 24,
|
||||
'proto': 'tcp',
|
||||
'portBegin': 22,
|
||||
'portEnd': 22,
|
||||
}]
|
||||
dispvm.write_firewall_conf(firewall)
|
||||
# and check again if wasn't saved anywhere else (#1608)
|
||||
self.assertNotEquals(dispvm_template.get_firewall_conf(),
|
||||
dispvm.get_firewall_conf())
|
||||
self.assertNotEquals(testvm1.get_firewall_conf(),
|
||||
dispvm.get_firewall_conf())
|
||||
p.stdin.write('\n')
|
||||
p.wait()
|
||||
finally:
|
||||
if original_firewall:
|
||||
original_firewall.seek(0)
|
||||
with open(dispvm_template.firewall_conf, 'w') as f:
|
||||
f.write(original_firewall.read())
|
||||
original_firewall.close()
|
||||
else:
|
||||
os.unlink(dispvm_template.firewall_conf)
|
||||
|
||||
def test_002_cleanup(self):
|
||||
self.qc.unlock_db()
|
||||
p = subprocess.Popen(['/usr/lib/qubes/qfile-daemon-dvm',
|
||||
'qubes.VMShell', 'dom0', 'DEFAULT'],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=open(os.devnull, 'w'))
|
||||
(stdout, _) = p.communicate(input="echo test; qubesdb-read /name; "
|
||||
"echo ERROR\n")
|
||||
self.assertEquals(p.returncode, 0)
|
||||
lines = stdout.splitlines()
|
||||
self.assertEqual(lines[0], "test")
|
||||
dispvm_name = lines[1]
|
||||
self.qc.lock_db_for_reading()
|
||||
self.qc.load()
|
||||
self.qc.unlock_db()
|
||||
dispvm = self.qc.get_vm_by_name(dispvm_name)
|
||||
self.assertIsNone(dispvm, "DispVM {} still exists in qubes.xml".format(
|
||||
dispvm_name))
|
||||
|
||||
def test_003_cleanup_destroyed(self):
|
||||
"""
|
||||
Check if DispVM is properly removed even if it terminated itself (#1660)
|
||||
:return:
|
||||
"""
|
||||
self.qc.unlock_db()
|
||||
p = subprocess.Popen(['/usr/lib/qubes/qfile-daemon-dvm',
|
||||
'qubes.VMShell', 'dom0', 'DEFAULT'],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=open(os.devnull, 'w'))
|
||||
p.stdin.write("qubesdb-read /name\n")
|
||||
p.stdin.write("echo ERROR\n")
|
||||
p.stdin.write("sudo poweroff\n")
|
||||
# do not close p.stdin on purpose - wait to automatic disconnect when
|
||||
# domain is destroyed
|
||||
timeout = 30
|
||||
while timeout > 0:
|
||||
if p.poll():
|
||||
break
|
||||
time.sleep(1)
|
||||
timeout -= 1
|
||||
# includes check for None - timeout
|
||||
self.assertEquals(p.returncode, 0)
|
||||
lines = p.stdout.read().splitlines()
|
||||
dispvm_name = lines[0]
|
||||
self.assertNotEquals(dispvm_name, "ERROR")
|
||||
self.qc.lock_db_for_reading()
|
||||
self.qc.load()
|
||||
self.qc.unlock_db()
|
||||
dispvm = self.qc.get_vm_by_name(dispvm_name)
|
||||
self.assertIsNone(dispvm, "DispVM {} still exists in qubes.xml".format(
|
||||
dispvm_name))
|
||||
|
||||
|
||||
class TC_20_DispVMMixin(qubes.tests.SystemTestsMixin):
|
||||
def test_000_prepare_dvm(self):
|
||||
self.qc.unlock_db()
|
||||
retcode = subprocess.call(['/usr/bin/qvm-create-default-dvm',
|
||||
self.template],
|
||||
stderr=open(os.devnull, 'w'))
|
||||
self.assertEqual(retcode, 0)
|
||||
self.qc.lock_db_for_writing()
|
||||
self.qc.load()
|
||||
self.assertIsNotNone(self.qc.get_vm_by_name(
|
||||
self.template + "-dvm"))
|
||||
# TODO: check mtime of snapshot file
|
||||
|
||||
def test_010_simple_dvm_run(self):
|
||||
self.qc.unlock_db()
|
||||
p = subprocess.Popen(['/usr/lib/qubes/qfile-daemon-dvm',
|
||||
'qubes.VMShell', 'dom0', 'DEFAULT'],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=open(os.devnull, 'w'))
|
||||
(stdout, _) = p.communicate(input="echo test")
|
||||
self.assertEqual(stdout, "test\n")
|
||||
# TODO: check if DispVM is destroyed
|
||||
|
||||
@unittest.skipUnless(spawn.find_executable('xdotool'),
|
||||
"xdotool not installed")
|
||||
def test_020_gui_app(self):
|
||||
self.qc.unlock_db()
|
||||
p = subprocess.Popen(['/usr/lib/qubes/qfile-daemon-dvm',
|
||||
'qubes.VMShell', 'dom0', 'DEFAULT'],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=open(os.devnull, 'w'))
|
||||
|
||||
# wait for DispVM startup:
|
||||
p.stdin.write("echo test\n")
|
||||
p.stdin.flush()
|
||||
l = p.stdout.readline()
|
||||
self.assertEqual(l, "test\n")
|
||||
|
||||
# potential race condition, but our tests are supposed to be
|
||||
# running on dedicated machine, so should not be a problem
|
||||
self.qc.lock_db_for_reading()
|
||||
self.qc.load()
|
||||
self.qc.unlock_db()
|
||||
|
||||
max_qid = 0
|
||||
for vm in self.qc.values():
|
||||
if not vm.is_disposablevm():
|
||||
continue
|
||||
if vm.qid > max_qid:
|
||||
max_qid = vm.qid
|
||||
dispvm = self.qc[max_qid]
|
||||
self.assertNotEqual(dispvm.qid, 0, "DispVM not found in qubes.xml")
|
||||
self.assertTrue(dispvm.is_running())
|
||||
try:
|
||||
window_title = 'user@%s' % (dispvm.template.name + "-dvm")
|
||||
p.stdin.write("xterm -e "
|
||||
"\"sh -c 'echo \\\"\033]0;{}\007\\\";read x;'\"\n".
|
||||
format(window_title))
|
||||
self.wait_for_window(window_title)
|
||||
|
||||
time.sleep(0.5)
|
||||
self.enter_keys_in_window(window_title, ['Return'])
|
||||
# Wait for window to close
|
||||
self.wait_for_window(window_title, show=False)
|
||||
finally:
|
||||
p.stdin.close()
|
||||
|
||||
wait_count = 0
|
||||
while dispvm.is_running():
|
||||
wait_count += 1
|
||||
if wait_count > 100:
|
||||
self.fail("Timeout while waiting for DispVM destruction")
|
||||
time.sleep(0.1)
|
||||
wait_count = 0
|
||||
while p.poll() is None:
|
||||
wait_count += 1
|
||||
if wait_count > 100:
|
||||
self.fail("Timeout while waiting for qfile-daemon-dvm "
|
||||
"termination")
|
||||
time.sleep(0.1)
|
||||
self.assertEqual(p.returncode, 0)
|
||||
|
||||
self.qc.lock_db_for_reading()
|
||||
self.qc.load()
|
||||
self.qc.unlock_db()
|
||||
self.assertIsNone(self.qc.get_vm_by_name(dispvm.name),
|
||||
"DispVM not removed from qubes.xml")
|
||||
|
||||
def _handle_editor(self, winid):
|
||||
(window_title, _) = subprocess.Popen(
|
||||
['xdotool', 'getwindowname', winid], stdout=subprocess.PIPE).\
|
||||
communicate()
|
||||
window_title = window_title.strip().\
|
||||
replace('(', '\(').replace(')', '\)')
|
||||
time.sleep(1)
|
||||
if "gedit" in window_title:
|
||||
subprocess.check_call(['xdotool', 'windowactivate', '--sync', winid,
|
||||
'type', 'Test test 2'])
|
||||
subprocess.check_call(['xdotool', 'key', '--window', winid,
|
||||
'key', 'Return'])
|
||||
time.sleep(0.5)
|
||||
subprocess.check_call(['xdotool',
|
||||
'key', 'ctrl+s', 'ctrl+q'])
|
||||
elif "LibreOffice" in window_title:
|
||||
# wait for actual editor (we've got splash screen)
|
||||
search = subprocess.Popen(['xdotool', 'search', '--sync',
|
||||
'--onlyvisible', '--all', '--name', '--class', 'disp*|Writer'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=open(os.path.devnull, 'w'))
|
||||
retcode = search.wait()
|
||||
if retcode == 0:
|
||||
winid = search.stdout.read().strip()
|
||||
time.sleep(0.5)
|
||||
subprocess.check_call(['xdotool', 'windowactivate', '--sync', winid,
|
||||
'type', 'Test test 2'])
|
||||
subprocess.check_call(['xdotool', 'key', '--window', winid,
|
||||
'key', 'Return'])
|
||||
time.sleep(0.5)
|
||||
subprocess.check_call(['xdotool',
|
||||
'key', '--delay', '100', 'ctrl+s',
|
||||
'Return', 'ctrl+q'])
|
||||
elif "emacs" in window_title:
|
||||
subprocess.check_call(['xdotool', 'windowactivate', '--sync', winid,
|
||||
'type', 'Test test 2'])
|
||||
subprocess.check_call(['xdotool', 'key', '--window', winid,
|
||||
'key', 'Return'])
|
||||
time.sleep(0.5)
|
||||
subprocess.check_call(['xdotool',
|
||||
'key', 'ctrl+x', 'ctrl+s'])
|
||||
subprocess.check_call(['xdotool',
|
||||
'key', 'ctrl+x', 'ctrl+c'])
|
||||
elif "vim" in window_title or "user@" in window_title:
|
||||
subprocess.check_call(['xdotool', 'windowactivate', '--sync', winid,
|
||||
'key', 'i', 'type', 'Test test 2'])
|
||||
subprocess.check_call(['xdotool', 'key', '--window', winid,
|
||||
'key', 'Return'])
|
||||
subprocess.check_call(
|
||||
['xdotool',
|
||||
'key', 'Escape', 'colon', 'w', 'q', 'Return'])
|
||||
else:
|
||||
self.fail("Unknown editor window: {}".format(window_title))
|
||||
|
||||
@unittest.skipUnless(spawn.find_executable('xdotool'),
|
||||
"xdotool not installed")
|
||||
def test_030_edit_file(self):
|
||||
testvm1 = self.qc.add_new_vm("QubesAppVm",
|
||||
name=self.make_vm_name('vm1'),
|
||||
template=self.qc.get_vm_by_name(
|
||||
self.template))
|
||||
testvm1.create_on_disk(verbose=False)
|
||||
self.qc.save()
|
||||
|
||||
testvm1.start()
|
||||
testvm1.run("echo test1 > /home/user/test.txt", wait=True)
|
||||
|
||||
self.qc.unlock_db()
|
||||
p = testvm1.run("qvm-open-in-dvm /home/user/test.txt",
|
||||
passio_popen=True)
|
||||
|
||||
wait_count = 0
|
||||
winid = None
|
||||
while True:
|
||||
search = subprocess.Popen(['xdotool', 'search',
|
||||
'--onlyvisible', '--class', 'disp*'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=open(os.path.devnull, 'w'))
|
||||
retcode = search.wait()
|
||||
if retcode == 0:
|
||||
winid = search.stdout.read().strip()
|
||||
# get window title
|
||||
(window_title, _) = subprocess.Popen(
|
||||
['xdotool', 'getwindowname', winid], stdout=subprocess.PIPE). \
|
||||
communicate()
|
||||
window_title = window_title.strip()
|
||||
# ignore LibreOffice splash screen and window with no title
|
||||
# set yet
|
||||
if window_title and not window_title.startswith("LibreOffice")\
|
||||
and not window_title == 'VMapp command':
|
||||
break
|
||||
wait_count += 1
|
||||
if wait_count > 100:
|
||||
self.fail("Timeout while waiting for editor window")
|
||||
time.sleep(0.3)
|
||||
|
||||
time.sleep(0.5)
|
||||
self._handle_editor(winid)
|
||||
p.wait()
|
||||
p = testvm1.run("cat /home/user/test.txt",
|
||||
passio_popen=True)
|
||||
(test_txt_content, _) = p.communicate()
|
||||
# Drop BOM if added by editor
|
||||
if test_txt_content.startswith('\xef\xbb\xbf'):
|
||||
test_txt_content = test_txt_content[3:]
|
||||
self.assertEqual(test_txt_content, "Test test 2\ntest1\n")
|
||||
|
||||
def load_tests(loader, tests, pattern):
|
||||
try:
|
||||
qc = qubes.qubes.QubesVmCollection()
|
||||
qc.lock_db_for_reading()
|
||||
qc.load()
|
||||
qc.unlock_db()
|
||||
templates = [vm.name for vm in qc.values() if
|
||||
isinstance(vm, qubes.qubes.QubesTemplateVm)]
|
||||
except OSError:
|
||||
templates = []
|
||||
for template in templates:
|
||||
tests.addTests(loader.loadTestsFromTestCase(
|
||||
type(
|
||||
'TC_20_DispVM_' + template,
|
||||
(TC_20_DispVMMixin, qubes.tests.QubesTestCase),
|
||||
{'template': template})))
|
||||
|
||||
return tests
|
||||
@ -39,8 +39,8 @@ class TC_00_Dom0UpgradeMixin(qubes.tests.SystemTestsMixin):
|
||||
Tests for downloading dom0 updates using VMs based on different templates
|
||||
"""
|
||||
pkg_name = 'qubes-test-pkg'
|
||||
dom0_update_common_opts = ['--disablerepo=*', '--enablerepo=test',
|
||||
'--setopt=test.copy_local=1']
|
||||
dom0_update_common_opts = ['--disablerepo=*', '--enablerepo=test']
|
||||
update_flag_path = '/var/lib/qubes/updates/dom0-updates-available'
|
||||
|
||||
@classmethod
|
||||
def generate_key(cls, keydir):
|
||||
@ -84,9 +84,9 @@ Expire-Date: 0
|
||||
p.stdin.write('''
|
||||
[test]
|
||||
name = Test
|
||||
baseurl = file:///tmp/repo
|
||||
baseurl = http://localhost:8080/
|
||||
enabled = 1
|
||||
''')
|
||||
''')
|
||||
p.stdin.close()
|
||||
p.wait()
|
||||
|
||||
@ -100,6 +100,10 @@ enabled = 1
|
||||
|
||||
def setUp(self):
|
||||
super(TC_00_Dom0UpgradeMixin, self).setUp()
|
||||
if self.template.startswith('whonix-'):
|
||||
# Whonix redirect all the traffic through tor, so repository
|
||||
# on http://localhost:8080/ is unavailable
|
||||
self.skipTest("Test not supported for this template")
|
||||
self.updatevm = self.qc.add_new_vm(
|
||||
"QubesProxyVm",
|
||||
name=self.make_vm_name("updatevm"),
|
||||
@ -114,6 +118,7 @@ enabled = 1
|
||||
subprocess.check_call(['sudo', 'rpm', '--import',
|
||||
os.path.join(self.tmpdir, 'pubkey.asc')])
|
||||
self.updatevm.start()
|
||||
self.repo_running = False
|
||||
|
||||
def tearDown(self):
|
||||
self.qc.lock_db_for_writing()
|
||||
@ -185,12 +190,27 @@ Test package
|
||||
elif retcode != 0:
|
||||
self.skipTest("createrepo failed with code {}, cannot perform the "
|
||||
"test".format(retcode))
|
||||
self.start_repo()
|
||||
|
||||
def start_repo(self):
|
||||
if not self.repo_running:
|
||||
self.updatevm.run("cd /tmp/repo &&"
|
||||
"python -m SimpleHTTPServer 8080")
|
||||
self.repo_running = True
|
||||
|
||||
def test_000_update(self):
|
||||
"""Dom0 update tests
|
||||
|
||||
Check if package update is:
|
||||
- detected
|
||||
- installed
|
||||
- "updates pending" flag is cleared
|
||||
"""
|
||||
filename = self.create_pkg(self.tmpdir, self.pkg_name, '1.0')
|
||||
subprocess.check_call(['sudo', 'rpm', '-i', filename])
|
||||
filename = self.create_pkg(self.tmpdir, self.pkg_name, '2.0')
|
||||
self.send_pkg(filename)
|
||||
open(self.update_flag_path, 'a').close()
|
||||
|
||||
logpath = os.path.join(self.tmpdir, 'dom0-update-output.txt')
|
||||
try:
|
||||
@ -210,6 +230,68 @@ Test package
|
||||
self.pkg_name)], stdout=open(os.devnull, 'w'))
|
||||
self.assertEqual(retcode, 0, 'Package {}-2.0 not installed after '
|
||||
'update'.format(self.pkg_name))
|
||||
self.assertFalse(os.path.exists(self.update_flag_path),
|
||||
"'updates pending' flag not cleared")
|
||||
|
||||
def test_005_update_flag_clear(self):
|
||||
"""Check if 'updates pending' flag is creared"""
|
||||
|
||||
# create any pkg (but not install it) to initialize repo in the VM
|
||||
filename = self.create_pkg(self.tmpdir, self.pkg_name, '1.0')
|
||||
self.send_pkg(filename)
|
||||
open(self.update_flag_path, 'a').close()
|
||||
|
||||
logpath = os.path.join(self.tmpdir, 'dom0-update-output.txt')
|
||||
try:
|
||||
subprocess.check_call(['sudo', 'qubes-dom0-update', '-y'] +
|
||||
self.dom0_update_common_opts,
|
||||
stdout=open(logpath, 'w'),
|
||||
stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError:
|
||||
self.fail("qubes-dom0-update failed: " + open(
|
||||
logpath).read())
|
||||
|
||||
with open(logpath) as f:
|
||||
dom0_update_output = f.read()
|
||||
self.assertFalse('Errno' in dom0_update_output or
|
||||
'Couldn\'t' in dom0_update_output,
|
||||
"qubes-dom0-update reported an error: {}".
|
||||
format(dom0_update_output))
|
||||
|
||||
self.assertFalse(os.path.exists(self.update_flag_path),
|
||||
"'updates pending' flag not cleared")
|
||||
|
||||
def test_006_update_flag_clear(self):
|
||||
"""Check if 'updates pending' flag is creared, using --clean"""
|
||||
|
||||
# create any pkg (but not install it) to initialize repo in the VM
|
||||
filename = self.create_pkg(self.tmpdir, self.pkg_name, '1.0')
|
||||
self.send_pkg(filename)
|
||||
open(self.update_flag_path, 'a').close()
|
||||
|
||||
# remove also repodata to test #1685
|
||||
if os.path.exists('/var/lib/qubes/updates/repodata'):
|
||||
shutil.rmtree('/var/lib/qubes/updates/repodata')
|
||||
logpath = os.path.join(self.tmpdir, 'dom0-update-output.txt')
|
||||
try:
|
||||
subprocess.check_call(['sudo', 'qubes-dom0-update', '-y',
|
||||
'--clean'] +
|
||||
self.dom0_update_common_opts,
|
||||
stdout=open(logpath, 'w'),
|
||||
stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError:
|
||||
self.fail("qubes-dom0-update failed: " + open(
|
||||
logpath).read())
|
||||
|
||||
with open(logpath) as f:
|
||||
dom0_update_output = f.read()
|
||||
self.assertFalse('Errno' in dom0_update_output or
|
||||
'Couldn\'t' in dom0_update_output,
|
||||
"qubes-dom0-update reported an error: {}".
|
||||
format(dom0_update_output))
|
||||
|
||||
self.assertFalse(os.path.exists(self.update_flag_path),
|
||||
"'updates pending' flag not cleared")
|
||||
|
||||
def test_010_instal(self):
|
||||
filename = self.create_pkg(self.tmpdir, self.pkg_name, '1.0')
|
||||
|
||||
107
tests/extra.py
Normal file
107
tests/extra.py
Normal file
@ -0,0 +1,107 @@
|
||||
#!/usr/bin/python2 -O
|
||||
# vim: fileencoding=utf-8
|
||||
|
||||
#
|
||||
# The Qubes OS Project, https://www.qubes-os.org/
|
||||
#
|
||||
# Copyright (C) 2016
|
||||
# Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
|
||||
#
|
||||
# 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 pkg_resources
|
||||
import qubes.tests
|
||||
import qubes.qubes
|
||||
|
||||
|
||||
class ExtraTestCase(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
|
||||
template = None
|
||||
|
||||
def setUp(self):
|
||||
super(ExtraTestCase, self).setUp()
|
||||
self.qc.unlock_db()
|
||||
self.default_netvm = None
|
||||
|
||||
def create_vms(self, names):
|
||||
"""
|
||||
Create AppVMs for the duration of the test. Will be automatically
|
||||
removed after completing the test.
|
||||
:param names: list of VM names to create (each of them will be
|
||||
prefixed with some test specific string)
|
||||
:return: list of created VM objects
|
||||
"""
|
||||
self.qc.lock_db_for_writing()
|
||||
self.qc.load()
|
||||
if self.template:
|
||||
template = self.qc.get_vm_by_name(self.template)
|
||||
else:
|
||||
template = self.qc.get_default_template()
|
||||
for vmname in names:
|
||||
vm = self.qc.add_new_vm("QubesAppVm",
|
||||
name=self.make_vm_name(vmname),
|
||||
template=template,
|
||||
uses_default_netvm=False,
|
||||
netvm=self.default_netvm)
|
||||
vm.create_on_disk(verbose=False)
|
||||
self.save_and_reload_db()
|
||||
self.qc.unlock_db()
|
||||
|
||||
# get objects after reload
|
||||
vms = []
|
||||
for vmname in names:
|
||||
vms.append(self.qc.get_vm_by_name(self.make_vm_name(vmname)))
|
||||
return vms
|
||||
|
||||
def enable_network(self):
|
||||
"""
|
||||
Enable access to the network. Must be called before creating VMs.
|
||||
"""
|
||||
self.default_netvm = self.qc.get_default_netvm()
|
||||
if self.template.startswith('whonix-ws'):
|
||||
whonix_netvm = self.qc.get_vm_by_name('sys-whonix')
|
||||
if whonix_netvm:
|
||||
self.default_netvm = whonix_netvm
|
||||
|
||||
def load_tests(loader, tests, pattern):
|
||||
for entry in pkg_resources.iter_entry_points('qubes.tests.extra'):
|
||||
for test_case in entry.load()():
|
||||
tests.addTests(loader.loadTestsFromTestCase(test_case))
|
||||
|
||||
try:
|
||||
qc = qubes.qubes.QubesVmCollection()
|
||||
qc.lock_db_for_reading()
|
||||
qc.load()
|
||||
qc.unlock_db()
|
||||
templates = [vm.name for vm in qc.values() if
|
||||
isinstance(vm, qubes.qubes.QubesTemplateVm)]
|
||||
except OSError:
|
||||
templates = []
|
||||
|
||||
for entry in pkg_resources.iter_entry_points(
|
||||
'qubes.tests.extra.for_template'):
|
||||
for test_case in entry.load()():
|
||||
for template in templates:
|
||||
tests.addTests(loader.loadTestsFromTestCase(
|
||||
type(
|
||||
'{}_{}_{}'.format(
|
||||
entry.name, test_case.__name__, template),
|
||||
(test_case,),
|
||||
{'template': template}
|
||||
)
|
||||
))
|
||||
|
||||
return tests
|
||||
75
tests/hardware.py
Normal file
75
tests/hardware.py
Normal file
@ -0,0 +1,75 @@
|
||||
#!/usr/bin/python2
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2016 Marek Marczykowski-Górecki
|
||||
# <marmarek@invisiblethingslab.com>
|
||||
#
|
||||
# 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 qubes.tests
|
||||
import time
|
||||
import subprocess
|
||||
from unittest import expectedFailure
|
||||
|
||||
|
||||
class TC_00_HVM(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
def setUp(self):
|
||||
super(TC_00_HVM, self).setUp()
|
||||
self.vm = self.qc.add_new_vm("QubesHVm",
|
||||
name=self.make_vm_name('vm1'))
|
||||
self.vm.create_on_disk(verbose=False)
|
||||
|
||||
@expectedFailure
|
||||
def test_000_pci_passthrough_presence(self):
|
||||
pcidev = os.environ.get('QUBES_TEST_PCIDEV', None)
|
||||
if pcidev is None:
|
||||
self.skipTest('Specify PCI device with QUBES_TEST_PCIDEV '
|
||||
'environment variable')
|
||||
self.vm.pcidevs = [pcidev]
|
||||
self.vm.pci_strictreset = False
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
|
||||
init_script = (
|
||||
"#!/bin/sh\n"
|
||||
"set -e\n"
|
||||
"lspci -n > /dev/xvdb\n"
|
||||
"poweroff\n"
|
||||
)
|
||||
|
||||
self.prepare_hvm_system_linux(self.vm, init_script,
|
||||
['/usr/sbin/lspci'])
|
||||
self.vm.start()
|
||||
timeout = 60
|
||||
while timeout > 0:
|
||||
if not self.vm.is_running():
|
||||
break
|
||||
time.sleep(1)
|
||||
timeout -= 1
|
||||
if self.vm.is_running():
|
||||
self.fail("Timeout while waiting for VM shutdown")
|
||||
|
||||
with open(self.vm.storage.private_img, 'r') as f:
|
||||
lspci_vm = f.read(512).strip('\0')
|
||||
p = subprocess.Popen(['lspci', '-ns', pcidev], stdout=subprocess.PIPE)
|
||||
(lspci_host, _) = p.communicate()
|
||||
# strip BDF, as it is different in VM
|
||||
pcidev_desc = ' '.join(lspci_host.strip().split(' ')[1:])
|
||||
self.assertIn(pcidev_desc, lspci_vm)
|
||||
125
tests/hvm.py
Normal file
125
tests/hvm.py
Normal file
@ -0,0 +1,125 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2016 Marek Marczykowski-Górecki
|
||||
# <marmarek@invisiblethingslab.com>
|
||||
#
|
||||
# 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 qubes.tests
|
||||
from qubes.qubes import QubesException
|
||||
|
||||
class TC_10_HVM(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
# TODO: test with some OS inside
|
||||
# TODO: windows tools tests
|
||||
|
||||
def test_000_create_start(self):
|
||||
testvm1 = self.qc.add_new_vm("QubesHVm",
|
||||
name=self.make_vm_name('vm1'))
|
||||
testvm1.create_on_disk(verbose=False)
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
testvm1.start()
|
||||
self.assertEquals(testvm1.get_power_state(), "Running")
|
||||
|
||||
def test_010_create_start_template(self):
|
||||
templatevm = self.qc.add_new_vm("QubesTemplateHVm",
|
||||
name=self.make_vm_name('template'))
|
||||
templatevm.create_on_disk(verbose=False)
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
|
||||
templatevm.start()
|
||||
self.assertEquals(templatevm.get_power_state(), "Running")
|
||||
|
||||
def test_020_create_start_template_vm(self):
|
||||
templatevm = self.qc.add_new_vm("QubesTemplateHVm",
|
||||
name=self.make_vm_name('template'))
|
||||
templatevm.create_on_disk(verbose=False)
|
||||
testvm2 = self.qc.add_new_vm("QubesHVm",
|
||||
name=self.make_vm_name('vm2'),
|
||||
template=templatevm)
|
||||
testvm2.create_on_disk(verbose=False)
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
|
||||
testvm2.start()
|
||||
self.assertEquals(testvm2.get_power_state(), "Running")
|
||||
|
||||
def test_030_prevent_simultaneus_start(self):
|
||||
templatevm = self.qc.add_new_vm("QubesTemplateHVm",
|
||||
name=self.make_vm_name('template'))
|
||||
templatevm.create_on_disk(verbose=False)
|
||||
testvm2 = self.qc.add_new_vm("QubesHVm",
|
||||
name=self.make_vm_name('vm2'),
|
||||
template=templatevm)
|
||||
testvm2.create_on_disk(verbose=False)
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
|
||||
templatevm.start()
|
||||
self.assertEquals(templatevm.get_power_state(), "Running")
|
||||
self.assertRaises(QubesException, testvm2.start)
|
||||
templatevm.force_shutdown()
|
||||
testvm2.start()
|
||||
self.assertEquals(testvm2.get_power_state(), "Running")
|
||||
self.assertRaises(QubesException, templatevm.start)
|
||||
|
||||
def test_100_resize_root_img(self):
|
||||
testvm1 = self.qc.add_new_vm("QubesHVm",
|
||||
name=self.make_vm_name('vm1'))
|
||||
testvm1.create_on_disk(verbose=False)
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
testvm1.resize_root_img(30*1024**3)
|
||||
self.assertEquals(testvm1.get_root_img_sz(), 30*1024**3)
|
||||
testvm1.start()
|
||||
self.assertEquals(testvm1.get_power_state(), "Running")
|
||||
# TODO: launch some OS there and check the size
|
||||
|
||||
def test_200_start_invalid_drive(self):
|
||||
"""Regression test for #1619"""
|
||||
testvm1 = self.qc.add_new_vm("QubesHVm",
|
||||
name=self.make_vm_name('vm1'))
|
||||
testvm1.create_on_disk(verbose=False)
|
||||
testvm1.drive = 'hd:dom0:/invalid'
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
try:
|
||||
testvm1.start()
|
||||
except Exception as e:
|
||||
self.assertIsInstance(e, QubesException)
|
||||
else:
|
||||
self.fail('No exception raised')
|
||||
|
||||
def test_201_start_invalid_drive_cdrom(self):
|
||||
"""Regression test for #1619"""
|
||||
testvm1 = self.qc.add_new_vm("QubesHVm",
|
||||
name=self.make_vm_name('vm1'))
|
||||
testvm1.create_on_disk(verbose=False)
|
||||
testvm1.drive = 'cdrom:dom0:/invalid'
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
try:
|
||||
testvm1.start()
|
||||
except Exception as e:
|
||||
self.assertIsInstance(e, QubesException)
|
||||
else:
|
||||
self.fail('No exception raised')
|
||||
|
||||
354
tests/mime.py
Normal file
354
tests/mime.py
Normal file
@ -0,0 +1,354 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2016 Marek Marczykowski-Górecki
|
||||
# <marmarek@invisiblethingslab.com>
|
||||
#
|
||||
# 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 distutils import spawn
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
import unittest
|
||||
|
||||
import qubes.tests
|
||||
import qubes.qubes
|
||||
from qubes.qubes import QubesVmCollection
|
||||
|
||||
@unittest.skipUnless(
|
||||
spawn.find_executable('xprop') and
|
||||
spawn.find_executable('xdotool') and
|
||||
spawn.find_executable('wmctrl'),
|
||||
"xprop or xdotool or wmctrl not installed")
|
||||
class TC_50_MimeHandlers(qubes.tests.SystemTestsMixin):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if cls.template == 'whonix-gw' or 'minimal' in cls.template:
|
||||
raise unittest.SkipTest(
|
||||
'Template {} not supported by this test'.format(cls.template))
|
||||
|
||||
if cls.template == 'whonix-ws':
|
||||
# TODO remove when Whonix-based DispVMs will work (Whonix 13?)
|
||||
raise unittest.SkipTest(
|
||||
'Template {} not supported by this test'.format(cls.template))
|
||||
|
||||
qc = QubesVmCollection()
|
||||
|
||||
cls._kill_test_vms(qc, prefix=qubes.tests.CLSVMPREFIX)
|
||||
|
||||
qc.lock_db_for_writing()
|
||||
qc.load()
|
||||
|
||||
cls._remove_test_vms(qc, qubes.qubes.vmm.libvirt_conn,
|
||||
prefix=qubes.tests.CLSVMPREFIX)
|
||||
|
||||
cls.source_vmname = cls.make_vm_name('source', True)
|
||||
source_vm = qc.add_new_vm("QubesAppVm",
|
||||
template=qc.get_vm_by_name(cls.template),
|
||||
name=cls.source_vmname)
|
||||
source_vm.create_on_disk(verbose=False)
|
||||
|
||||
cls.target_vmname = cls.make_vm_name('target', True)
|
||||
target_vm = qc.add_new_vm("QubesAppVm",
|
||||
template=qc.get_vm_by_name(cls.template),
|
||||
name=cls.target_vmname)
|
||||
target_vm.create_on_disk(verbose=False)
|
||||
|
||||
qc.save()
|
||||
qc.unlock_db()
|
||||
source_vm.start()
|
||||
target_vm.start()
|
||||
|
||||
# make sure that DispVMs will be started of the same template
|
||||
retcode = subprocess.call(['/usr/bin/qvm-create-default-dvm',
|
||||
cls.template],
|
||||
stderr=open(os.devnull, 'w'))
|
||||
assert retcode == 0, "Error preparing DispVM"
|
||||
|
||||
def setUp(self):
|
||||
super(TC_50_MimeHandlers, self).setUp()
|
||||
self.source_vm = self.qc.get_vm_by_name(self.source_vmname)
|
||||
self.target_vm = self.qc.get_vm_by_name(self.target_vmname)
|
||||
|
||||
def get_window_class(self, winid, dispvm=False):
|
||||
(vm_winid, _) = subprocess.Popen(
|
||||
['xprop', '-id', winid, '_QUBES_VMWINDOWID'],
|
||||
stdout=subprocess.PIPE
|
||||
).communicate()
|
||||
vm_winid = vm_winid.split("#")[1].strip('\n" ')
|
||||
if dispvm:
|
||||
(vmname, _) = subprocess.Popen(
|
||||
['xprop', '-id', winid, '_QUBES_VMNAME'],
|
||||
stdout=subprocess.PIPE
|
||||
).communicate()
|
||||
vmname = vmname.split("=")[1].strip('\n" ')
|
||||
window_class = None
|
||||
while window_class is None:
|
||||
# XXX to use self.qc.get_vm_by_name would require reloading
|
||||
# qubes.xml, so use qvm-run instead
|
||||
xprop = subprocess.Popen(
|
||||
['qvm-run', '-p', vmname, 'xprop -id {} WM_CLASS'.format(
|
||||
vm_winid)], stdout=subprocess.PIPE)
|
||||
(window_class, _) = xprop.communicate()
|
||||
if xprop.returncode != 0:
|
||||
self.skipTest("xprop failed, not installed?")
|
||||
if 'not found' in window_class:
|
||||
# WM_CLASS not set yet, wait a little
|
||||
time.sleep(0.1)
|
||||
window_class = None
|
||||
else:
|
||||
window_class = None
|
||||
while window_class is None:
|
||||
xprop = self.target_vm.run(
|
||||
'xprop -id {} WM_CLASS'.format(vm_winid),
|
||||
passio_popen=True)
|
||||
(window_class, _) = xprop.communicate()
|
||||
if xprop.returncode != 0:
|
||||
self.skipTest("xprop failed, not installed?")
|
||||
if 'not found' in window_class:
|
||||
# WM_CLASS not set yet, wait a little
|
||||
time.sleep(0.1)
|
||||
window_class = None
|
||||
# output: WM_CLASS(STRING) = "gnome-terminal-server", "Gnome-terminal"
|
||||
try:
|
||||
window_class = window_class.split("=")[1].split(",")[0].strip('\n" ')
|
||||
except IndexError:
|
||||
raise Exception(
|
||||
"Unexpected output from xprop: '{}'".format(window_class))
|
||||
|
||||
return window_class
|
||||
|
||||
def open_file_and_check_viewer(self, filename, expected_app_titles,
|
||||
expected_app_classes, dispvm=False):
|
||||
self.qc.unlock_db()
|
||||
if dispvm:
|
||||
p = self.source_vm.run("qvm-open-in-dvm {}".format(filename),
|
||||
passio_popen=True)
|
||||
vmpattern = "disp*"
|
||||
else:
|
||||
self.qrexec_policy('qubes.OpenInVM', self.source_vm.name,
|
||||
self.target_vmname)
|
||||
self.qrexec_policy('qubes.OpenURL', self.source_vm.name,
|
||||
self.target_vmname)
|
||||
p = self.source_vm.run("qvm-open-in-vm {} {}".format(
|
||||
self.target_vmname, filename), passio_popen=True)
|
||||
vmpattern = self.target_vmname
|
||||
wait_count = 0
|
||||
winid = None
|
||||
window_title = None
|
||||
while True:
|
||||
search = subprocess.Popen(['xdotool', 'search',
|
||||
'--onlyvisible', '--class', vmpattern],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=open(os.path.devnull, 'w'))
|
||||
retcode = search.wait()
|
||||
if retcode == 0:
|
||||
winid = search.stdout.read().strip()
|
||||
# get window title
|
||||
(window_title, _) = subprocess.Popen(
|
||||
['xdotool', 'getwindowname', winid], stdout=subprocess.PIPE). \
|
||||
communicate()
|
||||
window_title = window_title.strip()
|
||||
# ignore LibreOffice splash screen and window with no title
|
||||
# set yet
|
||||
if window_title and not window_title.startswith("LibreOffice")\
|
||||
and not window_title == 'VMapp command':
|
||||
break
|
||||
wait_count += 1
|
||||
if wait_count > 100:
|
||||
self.fail("Timeout while waiting for editor window")
|
||||
time.sleep(0.3)
|
||||
|
||||
# get window class
|
||||
window_class = self.get_window_class(winid, dispvm)
|
||||
# close the window - we've got the window class, it is no longer needed
|
||||
subprocess.check_call(['wmctrl', '-i', '-c', winid])
|
||||
p.wait()
|
||||
self.wait_for_window(window_title, show=False)
|
||||
|
||||
def check_matches(obj, patterns):
|
||||
return any((pat.search(obj) if isinstance(pat, type(re.compile('')))
|
||||
else pat in obj) for pat in patterns)
|
||||
|
||||
if not check_matches(window_title, expected_app_titles) and \
|
||||
not check_matches(window_class, expected_app_classes):
|
||||
self.fail("Opening file {} resulted in window '{} ({})', which is "
|
||||
"none of {!r} ({!r})".format(
|
||||
filename, window_title, window_class,
|
||||
expected_app_titles, expected_app_classes))
|
||||
|
||||
def prepare_txt(self, filename):
|
||||
p = self.source_vm.run("cat > {}".format(filename), passio_popen=True)
|
||||
p.stdin.write("This is test\n")
|
||||
p.stdin.close()
|
||||
retcode = p.wait()
|
||||
assert retcode == 0, "Failed to write {} file".format(filename)
|
||||
|
||||
def prepare_pdf(self, filename):
|
||||
self.prepare_txt("/tmp/source.txt")
|
||||
cmd = "convert /tmp/source.txt {}".format(filename)
|
||||
retcode = self.source_vm.run(cmd, wait=True)
|
||||
assert retcode == 0, "Failed to run '{}'".format(cmd)
|
||||
|
||||
def prepare_doc(self, filename):
|
||||
self.prepare_txt("/tmp/source.txt")
|
||||
cmd = "unoconv -f doc -o {} /tmp/source.txt".format(filename)
|
||||
retcode = self.source_vm.run(cmd, wait=True)
|
||||
if retcode != 0:
|
||||
self.skipTest("Failed to run '{}', not installed?".format(cmd))
|
||||
|
||||
def prepare_pptx(self, filename):
|
||||
self.prepare_txt("/tmp/source.txt")
|
||||
cmd = "unoconv -f pptx -o {} /tmp/source.txt".format(filename)
|
||||
retcode = self.source_vm.run(cmd, wait=True)
|
||||
if retcode != 0:
|
||||
self.skipTest("Failed to run '{}', not installed?".format(cmd))
|
||||
|
||||
def prepare_png(self, filename):
|
||||
self.prepare_txt("/tmp/source.txt")
|
||||
cmd = "convert /tmp/source.txt {}".format(filename)
|
||||
retcode = self.source_vm.run(cmd, wait=True)
|
||||
if retcode != 0:
|
||||
self.skipTest("Failed to run '{}', not installed?".format(cmd))
|
||||
|
||||
def prepare_jpg(self, filename):
|
||||
self.prepare_txt("/tmp/source.txt")
|
||||
cmd = "convert /tmp/source.txt {}".format(filename)
|
||||
retcode = self.source_vm.run(cmd, wait=True)
|
||||
if retcode != 0:
|
||||
self.skipTest("Failed to run '{}', not installed?".format(cmd))
|
||||
|
||||
def test_000_txt(self):
|
||||
filename = "/home/user/test_file.txt"
|
||||
self.prepare_txt(filename)
|
||||
self.open_file_and_check_viewer(filename, ["vim", "user@"],
|
||||
["gedit", "emacs", "libreoffice"])
|
||||
|
||||
def test_001_pdf(self):
|
||||
filename = "/home/user/test_file.pdf"
|
||||
self.prepare_pdf(filename)
|
||||
self.open_file_and_check_viewer(filename, [],
|
||||
["evince"])
|
||||
|
||||
def test_002_doc(self):
|
||||
filename = "/home/user/test_file.doc"
|
||||
self.prepare_doc(filename)
|
||||
self.open_file_and_check_viewer(filename, [],
|
||||
["libreoffice", "abiword"])
|
||||
|
||||
def test_003_pptx(self):
|
||||
filename = "/home/user/test_file.pptx"
|
||||
self.prepare_pptx(filename)
|
||||
self.open_file_and_check_viewer(filename, [],
|
||||
["libreoffice"])
|
||||
|
||||
def test_004_png(self):
|
||||
filename = "/home/user/test_file.png"
|
||||
self.prepare_png(filename)
|
||||
self.open_file_and_check_viewer(filename, [],
|
||||
["shotwell", "eog", "display"])
|
||||
|
||||
def test_005_jpg(self):
|
||||
filename = "/home/user/test_file.jpg"
|
||||
self.prepare_jpg(filename)
|
||||
self.open_file_and_check_viewer(filename, [],
|
||||
["shotwell", "eog", "display"])
|
||||
|
||||
def test_006_jpeg(self):
|
||||
filename = "/home/user/test_file.jpeg"
|
||||
self.prepare_jpg(filename)
|
||||
self.open_file_and_check_viewer(filename, [],
|
||||
["shotwell", "eog", "display"])
|
||||
|
||||
def test_010_url(self):
|
||||
self.open_file_and_check_viewer("https://www.qubes-os.org/", [],
|
||||
["Firefox", "Iceweasel", "Navigator"])
|
||||
|
||||
def test_100_txt_dispvm(self):
|
||||
filename = "/home/user/test_file.txt"
|
||||
self.prepare_txt(filename)
|
||||
self.open_file_and_check_viewer(filename, ["vim", "user@"],
|
||||
["gedit", "emacs", "libreoffice"],
|
||||
dispvm=True)
|
||||
|
||||
def test_101_pdf_dispvm(self):
|
||||
filename = "/home/user/test_file.pdf"
|
||||
self.prepare_pdf(filename)
|
||||
self.open_file_and_check_viewer(filename, [],
|
||||
["evince"],
|
||||
dispvm=True)
|
||||
|
||||
def test_102_doc_dispvm(self):
|
||||
filename = "/home/user/test_file.doc"
|
||||
self.prepare_doc(filename)
|
||||
self.open_file_and_check_viewer(filename, [],
|
||||
["libreoffice", "abiword"],
|
||||
dispvm=True)
|
||||
|
||||
def test_103_pptx_dispvm(self):
|
||||
filename = "/home/user/test_file.pptx"
|
||||
self.prepare_pptx(filename)
|
||||
self.open_file_and_check_viewer(filename, [],
|
||||
["libreoffice"],
|
||||
dispvm=True)
|
||||
|
||||
def test_104_png_dispvm(self):
|
||||
filename = "/home/user/test_file.png"
|
||||
self.prepare_png(filename)
|
||||
self.open_file_and_check_viewer(filename, [],
|
||||
["shotwell", "eog", "display"],
|
||||
dispvm=True)
|
||||
|
||||
def test_105_jpg_dispvm(self):
|
||||
filename = "/home/user/test_file.jpg"
|
||||
self.prepare_jpg(filename)
|
||||
self.open_file_and_check_viewer(filename, [],
|
||||
["shotwell", "eog", "display"],
|
||||
dispvm=True)
|
||||
|
||||
def test_106_jpeg_dispvm(self):
|
||||
filename = "/home/user/test_file.jpeg"
|
||||
self.prepare_jpg(filename)
|
||||
self.open_file_and_check_viewer(filename, [],
|
||||
["shotwell", "eog", "display"],
|
||||
dispvm=True)
|
||||
|
||||
def test_110_url_dispvm(self):
|
||||
self.open_file_and_check_viewer("https://www.qubes-os.org/", [],
|
||||
["Firefox", "Iceweasel", "Navigator"],
|
||||
dispvm=True)
|
||||
|
||||
def load_tests(loader, tests, pattern):
|
||||
try:
|
||||
qc = qubes.qubes.QubesVmCollection()
|
||||
qc.lock_db_for_reading()
|
||||
qc.load()
|
||||
qc.unlock_db()
|
||||
templates = [vm.name for vm in qc.values() if
|
||||
isinstance(vm, qubes.qubes.QubesTemplateVm)]
|
||||
except OSError:
|
||||
templates = []
|
||||
for template in templates:
|
||||
tests.addTests(loader.loadTestsFromTestCase(
|
||||
type(
|
||||
'TC_50_MimeHandlers_' + template,
|
||||
(TC_50_MimeHandlers, qubes.tests.QubesTestCase),
|
||||
{'template': template})))
|
||||
return tests
|
||||
@ -54,6 +54,9 @@ class VmNetworkingMixin(qubes.tests.SystemTestsMixin):
|
||||
|
||||
def setUp(self):
|
||||
super(VmNetworkingMixin, self).setUp()
|
||||
if self.template.startswith('whonix-'):
|
||||
self.skipTest("Test not supported here - Whonix uses its own "
|
||||
"firewall settings")
|
||||
self.testnetvm = self.qc.add_new_vm("QubesNetVm",
|
||||
name=self.make_vm_name('netvm1'),
|
||||
template=self.qc.get_vm_by_name(self.template))
|
||||
@ -85,6 +88,8 @@ class VmNetworkingMixin(qubes.tests.SystemTestsMixin):
|
||||
run_netvm_cmd("ip link set test0 up")
|
||||
run_netvm_cmd("ip addr add {}/24 dev test0".format(self.test_ip))
|
||||
run_netvm_cmd("iptables -I INPUT -d {} -j ACCEPT".format(self.test_ip))
|
||||
# ignore failure
|
||||
self.run_cmd(self.testnetvm, "killall --wait dnsmasq")
|
||||
run_netvm_cmd("dnsmasq -a {ip} -A /{name}/{ip} -i test0 -z".format(
|
||||
ip=self.test_ip, name=self.test_name))
|
||||
run_netvm_cmd("echo nameserver {} > /etc/resolv.conf".format(
|
||||
@ -121,6 +126,7 @@ class VmNetworkingMixin(qubes.tests.SystemTestsMixin):
|
||||
"Ping by IP from AppVM failed")
|
||||
|
||||
|
||||
@qubes.tests.expectedFailureIfTemplate('debian-7')
|
||||
@unittest.skipUnless(spawn.find_executable('xdotool'),
|
||||
"xdotool not installed")
|
||||
def test_020_simple_proxyvm_nm(self):
|
||||
@ -159,8 +165,8 @@ class VmNetworkingMixin(qubes.tests.SystemTestsMixin):
|
||||
|
||||
# check for nm-applet presence
|
||||
self.assertEqual(subprocess.call([
|
||||
'xdotool', 'search', '--all', '--name',
|
||||
'--class', '^(NetworkManager Applet|{})$'.format(self.proxy.name)],
|
||||
'xdotool', 'search', '--class', '{}:nm-applet'.format(
|
||||
self.proxy.name)],
|
||||
stdout=open('/dev/null', 'w')), 0, "nm-applet window not found")
|
||||
self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0,
|
||||
"Ping by IP failed (after NM reconnection")
|
||||
@ -320,12 +326,29 @@ class VmNetworkingMixin(qubes.tests.SystemTestsMixin):
|
||||
self.testvm1.start()
|
||||
|
||||
self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0)
|
||||
self.testvm1.run("ip addr flush dev eth0", user="root")
|
||||
self.testvm1.run("ip addr add 10.137.1.128/24 dev eth0", user="root")
|
||||
self.testvm1.run("ip route add dev eth0", user="root")
|
||||
self.testvm1.run("ip addr flush dev eth0", user="root", wait=True)
|
||||
self.testvm1.run("ip addr add 10.137.1.128/24 dev eth0", user="root",
|
||||
wait=True)
|
||||
self.testvm1.run("ip route add default dev eth0", user="root",
|
||||
wait=True)
|
||||
self.assertNotEqual(self.run_cmd(self.testvm1, self.ping_ip), 0,
|
||||
"Spoofed ping should be blocked")
|
||||
|
||||
def test_100_late_xldevd_startup(self):
|
||||
"""Regression test for #1990"""
|
||||
self.qc.unlock_db()
|
||||
# Simulater late xl devd startup
|
||||
cmd = "systemctl stop xendriverdomain"
|
||||
if self.run_cmd(self.testnetvm, cmd) != 0:
|
||||
self.fail("Command '%s' failed" % cmd)
|
||||
self.testvm1.start()
|
||||
|
||||
cmd = "systemctl start xendriverdomain"
|
||||
if self.run_cmd(self.testnetvm, cmd) != 0:
|
||||
self.fail("Command '%s' failed" % cmd)
|
||||
|
||||
self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0)
|
||||
|
||||
class VmUpdatesMixin(qubes.tests.SystemTestsMixin):
|
||||
"""
|
||||
Tests for VM updates
|
||||
@ -495,10 +518,10 @@ class VmUpdatesMixin(qubes.tests.SystemTestsMixin):
|
||||
p = self.netvm_repo.run(
|
||||
"mkdir -p /tmp/apt-repo/dists/test && "
|
||||
"cd /tmp/apt-repo/dists/test && "
|
||||
"cat > Release <<EOF && "
|
||||
"echo '' $(sha1sum {p} | cut -f 1 -d ' ') $(stat -c %s {p}) {p}"
|
||||
"cat > Release && "
|
||||
"echo '' $(sha256sum {p} | cut -f 1 -d ' ') $(stat -c %s {p}) {p}"
|
||||
" >> Release && "
|
||||
"echo '' $(sha1sum {z} | cut -f 1 -d ' ') $(stat -c %s {z}) {z}"
|
||||
"echo '' $(sha256sum {z} | cut -f 1 -d ' ') $(stat -c %s {z}) {z}"
|
||||
" >> Release"
|
||||
.format(p="main/binary-amd64/Packages",
|
||||
z="main/binary-amd64/Packages.gz"),
|
||||
@ -508,11 +531,10 @@ class VmUpdatesMixin(qubes.tests.SystemTestsMixin):
|
||||
"Label: Test repo\n"
|
||||
"Suite: test\n"
|
||||
"Codename: test\n"
|
||||
"Date: Tue, 27 Oct 2015 03:22:09 +0100\n"
|
||||
"Date: Tue, 27 Oct 2015 03:22:09 UTC\n"
|
||||
"Architectures: amd64\n"
|
||||
"Components: main\n"
|
||||
"SHA1:\n"
|
||||
"EOF\n"
|
||||
"SHA256:\n"
|
||||
)
|
||||
p.stdin.close()
|
||||
if p.wait() != 0:
|
||||
|
||||
172
tests/pvgrub.py
Normal file
172
tests/pvgrub.py
Normal file
@ -0,0 +1,172 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2016 Marek Marczykowski-Górecki
|
||||
# <marmarek@invisiblethingslab.com>
|
||||
#
|
||||
# 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 unittest
|
||||
import qubes.tests
|
||||
@unittest.skipUnless(os.path.exists('/var/lib/qubes/vm-kernels/pvgrub2'),
|
||||
'grub-xen package not installed')
|
||||
class TC_40_PVGrub(qubes.tests.SystemTestsMixin):
|
||||
def setUp(self):
|
||||
super(TC_40_PVGrub, self).setUp()
|
||||
supported = False
|
||||
if self.template.startswith('fedora-'):
|
||||
supported = True
|
||||
elif self.template.startswith('debian-'):
|
||||
supported = True
|
||||
if not supported:
|
||||
self.skipTest("Template {} not supported by this test".format(
|
||||
self.template))
|
||||
|
||||
def install_packages(self, vm):
|
||||
if self.template.startswith('fedora-'):
|
||||
cmd_install1 = 'dnf clean expire-cache && ' \
|
||||
'dnf install -y qubes-kernel-vm-support grub2-tools'
|
||||
cmd_install2 = 'dnf install -y kernel && ' \
|
||||
'KVER=$(rpm -q --qf %{VERSION}-%{RELEASE}.%{ARCH} kernel) && ' \
|
||||
'dnf install --allowerasing -y kernel-devel-$KVER && ' \
|
||||
'dkms autoinstall -k $KVER'
|
||||
cmd_update_grub = 'grub2-mkconfig -o /boot/grub2/grub.cfg'
|
||||
elif self.template.startswith('debian-'):
|
||||
cmd_install1 = 'apt-get update && apt-get install -y ' \
|
||||
'qubes-kernel-vm-support grub2-common'
|
||||
cmd_install2 = 'apt-get install -y linux-image-amd64'
|
||||
cmd_update_grub = 'mkdir /boot/grub && update-grub2'
|
||||
else:
|
||||
assert False, "Unsupported template?!"
|
||||
|
||||
for cmd in [cmd_install1, cmd_install2, cmd_update_grub]:
|
||||
p = vm.run(cmd, user="root", passio_popen=True, passio_stderr=True)
|
||||
(stdout, stderr) = p.communicate()
|
||||
self.assertEquals(p.returncode, 0,
|
||||
"Failed command: {}\nSTDOUT: {}\nSTDERR: {}"
|
||||
.format(cmd, stdout, stderr))
|
||||
|
||||
def get_kernel_version(self, vm):
|
||||
if self.template.startswith('fedora-'):
|
||||
cmd_get_kernel_version = 'rpm -q kernel|sort -n|tail -1|' \
|
||||
'cut -d - -f 2-'
|
||||
elif self.template.startswith('debian-'):
|
||||
cmd_get_kernel_version = \
|
||||
'dpkg-query --showformat=\'${Package}\\n\' --show ' \
|
||||
'\'linux-image-*-amd64\'|sort -n|tail -1|cut -d - -f 3-'
|
||||
else:
|
||||
raise RuntimeError("Unsupported template?!")
|
||||
|
||||
p = vm.run(cmd_get_kernel_version, user="root", passio_popen=True)
|
||||
(kver, _) = p.communicate()
|
||||
self.assertEquals(p.returncode, 0,
|
||||
"Failed command: {}".format(cmd_get_kernel_version))
|
||||
return kver.strip()
|
||||
|
||||
def test_000_standalone_vm(self):
|
||||
testvm1 = self.qc.add_new_vm("QubesAppVm",
|
||||
template=None,
|
||||
name=self.make_vm_name('vm1'))
|
||||
testvm1.create_on_disk(verbose=False,
|
||||
source_template=self.qc.get_vm_by_name(
|
||||
self.template))
|
||||
self.save_and_reload_db()
|
||||
self.qc.unlock_db()
|
||||
testvm1 = self.qc[testvm1.qid]
|
||||
testvm1.start()
|
||||
self.install_packages(testvm1)
|
||||
kver = self.get_kernel_version(testvm1)
|
||||
self.shutdown_and_wait(testvm1)
|
||||
|
||||
self.qc.lock_db_for_writing()
|
||||
self.qc.load()
|
||||
testvm1 = self.qc[testvm1.qid]
|
||||
testvm1.kernel = 'pvgrub2'
|
||||
self.save_and_reload_db()
|
||||
self.qc.unlock_db()
|
||||
testvm1 = self.qc[testvm1.qid]
|
||||
testvm1.start()
|
||||
p = testvm1.run('uname -r', passio_popen=True)
|
||||
(actual_kver, _) = p.communicate()
|
||||
self.assertEquals(actual_kver.strip(), kver)
|
||||
|
||||
def test_010_template_based_vm(self):
|
||||
test_template = self.qc.add_new_vm("QubesTemplateVm",
|
||||
template=None,
|
||||
name=self.make_vm_name('template'))
|
||||
test_template.clone_attrs(self.qc.get_vm_by_name(self.template))
|
||||
test_template.clone_disk_files(
|
||||
src_vm=self.qc.get_vm_by_name(self.template),
|
||||
verbose=False)
|
||||
|
||||
testvm1 = self.qc.add_new_vm("QubesAppVm",
|
||||
template=test_template,
|
||||
name=self.make_vm_name('vm1'))
|
||||
testvm1.create_on_disk(verbose=False,
|
||||
source_template=test_template)
|
||||
self.save_and_reload_db()
|
||||
self.qc.unlock_db()
|
||||
test_template = self.qc[test_template.qid]
|
||||
testvm1 = self.qc[testvm1.qid]
|
||||
test_template.start()
|
||||
self.install_packages(test_template)
|
||||
kver = self.get_kernel_version(test_template)
|
||||
self.shutdown_and_wait(test_template)
|
||||
|
||||
self.qc.lock_db_for_writing()
|
||||
self.qc.load()
|
||||
test_template = self.qc[test_template.qid]
|
||||
test_template.kernel = 'pvgrub2'
|
||||
testvm1 = self.qc[testvm1.qid]
|
||||
testvm1.kernel = 'pvgrub2'
|
||||
self.save_and_reload_db()
|
||||
self.qc.unlock_db()
|
||||
|
||||
# Check if TemplateBasedVM boots and has the right kernel
|
||||
testvm1 = self.qc[testvm1.qid]
|
||||
testvm1.start()
|
||||
p = testvm1.run('uname -r', passio_popen=True)
|
||||
(actual_kver, _) = p.communicate()
|
||||
self.assertEquals(actual_kver.strip(), kver)
|
||||
|
||||
# And the same for the TemplateVM itself
|
||||
test_template = self.qc[test_template.qid]
|
||||
test_template.start()
|
||||
p = test_template.run('uname -r', passio_popen=True)
|
||||
(actual_kver, _) = p.communicate()
|
||||
self.assertEquals(actual_kver.strip(), kver)
|
||||
|
||||
def load_tests(loader, tests, pattern):
|
||||
try:
|
||||
qc = qubes.qubes.QubesVmCollection()
|
||||
qc.lock_db_for_reading()
|
||||
qc.load()
|
||||
qc.unlock_db()
|
||||
templates = [vm.name for vm in qc.values() if
|
||||
isinstance(vm, qubes.qubes.QubesTemplateVm)]
|
||||
except OSError:
|
||||
templates = []
|
||||
for template in templates:
|
||||
tests.addTests(loader.loadTestsFromTestCase(
|
||||
type(
|
||||
'TC_40_PVGrub_' + template,
|
||||
(TC_40_PVGrub, qubes.tests.QubesTestCase),
|
||||
{'template': template})))
|
||||
return tests
|
||||
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/python2 -O
|
||||
# coding=utf-8
|
||||
|
||||
#
|
||||
# The Qubes OS Project, https://www.qubes-os.org/
|
||||
@ -23,11 +24,14 @@
|
||||
#
|
||||
|
||||
import multiprocessing
|
||||
import os
|
||||
import time
|
||||
import unittest
|
||||
|
||||
import qubes.qubes
|
||||
import qubes.tests
|
||||
import subprocess
|
||||
|
||||
|
||||
class TC_00_Regressions(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
# Bug: #906
|
||||
@ -56,3 +60,21 @@ class TC_00_Regressions(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase)
|
||||
self.assertIsNotNone(qc.get_vm_by_name(vmname1))
|
||||
self.assertIsNotNone(qc.get_vm_by_name(vmname2))
|
||||
|
||||
def test_bug_1389_dispvm_qubesdb_crash(self):
|
||||
"""
|
||||
Sometimes QubesDB instance in DispVM crashes at startup.
|
||||
Unfortunately we don't have reliable way to reproduce it, so try twice
|
||||
:return:
|
||||
"""
|
||||
self.qc.unlock_db()
|
||||
for try_no in xrange(2):
|
||||
p = subprocess.Popen(['/usr/lib/qubes/qfile-daemon-dvm',
|
||||
'qubes.VMShell', 'dom0', 'DEFAULT'],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=open(os.devnull, 'w'))
|
||||
p.stdin.write("qubesdb-read /name || echo ERROR\n")
|
||||
dispvm_name = p.stdout.readline()
|
||||
p.stdin.close()
|
||||
self.assertTrue(dispvm_name.startswith("disp"),
|
||||
"Try {} failed".format(try_no))
|
||||
|
||||
13
tests/run.py
13
tests/run.py
@ -133,7 +133,11 @@ class QubesTestResult(unittest.TestResult):
|
||||
|
||||
def addError(self, test, err): # pylint: disable=invalid-name
|
||||
super(QubesTestResult, self).addError(test, err)
|
||||
test.log.critical('ERROR ({err[0].__name__}: {err[1]!r})'.format(err=err))
|
||||
try:
|
||||
test.log.critical(
|
||||
'ERROR ({err[0].__name__}: {err[1]!r})'.format(err=err))
|
||||
except AttributeError:
|
||||
pass
|
||||
if self.showAll:
|
||||
self.stream.writeln(
|
||||
'{color[red]}{color[bold]}ERROR{color[normal]} ({})'.format(
|
||||
@ -157,7 +161,10 @@ class QubesTestResult(unittest.TestResult):
|
||||
|
||||
def addSkip(self, test, reason): # pylint: disable=invalid-name
|
||||
super(QubesTestResult, self).addSkip(test, reason)
|
||||
test.log.warning('skipped ({})'.format(reason))
|
||||
try:
|
||||
test.log.warning('skipped ({})'.format(reason))
|
||||
except AttributeError:
|
||||
pass
|
||||
if self.showAll:
|
||||
self.stream.writeln(
|
||||
'{color[cyan]}skipped{color[normal]} ({})'.format(
|
||||
@ -307,7 +314,7 @@ def main():
|
||||
for name in args.names:
|
||||
suite.addTests(
|
||||
[test for test in list_test_cases(alltests)
|
||||
if (str(test)+'/').startswith(name.replace('.', '/')+'/')])
|
||||
if str(test).startswith(name)])
|
||||
else:
|
||||
suite.addTests(loader.loadTestsFromName('qubes.tests'))
|
||||
|
||||
|
||||
77
tests/storage.py
Normal file
77
tests/storage.py
Normal file
@ -0,0 +1,77 @@
|
||||
# The Qubes OS Project, https://www.qubes-os.org/
|
||||
#
|
||||
# Copyright (C) 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
|
||||
#
|
||||
# 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 qubes.storage
|
||||
from qubes.qubes import defaults
|
||||
from qubes.storage.xen import XenPool, XenStorage
|
||||
from qubes.tests import QubesTestCase, SystemTestsMixin
|
||||
|
||||
|
||||
class TC_00_Storage(SystemTestsMixin, QubesTestCase):
|
||||
|
||||
""" This class tests the utility methods from :mod:``qubes.storage`` """
|
||||
|
||||
def test_000_dump(self):
|
||||
""" Dumps storage instance to a storage string """
|
||||
vmname = self.make_vm_name('appvm')
|
||||
template = self.qc.get_default_template()
|
||||
vm = self.qc.add_new_vm('QubesAppVm', name=vmname,
|
||||
pool_name='default', template=template)
|
||||
storage = vm.storage
|
||||
result = qubes.storage.dump(storage)
|
||||
expected = 'qubes.storage.xen.XenStorage'
|
||||
self.assertEquals(result, expected)
|
||||
|
||||
def test_001_load(self):
|
||||
""" Loads storage driver from a storage string """
|
||||
result = qubes.storage.load('qubes.storage.xen.XenStorage')
|
||||
self.assertTrue(result is XenStorage)
|
||||
|
||||
def test_002_default_pool_drivers(self):
|
||||
""" The only predifined pool driver is xen """
|
||||
result = defaults['pool_drivers'].keys()
|
||||
expected = ["xen"]
|
||||
self.assertEquals(result, expected)
|
||||
|
||||
def test_003_get_pool_klass(self):
|
||||
""" Expect the default pool to be `XenPool` """
|
||||
result = qubes.storage._get_pool_klass('default')
|
||||
self.assertTrue(result is XenPool)
|
||||
|
||||
def test_004_pool_exists_default(self):
|
||||
""" Expect the default pool to exists """
|
||||
self.assertTrue(qubes.storage.pool_exists('default'))
|
||||
|
||||
def test_005_pool_exists_random(self):
|
||||
""" Expect this pool to not a exist """
|
||||
self.assertFalse(
|
||||
qubes.storage.pool_exists('asdh312096r832598213iudhas'))
|
||||
|
||||
def test_006_add_remove_pool(self):
|
||||
""" Tries to adding and removing a pool. """
|
||||
pool_name = 'asdjhrp89132'
|
||||
|
||||
# make sure it's really does not exist
|
||||
qubes.storage.remove_pool(pool_name)
|
||||
|
||||
qubes.storage.add_pool(pool_name, driver='xen')
|
||||
self.assertTrue(qubes.storage.pool_exists(pool_name))
|
||||
|
||||
qubes.storage.remove_pool(pool_name)
|
||||
self.assertFalse(qubes.storage.pool_exists(pool_name))
|
||||
228
tests/storage_xen.py
Normal file
228
tests/storage_xen.py
Normal file
@ -0,0 +1,228 @@
|
||||
# The Qubes OS Project, https://www.qubes-os.org/
|
||||
#
|
||||
# Copyright (C) 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
|
||||
#
|
||||
# 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 shutil
|
||||
import qubes.storage
|
||||
from qubes.tests import QubesTestCase, SystemTestsMixin
|
||||
from qubes.storage.xen import XenStorage
|
||||
|
||||
|
||||
class TC_00_XenPool(SystemTestsMixin, QubesTestCase):
|
||||
|
||||
""" This class tests some properties of the 'default' pool. """
|
||||
|
||||
def test000_default_pool_dir(self):
|
||||
""" The predefined dir for the default pool should be ``/var/lib/qubes``
|
||||
|
||||
.. sealso::
|
||||
Data :data:``qubes.qubes.defaults['pool_config']``.
|
||||
"""
|
||||
vm = self._init_app_vm()
|
||||
result = qubes.storage.get_pool("default", vm).dir_path
|
||||
expected = '/var/lib/qubes/'
|
||||
self.assertEquals(result, expected)
|
||||
|
||||
def test001_default_storage_class(self):
|
||||
""" Check when using default pool the Storage is ``XenStorage``. """
|
||||
result = self._init_app_vm().storage
|
||||
self.assertIsInstance(result, XenStorage)
|
||||
|
||||
def test_002_default_pool_name(self):
|
||||
""" Default pool_name is 'default'. """
|
||||
vm = self._init_app_vm()
|
||||
self.assertEquals(vm.pool_name, "default")
|
||||
|
||||
def _init_app_vm(self):
|
||||
""" Return initalised, but not created, AppVm. """
|
||||
vmname = self.make_vm_name('appvm')
|
||||
template = self.qc.get_default_template()
|
||||
return self.qc.add_new_vm('QubesAppVm', name=vmname, template=template,
|
||||
pool_name='default')
|
||||
|
||||
|
||||
class TC_01_XenPool(SystemTestsMixin, QubesTestCase):
|
||||
|
||||
""" Test the paths for the default Xen file based storage (``XenStorage``).
|
||||
"""
|
||||
|
||||
POOL_DIR = '/var/lib/qubes/test-pool'
|
||||
APPVMS_DIR = '/var/lib/qubes/test-pool/appvms'
|
||||
TEMPLATES_DIR = '/var/lib/qubes/test-pool/vm-templates'
|
||||
SERVICE_DIR = '/var/lib/qubes/test-pool/servicevms'
|
||||
|
||||
def setUp(self):
|
||||
""" Add a test file based storage pool """
|
||||
super(TC_01_XenPool, self).setUp()
|
||||
qubes.storage.add_pool('test-pool', driver='xen',
|
||||
dir_path=self.POOL_DIR)
|
||||
|
||||
def tearDown(self):
|
||||
""" Remove the file based storage pool after testing """
|
||||
super(TC_01_XenPool, self).tearDown()
|
||||
qubes.storage.remove_pool("test-pool")
|
||||
shutil.rmtree(self.POOL_DIR, ignore_errors=True)
|
||||
|
||||
def test_001_pool_exists(self):
|
||||
""" Check if the storage pool was added to the storage pool config """
|
||||
self.assertTrue(qubes.storage.pool_exists('test-pool'))
|
||||
|
||||
def test_002_pool_dir_create(self):
|
||||
""" Check if the storage pool dir and subdirs were created """
|
||||
|
||||
# The dir should not exists before
|
||||
self.assertFalse(os.path.exists(self.POOL_DIR))
|
||||
|
||||
vmname = self.make_vm_name('appvm')
|
||||
template = self.qc.get_default_template()
|
||||
self.qc.add_new_vm('QubesAppVm', name=vmname, template=template,
|
||||
pool_name='test-pool')
|
||||
|
||||
self.assertTrue(os.path.exists(self.POOL_DIR))
|
||||
self.assertTrue(os.path.exists(self.APPVMS_DIR))
|
||||
self.assertTrue(os.path.exists(self.SERVICE_DIR))
|
||||
self.assertTrue(os.path.exists(self.TEMPLATES_DIR))
|
||||
|
||||
def test_003_pool_dir(self):
|
||||
""" Check if the vm storage pool_dir is the same as specified """
|
||||
vmname = self.make_vm_name('appvm')
|
||||
template = self.qc.get_default_template()
|
||||
vm = self.qc.add_new_vm('QubesAppVm', name=vmname, template=template,
|
||||
pool_name='test-pool')
|
||||
result = qubes.storage.get_pool('test-pool', vm).dir_path
|
||||
self.assertEquals(self.POOL_DIR, result)
|
||||
|
||||
def test_004_app_vmdir(self):
|
||||
""" Check the vm storage dir for an AppVm"""
|
||||
vmname = self.make_vm_name('appvm')
|
||||
template = self.qc.get_default_template()
|
||||
vm = self.qc.add_new_vm('QubesAppVm', name=vmname, template=template,
|
||||
pool_name='test-pool')
|
||||
|
||||
expected = os.path.join(self.APPVMS_DIR, vm.name)
|
||||
result = vm.storage.vmdir
|
||||
self.assertEquals(expected, result)
|
||||
|
||||
def test_005_hvm_vmdir(self):
|
||||
""" Check the vm storage dir for a HVM"""
|
||||
vmname = self.make_vm_name('hvm')
|
||||
vm = self.qc.add_new_vm('QubesHVm', name=vmname,
|
||||
pool_name='test-pool')
|
||||
|
||||
expected = os.path.join(self.APPVMS_DIR, vm.name)
|
||||
result = vm.storage.vmdir
|
||||
self.assertEquals(expected, result)
|
||||
|
||||
def test_006_net_vmdir(self):
|
||||
""" Check the vm storage dir for a Netvm"""
|
||||
vmname = self.make_vm_name('hvm')
|
||||
vm = self.qc.add_new_vm('QubesNetVm', name=vmname,
|
||||
pool_name='test-pool')
|
||||
|
||||
expected = os.path.join(self.SERVICE_DIR, vm.name)
|
||||
result = vm.storage.vmdir
|
||||
self.assertEquals(expected, result)
|
||||
|
||||
def test_007_proxy_vmdir(self):
|
||||
""" Check the vm storage dir for a ProxyVm"""
|
||||
vmname = self.make_vm_name('proxyvm')
|
||||
vm = self.qc.add_new_vm('QubesProxyVm', name=vmname,
|
||||
pool_name='test-pool')
|
||||
|
||||
expected = os.path.join(self.SERVICE_DIR, vm.name)
|
||||
result = vm.storage.vmdir
|
||||
self.assertEquals(expected, result)
|
||||
|
||||
def test_008_admin_vmdir(self):
|
||||
""" Check the vm storage dir for a AdminVm"""
|
||||
# TODO How to test AdminVm?
|
||||
pass
|
||||
|
||||
def test_009_template_vmdir(self):
|
||||
""" Check the vm storage dir for a TemplateVm"""
|
||||
vmname = self.make_vm_name('templatevm')
|
||||
vm = self.qc.add_new_vm('QubesTemplateVm', name=vmname,
|
||||
pool_name='test-pool')
|
||||
|
||||
expected = os.path.join(self.TEMPLATES_DIR, vm.name)
|
||||
result = vm.storage.vmdir
|
||||
self.assertEquals(expected, result)
|
||||
|
||||
def test_010_template_hvm_vmdir(self):
|
||||
""" Check the vm storage dir for a TemplateHVm"""
|
||||
vmname = self.make_vm_name('templatehvm')
|
||||
vm = self.qc.add_new_vm('QubesTemplateHVm', name=vmname,
|
||||
pool_name='test-pool')
|
||||
|
||||
expected = os.path.join(self.TEMPLATES_DIR, vm.name)
|
||||
result = vm.storage.vmdir
|
||||
self.assertEquals(expected, result)
|
||||
|
||||
def test_011_appvm_file_images(self):
|
||||
""" Check if all the needed image files are created for an AppVm"""
|
||||
|
||||
vmname = self.make_vm_name('appvm')
|
||||
template = self.qc.get_default_template()
|
||||
vm = self.qc.add_new_vm('QubesAppVm', name=vmname, template=template,
|
||||
pool_name='test-pool')
|
||||
vm.create_on_disk(verbose=False)
|
||||
|
||||
expected_vmdir = os.path.join(self.APPVMS_DIR, vm.name)
|
||||
self.assertEqualsAndExists(vm.storage.vmdir, expected_vmdir)
|
||||
|
||||
expected_private_path = os.path.join(expected_vmdir, 'private.img')
|
||||
self.assertEqualsAndExists(vm.storage.private_img,
|
||||
expected_private_path)
|
||||
|
||||
expected_volatile_path = os.path.join(expected_vmdir, 'volatile.img')
|
||||
self.assertEqualsAndExists(vm.storage.volatile_img,
|
||||
expected_volatile_path)
|
||||
|
||||
def test_012_hvm_file_images(self):
|
||||
""" Check if all the needed image files are created for a HVm"""
|
||||
|
||||
vmname = self.make_vm_name('hvm')
|
||||
vm = self.qc.add_new_vm('QubesHVm', name=vmname,
|
||||
pool_name='test-pool')
|
||||
vm.create_on_disk(verbose=False)
|
||||
|
||||
expected_vmdir = os.path.join(self.APPVMS_DIR, vm.name)
|
||||
self.assertEqualsAndExists(vm.storage.vmdir, expected_vmdir)
|
||||
|
||||
expected_private_path = os.path.join(expected_vmdir, 'private.img')
|
||||
self.assertEqualsAndExists(vm.storage.private_img,
|
||||
expected_private_path)
|
||||
|
||||
expected_root_path = os.path.join(expected_vmdir, 'root.img')
|
||||
self.assertEqualsAndExists(vm.storage.root_img, expected_root_path)
|
||||
|
||||
expected_volatile_path = os.path.join(expected_vmdir, 'volatile.img')
|
||||
self.assertEqualsAndExists(vm.storage.volatile_img,
|
||||
expected_volatile_path)
|
||||
|
||||
def assertEqualsAndExists(self, result_path, expected_path):
|
||||
""" Check if the ``result_path``, matches ``expected_path`` and exists.
|
||||
|
||||
See also: :meth:``assertExist``
|
||||
"""
|
||||
self.assertEquals(result_path, expected_path)
|
||||
self.assertExist(result_path)
|
||||
|
||||
def assertExist(self, path):
|
||||
""" Assert that the given path exists. """
|
||||
self.assertTrue(os.path.exists(path))
|
||||
@ -33,6 +33,7 @@ import time
|
||||
from qubes.qubes import QubesVmCollection, defaults, QubesException
|
||||
|
||||
import qubes.tests
|
||||
import re
|
||||
|
||||
TEST_DATA = "0123456789" * 1024
|
||||
|
||||
@ -54,24 +55,6 @@ class TC_00_AppVMMixin(qubes.tests.SystemTestsMixin):
|
||||
self.testvm1 = self.qc[self.testvm1.qid]
|
||||
self.testvm2 = self.qc[self.testvm2.qid]
|
||||
|
||||
def enter_keys_in_window(self, title, keys):
|
||||
"""
|
||||
Search for window with given title, then enter listed keys there.
|
||||
The function will wait for said window to appear.
|
||||
|
||||
:param title: title of window
|
||||
:param keys: list of keys to enter, as for `xdotool key`
|
||||
:return: None
|
||||
"""
|
||||
|
||||
# 'xdotool search --sync' sometimes crashes on some race when
|
||||
# accessing window properties
|
||||
self.wait_for_window(title)
|
||||
command = ['xdotool', 'search', '--name', title,
|
||||
'windowactivate',
|
||||
'key'] + keys
|
||||
subprocess.check_call(command)
|
||||
|
||||
def test_000_start_shutdown(self):
|
||||
self.testvm1.start()
|
||||
self.assertEquals(self.testvm1.get_power_state(), "Running")
|
||||
@ -144,7 +127,7 @@ class TC_00_AppVMMixin(qubes.tests.SystemTestsMixin):
|
||||
time.sleep(0.5)
|
||||
subprocess.check_call(
|
||||
['xdotool', 'search', '--name', title,
|
||||
'windowactivate', 'type', 'exit\n'])
|
||||
'windowactivate', '--sync', 'type', 'exit\n'])
|
||||
|
||||
wait_count = 0
|
||||
while subprocess.call(['xdotool', 'search', '--name', title],
|
||||
@ -185,7 +168,7 @@ class TC_00_AppVMMixin(qubes.tests.SystemTestsMixin):
|
||||
time.sleep(0.5)
|
||||
subprocess.check_call(
|
||||
['xdotool', 'search', '--name', title,
|
||||
'windowactivate', 'type', 'exit\n'])
|
||||
'windowactivate', '--sync', 'type', 'exit\n'])
|
||||
|
||||
wait_count = 0
|
||||
while subprocess.call(['xdotool', 'search', '--name', title],
|
||||
@ -546,17 +529,123 @@ class TC_00_AppVMMixin(qubes.tests.SystemTestsMixin):
|
||||
self.fail("Timeout, probably deadlock")
|
||||
self.assertEqual(result.value, 0, "Service call failed")
|
||||
|
||||
@unittest.skipUnless(spawn.find_executable('xdotool'),
|
||||
"xdotool not installed")
|
||||
def test_080_qrexec_service_argument_allow_default(self):
|
||||
"""Qrexec service call with argument"""
|
||||
self.testvm1.start()
|
||||
self.testvm2.start()
|
||||
p = self.testvm2.run("cat > /etc/qubes-rpc/test.Argument", user="root",
|
||||
passio_popen=True)
|
||||
p.communicate("/bin/echo $1")
|
||||
|
||||
with open("/etc/qubes-rpc/policy/test.Argument", "w") as policy:
|
||||
policy.write("%s %s allow" % (self.testvm1.name, self.testvm2.name))
|
||||
self.addCleanup(os.unlink, "/etc/qubes-rpc/policy/test.Argument")
|
||||
|
||||
p = self.testvm1.run("/usr/lib/qubes/qrexec-client-vm {} "
|
||||
"test.Argument+argument".format(self.testvm2.name),
|
||||
passio_popen=True)
|
||||
(stdout, stderr) = p.communicate()
|
||||
self.assertEqual(stdout, "argument\n")
|
||||
|
||||
def test_081_qrexec_service_argument_allow_specific(self):
|
||||
"""Qrexec service call with argument - allow only specific value"""
|
||||
self.testvm1.start()
|
||||
self.testvm2.start()
|
||||
p = self.testvm2.run("cat > /etc/qubes-rpc/test.Argument", user="root",
|
||||
passio_popen=True)
|
||||
p.communicate("/bin/echo $1")
|
||||
|
||||
with open("/etc/qubes-rpc/policy/test.Argument", "w") as policy:
|
||||
policy.write("$anyvm $anyvm deny")
|
||||
self.addCleanup(os.unlink, "/etc/qubes-rpc/policy/test.Argument")
|
||||
|
||||
with open("/etc/qubes-rpc/policy/test.Argument+argument", "w") as \
|
||||
policy:
|
||||
policy.write("%s %s allow" % (self.testvm1.name, self.testvm2.name))
|
||||
self.addCleanup(os.unlink,
|
||||
"/etc/qubes-rpc/policy/test.Argument+argument")
|
||||
|
||||
p = self.testvm1.run("/usr/lib/qubes/qrexec-client-vm {} "
|
||||
"test.Argument+argument".format(self.testvm2.name),
|
||||
passio_popen=True)
|
||||
(stdout, stderr) = p.communicate()
|
||||
self.assertEqual(stdout, "argument\n")
|
||||
|
||||
def test_082_qrexec_service_argument_deny_specific(self):
|
||||
"""Qrexec service call with argument - deny specific value"""
|
||||
self.testvm1.start()
|
||||
self.testvm2.start()
|
||||
p = self.testvm2.run("cat > /etc/qubes-rpc/test.Argument", user="root",
|
||||
passio_popen=True)
|
||||
p.communicate("/bin/echo $1")
|
||||
|
||||
with open("/etc/qubes-rpc/policy/test.Argument", "w") as policy:
|
||||
policy.write("$anyvm $anyvm allow")
|
||||
self.addCleanup(os.unlink, "/etc/qubes-rpc/policy/test.Argument")
|
||||
|
||||
with open("/etc/qubes-rpc/policy/test.Argument+argument", "w") as \
|
||||
policy:
|
||||
policy.write("%s %s deny" % (self.testvm1.name, self.testvm2.name))
|
||||
self.addCleanup(os.unlink,
|
||||
"/etc/qubes-rpc/policy/test.Argument+argument")
|
||||
|
||||
p = self.testvm1.run("/usr/lib/qubes/qrexec-client-vm {} "
|
||||
"test.Argument+argument".format(self.testvm2.name),
|
||||
passio_popen=True)
|
||||
(stdout, stderr) = p.communicate()
|
||||
self.assertEqual(stdout, "")
|
||||
self.assertEqual(p.returncode, 1, "Service request should be denied")
|
||||
|
||||
def test_083_qrexec_service_argument_specific_implementation(self):
|
||||
"""Qrexec service call with argument - argument specific
|
||||
implementatation"""
|
||||
self.testvm1.start()
|
||||
self.testvm2.start()
|
||||
p = self.testvm2.run("cat > /etc/qubes-rpc/test.Argument", user="root",
|
||||
passio_popen=True)
|
||||
p.communicate("/bin/echo $1")
|
||||
|
||||
p = self.testvm2.run("cat > /etc/qubes-rpc/test.Argument+argument",
|
||||
user="root", passio_popen=True)
|
||||
p.communicate("/bin/echo specific: $1")
|
||||
|
||||
with open("/etc/qubes-rpc/policy/test.Argument", "w") as policy:
|
||||
policy.write("%s %s allow" % (self.testvm1.name, self.testvm2.name))
|
||||
self.addCleanup(os.unlink, "/etc/qubes-rpc/policy/test.Argument")
|
||||
|
||||
p = self.testvm1.run("/usr/lib/qubes/qrexec-client-vm {} "
|
||||
"test.Argument+argument".format(self.testvm2.name),
|
||||
passio_popen=True)
|
||||
(stdout, stderr) = p.communicate()
|
||||
self.assertEqual(stdout, "specific: argument\n")
|
||||
|
||||
def test_084_qrexec_service_argument_extra_env(self):
|
||||
"""Qrexec service call with argument - extra env variables"""
|
||||
self.testvm1.start()
|
||||
self.testvm2.start()
|
||||
p = self.testvm2.run("cat > /etc/qubes-rpc/test.Argument", user="root",
|
||||
passio_popen=True)
|
||||
p.communicate("/bin/echo $QREXEC_SERVICE_FULL_NAME "
|
||||
"$QREXEC_SERVICE_ARGUMENT")
|
||||
|
||||
with open("/etc/qubes-rpc/policy/test.Argument", "w") as policy:
|
||||
policy.write("%s %s allow" % (self.testvm1.name, self.testvm2.name))
|
||||
self.addCleanup(os.unlink, "/etc/qubes-rpc/policy/test.Argument")
|
||||
|
||||
p = self.testvm1.run("/usr/lib/qubes/qrexec-client-vm {} "
|
||||
"test.Argument+argument".format(self.testvm2.name),
|
||||
passio_popen=True)
|
||||
(stdout, stderr) = p.communicate()
|
||||
self.assertEqual(stdout, "test.Argument+argument argument\n")
|
||||
|
||||
def test_100_qrexec_filecopy(self):
|
||||
self.testvm1.start()
|
||||
self.testvm2.start()
|
||||
self.qrexec_policy('qubes.Filecopy', self.testvm1.name,
|
||||
self.testvm2.name)
|
||||
p = self.testvm1.run("qvm-copy-to-vm %s /etc/passwd" %
|
||||
self.testvm2.name, passio_popen=True,
|
||||
passio_stderr=True)
|
||||
# Confirm transfer
|
||||
subprocess.check_call(
|
||||
['xdotool', 'search', '--sync', '--name', 'Question', 'key', 'y'])
|
||||
p.wait()
|
||||
self.assertEqual(p.returncode, 0, "qvm-copy-to-vm failed: %s" %
|
||||
p.stderr.read())
|
||||
@ -566,15 +655,55 @@ class TC_00_AppVMMixin(qubes.tests.SystemTestsMixin):
|
||||
wait=True)
|
||||
self.assertEqual(retcode, 0, "file differs")
|
||||
|
||||
@unittest.skipUnless(spawn.find_executable('xdotool'),
|
||||
"xdotool not installed")
|
||||
def test_105_qrexec_filemove(self):
|
||||
self.testvm1.start()
|
||||
self.testvm2.start()
|
||||
self.qrexec_policy('qubes.Filecopy', self.testvm1.name,
|
||||
self.testvm2.name)
|
||||
retcode = self.testvm1.run("cp /etc/passwd passwd", wait=True)
|
||||
assert retcode == 0, "Failed to prepare source file"
|
||||
p = self.testvm1.run("qvm-move-to-vm %s passwd" %
|
||||
self.testvm2.name, passio_popen=True,
|
||||
passio_stderr=True)
|
||||
p.wait()
|
||||
self.assertEqual(p.returncode, 0, "qvm-move-to-vm failed: %s" %
|
||||
p.stderr.read())
|
||||
retcode = self.testvm2.run("diff /etc/passwd "
|
||||
"/home/user/QubesIncoming/{}/passwd".format(
|
||||
self.testvm1.name),
|
||||
wait=True)
|
||||
self.assertEqual(retcode, 0, "file differs")
|
||||
retcode = self.testvm1.run("test -f passwd", wait=True)
|
||||
self.assertEqual(retcode, 1, "source file not removed")
|
||||
|
||||
def test_101_qrexec_filecopy_with_autostart(self):
|
||||
self.testvm1.start()
|
||||
self.qrexec_policy('qubes.Filecopy', self.testvm1.name,
|
||||
self.testvm2.name)
|
||||
p = self.testvm1.run("qvm-copy-to-vm %s /etc/passwd" %
|
||||
self.testvm2.name, passio_popen=True,
|
||||
passio_stderr=True)
|
||||
p.wait()
|
||||
self.assertEqual(p.returncode, 0, "qvm-copy-to-vm failed: %s" %
|
||||
p.stderr.read())
|
||||
# workaround for libvirt bug (domain ID isn't updated when is started
|
||||
# from other application) - details in
|
||||
# QubesOS/qubes-core-libvirt@63ede4dfb4485c4161dd6a2cc809e8fb45ca664f
|
||||
self.testvm2._libvirt_domain = None
|
||||
self.assertTrue(self.testvm2.is_running())
|
||||
retcode = self.testvm2.run("diff /etc/passwd "
|
||||
"/home/user/QubesIncoming/{}/passwd".format(
|
||||
self.testvm1.name),
|
||||
wait=True)
|
||||
self.assertEqual(retcode, 0, "file differs")
|
||||
|
||||
def test_110_qrexec_filecopy_deny(self):
|
||||
self.testvm1.start()
|
||||
self.testvm2.start()
|
||||
self.qrexec_policy('qubes.Filecopy', self.testvm1.name,
|
||||
self.testvm2.name, allow=False)
|
||||
p = self.testvm1.run("qvm-copy-to-vm %s /etc/passwd" %
|
||||
self.testvm2.name, passio_popen=True)
|
||||
# Deny transfer
|
||||
self.enter_keys_in_window('Question', ['n'])
|
||||
p.wait()
|
||||
self.assertNotEqual(p.returncode, 0, "qvm-copy-to-vm unexpectedly "
|
||||
"succeeded")
|
||||
@ -586,15 +715,13 @@ class TC_00_AppVMMixin(qubes.tests.SystemTestsMixin):
|
||||
|
||||
@unittest.skip("Xen gntalloc driver crashes when page is mapped in the "
|
||||
"same domain")
|
||||
@unittest.skipUnless(spawn.find_executable('xdotool'),
|
||||
"xdotool not installed")
|
||||
def test_120_qrexec_filecopy_self(self):
|
||||
self.testvm1.start()
|
||||
self.qrexec_policy('qubes.Filecopy', self.testvm1.name,
|
||||
self.testvm1.name)
|
||||
p = self.testvm1.run("qvm-copy-to-vm %s /etc/passwd" %
|
||||
self.testvm1.name, passio_popen=True,
|
||||
passio_stderr=True)
|
||||
# Confirm transfer
|
||||
self.enter_keys_in_window('Question', ['y'])
|
||||
p.wait()
|
||||
self.assertEqual(p.returncode, 0, "qvm-copy-to-vm failed: %s" %
|
||||
p.stderr.read())
|
||||
@ -604,6 +731,41 @@ class TC_00_AppVMMixin(qubes.tests.SystemTestsMixin):
|
||||
wait=True)
|
||||
self.assertEqual(retcode, 0, "file differs")
|
||||
|
||||
@unittest.skipUnless(spawn.find_executable('xdotool'),
|
||||
"xdotool not installed")
|
||||
def test_130_qrexec_filemove_disk_full(self):
|
||||
self.testvm1.start()
|
||||
self.testvm2.start()
|
||||
self.qrexec_policy('qubes.Filecopy', self.testvm1.name,
|
||||
self.testvm2.name)
|
||||
# Prepare test file
|
||||
prepare_cmd = ("yes teststring | dd of=testfile bs=1M "
|
||||
"count=50 iflag=fullblock")
|
||||
retcode = self.testvm1.run(prepare_cmd, wait=True)
|
||||
if retcode != 0:
|
||||
raise RuntimeError("Failed '{}' in {}".format(prepare_cmd,
|
||||
self.testvm1.name))
|
||||
# Prepare target directory with limited size
|
||||
prepare_cmd = (
|
||||
"mkdir -p /home/user/QubesIncoming && "
|
||||
"chown user /home/user/QubesIncoming && "
|
||||
"mount -t tmpfs none /home/user/QubesIncoming -o size=48M"
|
||||
)
|
||||
retcode = self.testvm2.run(prepare_cmd, user="root", wait=True)
|
||||
if retcode != 0:
|
||||
raise RuntimeError("Failed '{}' in {}".format(prepare_cmd,
|
||||
self.testvm2.name))
|
||||
p = self.testvm1.run("qvm-move-to-vm %s testfile" %
|
||||
self.testvm2.name, passio_popen=True,
|
||||
passio_stderr=True)
|
||||
# Close GUI error message
|
||||
self.enter_keys_in_window('Error', ['Return'])
|
||||
p.wait()
|
||||
self.assertNotEqual(p.returncode, 0, "qvm-move-to-vm should fail")
|
||||
retcode = self.testvm1.run("test -f testfile", wait=True)
|
||||
self.assertEqual(retcode, 0, "testfile should not be deleted in "
|
||||
"source VM")
|
||||
|
||||
def test_200_timezone(self):
|
||||
"""Test whether timezone setting is properly propagated to the VM"""
|
||||
if "whonix" in self.template:
|
||||
@ -623,6 +785,8 @@ class TC_00_AppVMMixin(qubes.tests.SystemTestsMixin):
|
||||
|
||||
def test_210_time_sync(self):
|
||||
"""Test time synchronization mechanism"""
|
||||
if self.template.startswith('whonix-'):
|
||||
self.skipTest('qvm-sync-clock disabled for Whonix VMs')
|
||||
self.testvm1.start()
|
||||
self.testvm2.start()
|
||||
(start_time, _) = subprocess.Popen(["date", "-u", "+%s"],
|
||||
@ -650,6 +814,10 @@ class TC_00_AppVMMixin(qubes.tests.SystemTestsMixin):
|
||||
self.assertEquals(retcode, 0,
|
||||
"qvm-sync-clock failed with code {}".
|
||||
format(retcode))
|
||||
# qvm-sync-clock is asynchronous - it spawns qubes.SetDateTime
|
||||
# service, send it timestamp value and exists without waiting for
|
||||
# actual time set
|
||||
time.sleep(1)
|
||||
(vm_time, _) = self.testvm1.run("date -u +%s",
|
||||
passio_popen=True).communicate()
|
||||
self.assertAlmostEquals(int(vm_time), int(start_time), delta=30)
|
||||
@ -677,7 +845,9 @@ class TC_00_AppVMMixin(qubes.tests.SystemTestsMixin):
|
||||
# First offline test
|
||||
self.testvm1.resize_private_img(4*1024**3)
|
||||
self.testvm1.start()
|
||||
p = self.testvm1.run('df --output=size /rw|tail -n 1',
|
||||
df_cmd = '( df --output=size /rw || df /rw | awk \'{print $2}\' )|' \
|
||||
'tail -n 1'
|
||||
p = self.testvm1.run(df_cmd,
|
||||
passio_popen=True)
|
||||
# new_size in 1k-blocks
|
||||
(new_size, _) = p.communicate()
|
||||
@ -685,489 +855,157 @@ class TC_00_AppVMMixin(qubes.tests.SystemTestsMixin):
|
||||
self.assertGreater(int(new_size.strip()), 3.8*1024**2)
|
||||
# Then online test
|
||||
self.testvm1.resize_private_img(6*1024**3)
|
||||
p = self.testvm1.run('df --output=size /rw|tail -n 1',
|
||||
p = self.testvm1.run(df_cmd,
|
||||
passio_popen=True)
|
||||
# new_size in 1k-blocks
|
||||
(new_size, _) = p.communicate()
|
||||
# some safety margin for FS metadata
|
||||
self.assertGreater(int(new_size.strip()), 5.8*1024**2)
|
||||
|
||||
|
||||
class TC_05_StandaloneVM(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
def test_000_create_start(self):
|
||||
testvm1 = self.qc.add_new_vm("QubesAppVm",
|
||||
template=None,
|
||||
name=self.make_vm_name('vm1'))
|
||||
testvm1.create_on_disk(verbose=False,
|
||||
source_template=self.qc.get_default_template())
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
testvm1.start()
|
||||
self.assertEquals(testvm1.get_power_state(), "Running")
|
||||
|
||||
def test_100_resize_root_img(self):
|
||||
testvm1 = self.qc.add_new_vm("QubesAppVm",
|
||||
template=None,
|
||||
name=self.make_vm_name('vm1'))
|
||||
testvm1.create_on_disk(verbose=False,
|
||||
source_template=self.qc.get_default_template())
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
testvm1.resize_root_img(20*1024**3)
|
||||
timeout = 60
|
||||
while testvm1.is_running():
|
||||
time.sleep(1)
|
||||
timeout -= 1
|
||||
if timeout == 0:
|
||||
self.fail("Timeout while waiting for VM shutdown")
|
||||
self.assertEquals(testvm1.get_root_img_sz(), 20*1024**3)
|
||||
testvm1.start()
|
||||
p = testvm1.run('df --output=size /|tail -n 1',
|
||||
passio_popen=True)
|
||||
# new_size in 1k-blocks
|
||||
(new_size, _) = p.communicate()
|
||||
# some safety margin for FS metadata
|
||||
self.assertGreater(int(new_size.strip()), 19*1024**2)
|
||||
|
||||
|
||||
class TC_10_HVM(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
# TODO: test with some OS inside
|
||||
# TODO: windows tools tests
|
||||
|
||||
def test_000_create_start(self):
|
||||
testvm1 = self.qc.add_new_vm("QubesHVm",
|
||||
name=self.make_vm_name('vm1'))
|
||||
testvm1.create_on_disk(verbose=False)
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
testvm1.start()
|
||||
self.assertEquals(testvm1.get_power_state(), "Running")
|
||||
|
||||
def test_010_create_start_template(self):
|
||||
templatevm = self.qc.add_new_vm("QubesTemplateHVm",
|
||||
name=self.make_vm_name('template'))
|
||||
templatevm.create_on_disk(verbose=False)
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
|
||||
templatevm.start()
|
||||
self.assertEquals(templatevm.get_power_state(), "Running")
|
||||
|
||||
def test_020_create_start_template_vm(self):
|
||||
templatevm = self.qc.add_new_vm("QubesTemplateHVm",
|
||||
name=self.make_vm_name('template'))
|
||||
templatevm.create_on_disk(verbose=False)
|
||||
testvm2 = self.qc.add_new_vm("QubesHVm",
|
||||
name=self.make_vm_name('vm2'),
|
||||
template=templatevm)
|
||||
testvm2.create_on_disk(verbose=False)
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
|
||||
testvm2.start()
|
||||
self.assertEquals(testvm2.get_power_state(), "Running")
|
||||
|
||||
def test_030_prevent_simultaneus_start(self):
|
||||
templatevm = self.qc.add_new_vm("QubesTemplateHVm",
|
||||
name=self.make_vm_name('template'))
|
||||
templatevm.create_on_disk(verbose=False)
|
||||
testvm2 = self.qc.add_new_vm("QubesHVm",
|
||||
name=self.make_vm_name('vm2'),
|
||||
template=templatevm)
|
||||
testvm2.create_on_disk(verbose=False)
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
|
||||
templatevm.start()
|
||||
self.assertEquals(templatevm.get_power_state(), "Running")
|
||||
self.assertRaises(QubesException, testvm2.start)
|
||||
templatevm.force_shutdown()
|
||||
testvm2.start()
|
||||
self.assertEquals(testvm2.get_power_state(), "Running")
|
||||
self.assertRaises(QubesException, templatevm.start)
|
||||
|
||||
def test_100_resize_root_img(self):
|
||||
testvm1 = self.qc.add_new_vm("QubesHVm",
|
||||
name=self.make_vm_name('vm1'))
|
||||
testvm1.create_on_disk(verbose=False)
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
testvm1.resize_root_img(30*1024**3)
|
||||
self.assertEquals(testvm1.get_root_img_sz(), 30*1024**3)
|
||||
testvm1.start()
|
||||
self.assertEquals(testvm1.get_power_state(), "Running")
|
||||
# TODO: launch some OS there and check the size
|
||||
|
||||
class TC_20_DispVMMixin(qubes.tests.SystemTestsMixin):
|
||||
def test_000_prepare_dvm(self):
|
||||
self.qc.unlock_db()
|
||||
retcode = subprocess.call(['/usr/bin/qvm-create-default-dvm',
|
||||
self.template],
|
||||
stderr=open(os.devnull, 'w'))
|
||||
self.assertEqual(retcode, 0)
|
||||
self.qc.lock_db_for_writing()
|
||||
self.qc.load()
|
||||
self.assertIsNotNone(self.qc.get_vm_by_name(
|
||||
self.template + "-dvm"))
|
||||
# TODO: check mtime of snapshot file
|
||||
|
||||
def test_010_simple_dvm_run(self):
|
||||
self.qc.unlock_db()
|
||||
p = subprocess.Popen(['/usr/lib/qubes/qfile-daemon-dvm',
|
||||
'qubes.VMShell', 'dom0', 'DEFAULT'],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=open(os.devnull, 'w'))
|
||||
(stdout, _) = p.communicate(input="echo test")
|
||||
self.assertEqual(stdout, "test\n")
|
||||
# TODO: check if DispVM is destroyed
|
||||
|
||||
@unittest.skipUnless(spawn.find_executable('xdotool'),
|
||||
"xdotool not installed")
|
||||
def test_020_gui_app(self):
|
||||
self.qc.unlock_db()
|
||||
p = subprocess.Popen(['/usr/lib/qubes/qfile-daemon-dvm',
|
||||
'qubes.VMShell', 'dom0', 'DEFAULT'],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=open(os.devnull, 'w'))
|
||||
def test_300_bug_1028_gui_memory_pinning(self):
|
||||
"""
|
||||
If VM window composition buffers are relocated in memory, GUI will
|
||||
still use old pointers and will display old pages
|
||||
:return:
|
||||
"""
|
||||
self.testvm1.memory = 800
|
||||
self.testvm1.maxmem = 800
|
||||
# exclude from memory balancing
|
||||
self.testvm1.services['meminfo-writer'] = False
|
||||
self.testvm1.start()
|
||||
# and allow large map count
|
||||
self.testvm1.run("echo 256000 > /proc/sys/vm/max_map_count",
|
||||
user="root", wait=True)
|
||||
allocator_c = (
|
||||
"#include <sys/mman.h>\n"
|
||||
"#include <stdlib.h>\n"
|
||||
"#include <stdio.h>\n"
|
||||
"\n"
|
||||
"int main(int argc, char **argv) {\n"
|
||||
" int total_pages;\n"
|
||||
" char *addr, *iter;\n"
|
||||
"\n"
|
||||
" total_pages = atoi(argv[1]);\n"
|
||||
" addr = mmap(NULL, total_pages * 0x1000, PROT_READ | "
|
||||
"PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE | MAP_POPULATE, -1, 0);\n"
|
||||
" if (addr == MAP_FAILED) {\n"
|
||||
" perror(\"mmap\");\n"
|
||||
" exit(1);\n"
|
||||
" }\n"
|
||||
" printf(\"Stage1\\n\");\n"
|
||||
" fflush(stdout);\n"
|
||||
" getchar();\n"
|
||||
" for (iter = addr; iter < addr + total_pages*0x1000; iter += "
|
||||
"0x2000) {\n"
|
||||
" if (mlock(iter, 0x1000) == -1) {\n"
|
||||
" perror(\"mlock\");\n"
|
||||
" fprintf(stderr, \"%d of %d\\n\", (iter-addr)/0x1000, "
|
||||
"total_pages);\n"
|
||||
" exit(1);\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" printf(\"Stage2\\n\");\n"
|
||||
" fflush(stdout);\n"
|
||||
" for (iter = addr+0x1000; iter < addr + total_pages*0x1000; "
|
||||
"iter += 0x2000) {\n"
|
||||
" if (munmap(iter, 0x1000) == -1) {\n"
|
||||
" perror(\"munmap\");\n"
|
||||
" exit(1);\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" printf(\"Stage3\\n\");\n"
|
||||
" fflush(stdout);\n"
|
||||
" fclose(stdout);\n"
|
||||
" getchar();\n"
|
||||
"\n"
|
||||
" return 0;\n"
|
||||
"}\n")
|
||||
|
||||
# wait for DispVM startup:
|
||||
p.stdin.write("echo test\n")
|
||||
p.stdin.flush()
|
||||
l = p.stdout.readline()
|
||||
self.assertEqual(l, "test\n")
|
||||
p = self.testvm1.run("cat > allocator.c", passio_popen=True)
|
||||
p.communicate(allocator_c)
|
||||
p = self.testvm1.run("gcc allocator.c -o allocator",
|
||||
passio_popen=True, passio_stderr=True)
|
||||
(stdout, stderr) = p.communicate()
|
||||
if p.returncode != 0:
|
||||
self.skipTest("allocator compile failed: {}".format(stderr))
|
||||
|
||||
# potential race condition, but our tests are supposed to be
|
||||
# running on dedicated machine, so should not be a problem
|
||||
self.qc.lock_db_for_reading()
|
||||
self.qc.load()
|
||||
self.qc.unlock_db()
|
||||
# drop caches to have even more memory pressure
|
||||
self.testvm1.run("echo 3 > /proc/sys/vm/drop_caches",
|
||||
user="root", wait=True)
|
||||
|
||||
max_qid = 0
|
||||
for vm in self.qc.values():
|
||||
if not vm.is_disposablevm():
|
||||
continue
|
||||
if vm.qid > max_qid:
|
||||
max_qid = vm.qid
|
||||
dispvm = self.qc[max_qid]
|
||||
self.assertNotEqual(dispvm.qid, 0, "DispVM not found in qubes.xml")
|
||||
self.assertTrue(dispvm.is_running())
|
||||
try:
|
||||
window_title = 'user@%s' % (dispvm.template.name + "-dvm")
|
||||
p.stdin.write("xterm -e "
|
||||
"\"sh -s -c 'echo \\\"\033]0;{}\007\\\";read;'\"\n".
|
||||
format(window_title))
|
||||
self.wait_for_window(window_title)
|
||||
# now fragment all free memory
|
||||
p = self.testvm1.run("grep ^MemFree: /proc/meminfo|awk '{print $2}'",
|
||||
passio_popen=True)
|
||||
memory_pages = int(p.communicate()[0].strip())
|
||||
memory_pages /= 4 # 4k pages
|
||||
alloc1 = self.testvm1.run(
|
||||
"ulimit -l unlimited; exec /home/user/allocator {}".format(
|
||||
memory_pages),
|
||||
user="root", passio_popen=True, passio_stderr=True)
|
||||
# wait for memory being allocated; can't use just .read(), because EOF
|
||||
# passing is unreliable while the process is still running
|
||||
alloc1.stdin.write("\n")
|
||||
alloc1.stdin.flush()
|
||||
alloc_out = alloc1.stdout.read(len("Stage1\nStage2\nStage3\n"))
|
||||
|
||||
time.sleep(0.5)
|
||||
subprocess.check_call(['xdotool', 'search', '--name', window_title,
|
||||
'windowactivate', 'key', 'Return'])
|
||||
if "Stage3" not in alloc_out:
|
||||
# read stderr only in case of failed assert, but still have nice
|
||||
# failure message (don't use self.fail() directly)
|
||||
self.assertIn("Stage3", alloc_out, alloc1.stderr.read())
|
||||
|
||||
wait_count = 0
|
||||
while subprocess.call(['xdotool', 'search', '--name', window_title],
|
||||
stdout=open(os.path.devnull, 'w'),
|
||||
stderr=subprocess.STDOUT) == 0:
|
||||
wait_count += 1
|
||||
if wait_count > 100:
|
||||
self.fail("Timeout while waiting for gnome-terminal "
|
||||
"termination")
|
||||
time.sleep(0.1)
|
||||
finally:
|
||||
p.stdin.close()
|
||||
# now, launch some window - it should get fragmented composition buffer
|
||||
# it is important to have some changing content there, to generate
|
||||
# content update events (aka damage notify)
|
||||
proc = self.testvm1.run("gnome-terminal --full-screen -e top",
|
||||
passio_popen=True)
|
||||
|
||||
wait_count = 0
|
||||
while dispvm.is_running():
|
||||
wait_count += 1
|
||||
if wait_count > 100:
|
||||
self.fail("Timeout while waiting for DispVM destruction")
|
||||
time.sleep(0.1)
|
||||
wait_count = 0
|
||||
while p.poll() is None:
|
||||
wait_count += 1
|
||||
if wait_count > 100:
|
||||
self.fail("Timeout while waiting for qfile-daemon-dvm "
|
||||
"termination")
|
||||
time.sleep(0.1)
|
||||
self.assertEqual(p.returncode, 0)
|
||||
# help xdotool a little...
|
||||
time.sleep(2)
|
||||
# get window ID
|
||||
search = subprocess.Popen(['xdotool', 'search', '--sync',
|
||||
'--onlyvisible', '--class', self.testvm1.name + ':.*erminal'],
|
||||
stdout=subprocess.PIPE)
|
||||
winid = search.communicate()[0].strip()
|
||||
xprop = subprocess.Popen(['xprop', '-notype', '-id', winid,
|
||||
'_QUBES_VMWINDOWID'], stdout=subprocess.PIPE)
|
||||
vm_winid = xprop.stdout.read().strip().split(' ')[4]
|
||||
|
||||
self.qc.lock_db_for_reading()
|
||||
self.qc.load()
|
||||
self.qc.unlock_db()
|
||||
self.assertIsNone(self.qc.get_vm_by_name(dispvm.name),
|
||||
"DispVM not removed from qubes.xml")
|
||||
# now free the fragmented memory and trigger compaction
|
||||
alloc1.stdin.write("\n")
|
||||
alloc1.wait()
|
||||
self.testvm1.run("echo 1 > /proc/sys/vm/compact_memory", user="root")
|
||||
|
||||
def _handle_editor(self, winid):
|
||||
(window_title, _) = subprocess.Popen(
|
||||
['xdotool', 'getwindowname', winid], stdout=subprocess.PIPE).\
|
||||
communicate()
|
||||
window_title = window_title.strip().\
|
||||
replace('(', '\(').replace(')', '\)')
|
||||
time.sleep(1)
|
||||
if "gedit" in window_title:
|
||||
subprocess.check_call(['xdotool', 'search', '--name', window_title,
|
||||
'windowactivate', 'type', 'test test 2\n'])
|
||||
time.sleep(0.5)
|
||||
subprocess.check_call(['xdotool', 'search', '--name', window_title,
|
||||
'key', 'ctrl+s', 'ctrl+q'])
|
||||
elif "emacs" in window_title:
|
||||
subprocess.check_call(['xdotool', 'search', '--name', window_title,
|
||||
'windowactivate', 'type', 'test test 2\n'])
|
||||
time.sleep(0.5)
|
||||
subprocess.check_call(['xdotool', 'search', '--name', window_title,
|
||||
'key', 'ctrl+x', 'ctrl+s'])
|
||||
subprocess.check_call(['xdotool', 'search', '--name', window_title,
|
||||
'key', 'ctrl+x', 'ctrl+c'])
|
||||
elif "vim" in window_title:
|
||||
subprocess.check_call(['xdotool', 'search', '--name', window_title,
|
||||
'windowactivate', 'key', 'i',
|
||||
'type', 'test test 2\n'])
|
||||
subprocess.check_call(
|
||||
['xdotool', 'search', '--name', window_title,
|
||||
'key', 'Escape', 'colon', 'w', 'q', 'Return'])
|
||||
else:
|
||||
self.fail("Unknown editor window: {}".format(window_title))
|
||||
# now window may be already "broken"; to be sure, allocate (=zero)
|
||||
# some memory
|
||||
alloc2 = self.testvm1.run(
|
||||
"ulimit -l unlimited; /home/user/allocator {}".format(memory_pages),
|
||||
user="root", passio_popen=True, passio_stderr=True)
|
||||
alloc2.stdout.read(len("Stage1\n"))
|
||||
|
||||
@unittest.skipUnless(spawn.find_executable('xdotool'),
|
||||
"xdotool not installed")
|
||||
def test_030_edit_file(self):
|
||||
testvm1 = self.qc.add_new_vm("QubesAppVm",
|
||||
name=self.make_vm_name('vm1'),
|
||||
template=self.qc.get_vm_by_name(
|
||||
self.template))
|
||||
testvm1.create_on_disk(verbose=False)
|
||||
self.qc.save()
|
||||
# wait for damage notify - top updates every 3 sec by default
|
||||
time.sleep(6)
|
||||
|
||||
testvm1.start()
|
||||
testvm1.run("echo test1 > /home/user/test.txt", wait=True)
|
||||
# now take screenshot of the window, from dom0 and VM
|
||||
# choose pnm format, as it doesn't have any useless metadata - easy
|
||||
# to compare
|
||||
p = self.testvm1.run("import -window {} pnm:-".format(vm_winid),
|
||||
passio_popen=True, passio_stderr=True)
|
||||
(vm_image, stderr) = p.communicate()
|
||||
if p.returncode != 0:
|
||||
raise Exception("Failed to get VM window image: {}".format(
|
||||
stderr))
|
||||
|
||||
self.qc.unlock_db()
|
||||
p = testvm1.run("qvm-open-in-dvm /home/user/test.txt",
|
||||
passio_popen=True)
|
||||
p = subprocess.Popen(["import", "-window", winid, "pnm:-"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(dom0_image, stderr) = p.communicate()
|
||||
if p.returncode != 0:
|
||||
raise Exception("Failed to get dom0 window image: {}".format(
|
||||
stderr))
|
||||
|
||||
wait_count = 0
|
||||
winid = None
|
||||
while True:
|
||||
search = subprocess.Popen(['xdotool', 'search',
|
||||
'--onlyvisible', '--class', 'disp*'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=open(os.path.devnull, 'w'))
|
||||
retcode = search.wait()
|
||||
if retcode == 0:
|
||||
winid = search.stdout.read().strip()
|
||||
break
|
||||
wait_count += 1
|
||||
if wait_count > 100:
|
||||
self.fail("Timeout while waiting for editor window")
|
||||
time.sleep(0.3)
|
||||
|
||||
self._handle_editor(winid)
|
||||
p.wait()
|
||||
p = testvm1.run("cat /home/user/test.txt",
|
||||
passio_popen=True)
|
||||
(test_txt_content, _) = p.communicate()
|
||||
self.assertEqual(test_txt_content, "test test 2\ntest1\n")
|
||||
|
||||
|
||||
class TC_30_Gui_daemon(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
|
||||
@unittest.skipUnless(spawn.find_executable('xdotool'),
|
||||
"xdotool not installed")
|
||||
def test_000_clipboard(self):
|
||||
testvm1 = self.qc.add_new_vm("QubesAppVm",
|
||||
name=self.make_vm_name('vm1'),
|
||||
template=self.qc.get_default_template())
|
||||
testvm1.create_on_disk(verbose=False)
|
||||
testvm2 = self.qc.add_new_vm("QubesAppVm",
|
||||
name=self.make_vm_name('vm2'),
|
||||
template=self.qc.get_default_template())
|
||||
testvm2.create_on_disk(verbose=False)
|
||||
self.qc.save()
|
||||
self.qc.unlock_db()
|
||||
|
||||
testvm1.start()
|
||||
testvm2.start()
|
||||
|
||||
window_title = 'user@{}'.format(testvm1.name)
|
||||
testvm1.run('zenity --text-info --editable --title={}'.format(
|
||||
window_title))
|
||||
|
||||
self.wait_for_window(window_title)
|
||||
time.sleep(0.5)
|
||||
test_string = "test{}".format(testvm1.xid)
|
||||
|
||||
# Type and copy some text
|
||||
subprocess.check_call(['xdotool', 'search', '--name', window_title,
|
||||
'windowactivate',
|
||||
'type', '{}'.format(test_string)])
|
||||
# second xdotool call because type --terminator do not work (SEGV)
|
||||
# additionally do not use search here, so window stack will be empty
|
||||
# and xdotool will use XTEST instead of generating events manually -
|
||||
# this will be much better - at least because events will have
|
||||
# correct timestamp (so gui-daemon would not drop the copy request)
|
||||
subprocess.check_call(['xdotool',
|
||||
'key', 'ctrl+a', 'ctrl+c', 'ctrl+shift+c',
|
||||
'Escape'])
|
||||
|
||||
clipboard_content = \
|
||||
open('/var/run/qubes/qubes-clipboard.bin', 'r').read().strip()
|
||||
self.assertEquals(clipboard_content, test_string,
|
||||
"Clipboard copy operation failed - content")
|
||||
clipboard_source = \
|
||||
open('/var/run/qubes/qubes-clipboard.bin.source',
|
||||
'r').read().strip()
|
||||
self.assertEquals(clipboard_source, testvm1.name,
|
||||
"Clipboard copy operation failed - owner")
|
||||
|
||||
# Then paste it to the other window
|
||||
window_title = 'user@{}'.format(testvm2.name)
|
||||
p = testvm2.run('zenity --entry --title={} > test.txt'.format(
|
||||
window_title), passio_popen=True)
|
||||
self.wait_for_window(window_title)
|
||||
|
||||
subprocess.check_call(['xdotool', 'key', '--delay', '100',
|
||||
'ctrl+shift+v', 'ctrl+v', 'Return'])
|
||||
p.wait()
|
||||
|
||||
# And compare the result
|
||||
(test_output, _) = testvm2.run('cat test.txt',
|
||||
passio_popen=True).communicate()
|
||||
self.assertEquals(test_string, test_output.strip())
|
||||
|
||||
clipboard_content = \
|
||||
open('/var/run/qubes/qubes-clipboard.bin', 'r').read().strip()
|
||||
self.assertEquals(clipboard_content, "",
|
||||
"Clipboard not wiped after paste - content")
|
||||
clipboard_source = \
|
||||
open('/var/run/qubes/qubes-clipboard.bin.source', 'r').read(
|
||||
|
||||
).strip()
|
||||
self.assertEquals(clipboard_source, "",
|
||||
"Clipboard not wiped after paste - owner")
|
||||
|
||||
|
||||
@unittest.skipUnless(os.path.exists('/var/lib/qubes/vm-kernels/pvgrub2'),
|
||||
'grub-xen package not installed')
|
||||
class TC_40_PVGrub(qubes.tests.SystemTestsMixin):
|
||||
def setUp(self):
|
||||
super(TC_40_PVGrub, self).setUp()
|
||||
supported = False
|
||||
if self.template.startswith('fedora-'):
|
||||
supported = True
|
||||
elif self.template.startswith('debian-'):
|
||||
supported = True
|
||||
if not supported:
|
||||
self.skipTest("Template {} not supported by this test".format(
|
||||
self.template))
|
||||
|
||||
def install_packages(self, vm):
|
||||
if self.template.startswith('fedora-'):
|
||||
cmd_install1 = 'yum install -y qubes-kernel-vm-support grub2-tools'
|
||||
cmd_install2 = 'yum install -y kernel kernel-devel'
|
||||
cmd_update_grub = 'grub2-mkconfig -o /boot/grub2/grub.cfg'
|
||||
elif self.template.startswith('debian-'):
|
||||
cmd_install1 = 'apt-get update && apt-get install -y ' \
|
||||
'qubes-kernel-vm-support grub2-common'
|
||||
cmd_install2 = 'apt-get install -y linux-image-amd64'
|
||||
cmd_update_grub = 'mkdir /boot/grub && update-grub2'
|
||||
else:
|
||||
assert False, "Unsupported template?!"
|
||||
|
||||
for cmd in [cmd_install1, cmd_install2, cmd_update_grub]:
|
||||
p = vm.run(cmd, user="root", passio_popen=True, passio_stderr=True)
|
||||
(stdout, stderr) = p.communicate()
|
||||
self.assertEquals(p.returncode, 0,
|
||||
"Failed command: {}\nSTDOUT: {}\nSTDERR: {}"
|
||||
.format(cmd, stdout, stderr))
|
||||
|
||||
def get_kernel_version(self, vm):
|
||||
if self.template.startswith('fedora-'):
|
||||
cmd_get_kernel_version = 'rpm -q kernel|sort -n|tail -1|' \
|
||||
'cut -d - -f 2-'
|
||||
elif self.template.startswith('debian-'):
|
||||
cmd_get_kernel_version = \
|
||||
'dpkg-query --showformat=\'${Package}\\n\' --show ' \
|
||||
'\'linux-image-*-amd64\'|sort -n|tail -1|cut -d - -f 3-'
|
||||
else:
|
||||
raise RuntimeError("Unsupported template?!")
|
||||
|
||||
p = vm.run(cmd_get_kernel_version, user="root", passio_popen=True)
|
||||
(kver, _) = p.communicate()
|
||||
self.assertEquals(p.returncode, 0,
|
||||
"Failed command: {}".format(cmd_get_kernel_version))
|
||||
return kver.strip()
|
||||
|
||||
def test_000_standalone_vm(self):
|
||||
testvm1 = self.qc.add_new_vm("QubesAppVm",
|
||||
template=None,
|
||||
name=self.make_vm_name('vm1'))
|
||||
testvm1.create_on_disk(verbose=False,
|
||||
source_template=self.qc.get_vm_by_name(
|
||||
self.template))
|
||||
self.save_and_reload_db()
|
||||
self.qc.unlock_db()
|
||||
testvm1 = self.qc[testvm1.qid]
|
||||
testvm1.start()
|
||||
self.install_packages(testvm1)
|
||||
kver = self.get_kernel_version(testvm1)
|
||||
self.shutdown_and_wait(testvm1)
|
||||
|
||||
self.qc.lock_db_for_writing()
|
||||
self.qc.load()
|
||||
testvm1 = self.qc[testvm1.qid]
|
||||
testvm1.kernel = 'pvgrub2'
|
||||
self.save_and_reload_db()
|
||||
self.qc.unlock_db()
|
||||
testvm1 = self.qc[testvm1.qid]
|
||||
testvm1.start()
|
||||
p = testvm1.run('uname -r', passio_popen=True)
|
||||
(actual_kver, _) = p.communicate()
|
||||
self.assertEquals(actual_kver.strip(), kver)
|
||||
|
||||
def test_010_template_based_vm(self):
|
||||
test_template = self.qc.add_new_vm("QubesTemplateVm",
|
||||
template=None,
|
||||
name=self.make_vm_name('template'))
|
||||
test_template.clone_attrs(self.qc.get_vm_by_name(self.template))
|
||||
test_template.clone_disk_files(
|
||||
src_vm=self.qc.get_vm_by_name(self.template),
|
||||
verbose=False)
|
||||
|
||||
testvm1 = self.qc.add_new_vm("QubesAppVm",
|
||||
template=test_template,
|
||||
name=self.make_vm_name('vm1'))
|
||||
testvm1.create_on_disk(verbose=False,
|
||||
source_template=test_template)
|
||||
self.save_and_reload_db()
|
||||
self.qc.unlock_db()
|
||||
test_template = self.qc[test_template.qid]
|
||||
testvm1 = self.qc[testvm1.qid]
|
||||
test_template.start()
|
||||
self.install_packages(test_template)
|
||||
kver = self.get_kernel_version(test_template)
|
||||
self.shutdown_and_wait(test_template)
|
||||
|
||||
self.qc.lock_db_for_writing()
|
||||
self.qc.load()
|
||||
test_template = self.qc[test_template.qid]
|
||||
test_template.kernel = 'pvgrub2'
|
||||
testvm1 = self.qc[testvm1.qid]
|
||||
testvm1.kernel = 'pvgrub2'
|
||||
self.save_and_reload_db()
|
||||
self.qc.unlock_db()
|
||||
|
||||
# Check if TemplateBasedVM boots and has the right kernel
|
||||
testvm1 = self.qc[testvm1.qid]
|
||||
testvm1.start()
|
||||
p = testvm1.run('uname -r', passio_popen=True)
|
||||
(actual_kver, _) = p.communicate()
|
||||
self.assertEquals(actual_kver.strip(), kver)
|
||||
|
||||
# And the same for the TemplateVM itself
|
||||
test_template = self.qc[test_template.qid]
|
||||
test_template.start()
|
||||
p = test_template.run('uname -r', passio_popen=True)
|
||||
(actual_kver, _) = p.communicate()
|
||||
self.assertEquals(actual_kver.strip(), kver)
|
||||
if vm_image != dom0_image:
|
||||
self.fail("Dom0 window doesn't match VM window content")
|
||||
|
||||
|
||||
def load_tests(loader, tests, pattern):
|
||||
@ -1187,15 +1025,4 @@ def load_tests(loader, tests, pattern):
|
||||
(TC_00_AppVMMixin, qubes.tests.QubesTestCase),
|
||||
{'template': template})))
|
||||
|
||||
tests.addTests(loader.loadTestsFromTestCase(
|
||||
type(
|
||||
'TC_20_DispVM_' + template,
|
||||
(TC_20_DispVMMixin, qubes.tests.QubesTestCase),
|
||||
{'template': template})))
|
||||
tests.addTests(loader.loadTestsFromTestCase(
|
||||
type(
|
||||
'TC_40_PVGrub_' + template,
|
||||
(TC_40_PVGrub, qubes.tests.QubesTestCase),
|
||||
{'template': template})))
|
||||
|
||||
return tests
|
||||
|
||||
@ -9,20 +9,21 @@
|
||||
<loader>hvmloader</loader>
|
||||
<boot dev='cdrom'/>
|
||||
<boot dev='hd'/>
|
||||
{disable_network1}<cmdline>-net lwip,client_ip={ip},server_ip={dns2},dns={dns1},gw={gateway},netmask={netmask}</cmdline>{disable_network2}
|
||||
</os>
|
||||
<features>
|
||||
<pae/>
|
||||
<acpi/>
|
||||
<apic/>
|
||||
<viridian/>
|
||||
{features}
|
||||
</features>
|
||||
<clock offset='variable' adjustment='{timeoffset}' basis='{time_basis}'/>
|
||||
<on_poweroff>destroy</on_poweroff>
|
||||
<on_reboot>destroy</on_reboot>
|
||||
<on_crash>destroy</on_crash>
|
||||
<devices>
|
||||
<emulator type='stubdom'/>
|
||||
{no_network_begin}<emulator type='stubdom'/>{no_network_end}
|
||||
{network_begin}<emulator type='stubdom' cmdline='-net lwip,client_ip={ip},server_ip={dns2},dns={dns1},gw={gateway},netmask={netmask}'/>{network_end}
|
||||
{rootdev}
|
||||
{privatedev}
|
||||
{otherdevs}
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
<initrd>{kerneldir}/initramfs</initrd>
|
||||
<cmdline>root=/dev/mapper/dmroot ro nomodeset console=hvc0 rd_NO_PLYMOUTH 3 {kernelopts}</cmdline>
|
||||
</os>
|
||||
<features>{features}</features>
|
||||
<clock offset='utc' adjustment='reset'>
|
||||
<timer name="tsc" mode="native"/>
|
||||
</clock>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user