Move applying configuration to kickstart addon

Separate UI from applying configuration by implementing proper
anaconda/kickstart addon. This have multiple benefits:
 - entering settings window twice does not result in configuration being
   applied multiple times
 - all settings can be saved and loaded in kickstart file - this allows
   automating this part of installation too
 - it's much easier to implement other UI (text) for the same mechanism

There is one little catch: initial-setup (contrary to base anaconda)
does not provide graphical indicator for long-running system
configuration, so while configuration is progress, the UI is frozen.
Workaround this by re-using old progress dialog, but from kickstart
addon this time. This means the kickstart needs to know whether it's
running from GUI or not.

Fixes QubesOS/qubes-issues#2433
(backported from R4.1, adjusted for older anaconda)
This commit is contained in:
Marek Marczykowski-Górecki 2019-06-11 05:31:03 +02:00
parent 0ec125b967
commit 556b584f74
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
5 changed files with 494 additions and 366 deletions

View File

@ -2,4 +2,4 @@
# if X server is not yet running
#import gui
import tui
#import tui

View File

@ -4,4 +4,98 @@ work.
"""
import spokes
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
gi.require_version('GLib', '2.0')
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GLib
import logging
import threading
class ThreadDialog(Gtk.Dialog):
def __init__(self, title, fun, args, transient_for=None):
Gtk.Dialog.__init__(self, title=title, transient_for=transient_for)
self.set_modal(True)
self.set_default_size(500, 100)
self.connect('delete-event', self.on_delete_event)
self.progress = Gtk.ProgressBar()
self.progress.set_pulse_step(100)
self.progress.set_text("")
self.progress.set_show_text(False)
self.label = Gtk.Label()
self.label.set_line_wrap(True)
self.label.set_text("")
self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.box.pack_start(self.progress, True, True, 0)
self.box.pack_start(self.label, True, True, 0)
self.get_content_area().pack_start(self.box, True, True, 0)
self.fun = fun
self.args = args
self.logger = logging.getLogger("anaconda")
self.thread = threading.Thread(target=self.run_fun)
def run_fun(self):
try:
self.fun(*self.args)
except Exception as e:
self.showErrorMessage(str(e))
finally:
self.done()
def on_delete_event(self, widget=None, *args):
# Ignore the clicks on the close button by returning True.
self.logger.info("Caught delete-event")
return True
def set_text(self, text):
Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT, 0, self.label.set_text, text)
def done(self):
Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT, 100, self.done_helper, ())
def done_helper(self, *args):
self.logger.info("Joining thread.")
self.thread.join()
self.logger.info("Stopping self.")
self.response(Gtk.ResponseType.ACCEPT)
def run_in_ui_thread(self, fun, *args):
Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT, 0, fun, *args)
def run(self):
self.thread.start()
self.progress.pulse()
self.show_all()
ret = None
while ret in (None, Gtk.ResponseType.DELETE_EVENT):
ret = super(ThreadDialog, self).run()
return ret
def showErrorMessage(self, text):
self.run_in_ui_thread(self.showErrorMessageHelper, text)
def showErrorMessageHelper(self, text):
dlg = Gtk.MessageDialog(title="Error", message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK, text=text)
dlg.set_position(Gtk.WindowPosition.CENTER)
dlg.set_modal(True)
dlg.set_transient_for(self)
dlg.run()
dlg.destroy()

View File

@ -29,15 +29,7 @@
_ = lambda x: x
N_ = lambda x: x
import distutils.version
import functools
import grp
import logging
import os
import os.path
import pyudev
import subprocess
import threading
import gi
@ -46,10 +38,7 @@ gi.require_version('Gdk', '3.0')
gi.require_version('GLib', '2.0')
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GLib
from pyanaconda import iutil
from pyanaconda.ui.categories.system import SystemCategory
from pyanaconda.ui.gui.spokes import NormalSpoke
from pyanaconda.ui.common import FirstbootOnlySpokeMixIn
@ -57,53 +46,15 @@ from pyanaconda.ui.common import FirstbootOnlySpokeMixIn
# export only the spoke, no helper functions, classes or constants
__all__ = ["QubesOsSpoke"]
def is_package_installed(pkgname):
pkglist = subprocess.check_output(['rpm', '-qa', pkgname])
return bool(pkglist)
def usb_keyboard_present():
context = pyudev.Context()
keyboards = context.list_devices(subsystem='input', ID_INPUT_KEYBOARD='1')
return any([d.get('ID_USB_INTERFACES', False) for d in keyboards])
def started_from_usb():
def get_all_used_devices(dev):
stat = os.stat(dev)
if stat.st_rdev:
# XXX any better idea how to handle device-mapper?
sysfs_slaves = '/sys/dev/block/{}:{}/slaves'.format(
os.major(stat.st_rdev), os.minor(stat.st_rdev))
if os.path.exists(sysfs_slaves):
for slave_dev in os.listdir(sysfs_slaves):
for d in get_all_used_devices('/dev/{}'.format(slave_dev)):
yield d
else:
yield dev
context = pyudev.Context()
mounts = open('/proc/mounts').readlines()
for mount in mounts:
device = mount.split(' ')[0]
if not os.path.exists(device):
continue
for dev in get_all_used_devices(device):
udev_info = pyudev.Device.from_device_file(context, dev)
if udev_info.get('ID_USB_INTERFACES', False):
return True
return False
class QubesChoice(object):
instances = []
def __init__(self, label, states, depend=None, extra_check=None,
replace=None, indent=False):
def __init__(self, label, depend=None, extra_check=None,
indent=False):
self.widget = Gtk.CheckButton(label=label)
self.states = states
self.depend = depend
self.extra_check = extra_check
self.selected = None
self.replace = replace
self._can_be_sensitive = True
@ -129,6 +80,11 @@ class QubesChoice(object):
if self.selected is not None
else self.widget.get_sensitive() and self.widget.get_active())
def set_selected(self, value):
self.widget.set_active(value)
if self.selected is not None:
self.selected = value
def store_selected(self):
self.selected = self.get_selected()
@ -146,27 +102,14 @@ class QubesChoice(object):
choice.set_sensitive(not selected and
(choice.depend is None or choice.depend.get_selected()))
@classmethod
def get_states(cls):
replaced = functools.reduce(
lambda x, y: x+y if y else x,
(choice.replace for choice in cls.instances if
choice.get_selected()),
()
)
for choice in cls.instances:
if choice.get_selected():
for state in choice.states:
if state not in replaced:
yield state
class DisabledChoice(QubesChoice):
def __init__(self, label):
super(DisabledChoice, self).__init__(label, ())
super(DisabledChoice, self).__init__(label)
self.widget.set_sensitive(False)
self._can_be_sensitive = False
class QubesOsSpoke(FirstbootOnlySpokeMixIn, NormalSpoke):
"""
Since this class inherits from the FirstbootOnlySpokeMixIn, it will
@ -226,55 +169,43 @@ class QubesOsSpoke(FirstbootOnlySpokeMixIn, NormalSpoke):
self.main_box = self.builder.get_object("mainBox")
self.thread_dialog = None
self.qubes_user = None
self.qubes_gid = None
self.default_template = 'fedora-30'
self.set_stage("Start-up")
self.done = False
self.qubes_data = self.data.addons.org_qubes_os_initial_setup
self.__init_qubes_choices()
def __init_qubes_choices(self):
self.choice_network = QubesChoice(
_('Create default system qubes (sys-net, sys-firewall, default DispVM)'),
('qvm.sys-net', 'qvm.sys-firewall', 'qvm.default-dispvm'))
self.choice_system = QubesChoice(
_('Create default system qubes (sys-net, sys-firewall, default DispVM)'))
self.choice_default = QubesChoice(
_('Create default application qubes '
'(personal, work, untrusted, vault)'),
('qvm.personal', 'qvm.work', 'qvm.untrusted', 'qvm.vault'),
depend=self.choice_network)
depend=self.choice_system)
if (is_package_installed('qubes-template-whonix-gw*') and
is_package_installed('qubes-template-whonix-ws*')):
if self.qubes_data.whonix_available:
self.choice_whonix = QubesChoice(
_('Create Whonix Gateway and Workstation qubes '
'(sys-whonix, anon-whonix)'),
('qvm.sys-whonix', 'qvm.anon-whonix'),
depend=self.choice_network)
depend=self.choice_system)
else:
self.choice_whonix = DisabledChoice(_("Whonix not installed"))
self.choice_whonix_updates = QubesChoice(
_('Enable system and template updates over the Tor anonymity '
'network using Whonix'),
('qvm.updates-via-whonix',),
depend=self.choice_whonix,
indent=True)
if not usb_keyboard_present() and not started_from_usb():
if self.qubes_data.usbvm_available:
self.choice_usb = QubesChoice(
_('Create USB qube holding all USB controllers (sys-usb)'),
('qvm.sys-usb',))
_('Create USB qube holding all USB controllers (sys-usb)'))
else:
self.choice_usb = DisabledChoice(
_('USB qube configuration disabled - you are using USB '
'keyboard or USB disk'))
self.choice_usb_with_net = QubesChoice(
self.choice_usb_with_netvm = QubesChoice(
_("Use sys-net qube for both networking and USB devices"),
('pillar.qvm.sys-net-as-usbvm',),
depend=self.choice_usb,
indent=True
)
@ -289,7 +220,7 @@ class QubesOsSpoke(FirstbootOnlySpokeMixIn, NormalSpoke):
self.check_advanced.set_active(False)
self.choice_network.widget.set_active(True)
self.choice_system.widget.set_active(True)
self.choice_default.widget.set_active(True)
if self.choice_whonix.widget.get_sensitive():
self.choice_whonix.widget.set_active(True)
@ -307,6 +238,7 @@ class QubesOsSpoke(FirstbootOnlySpokeMixIn, NormalSpoke):
"""
NormalSpoke.initialize(self)
self.qubes_data.gui_mode = True
def refresh(self):
"""
@ -318,8 +250,12 @@ class QubesOsSpoke(FirstbootOnlySpokeMixIn, NormalSpoke):
"""
# nothing to do here
pass
self.choice_system.set_selected(self.qubes_data.system_vms)
self.choice_default.set_selected(self.qubes_data.default_vms)
self.choice_whonix.set_selected(self.qubes_data.whonix_vms)
self.choice_whonix_updates.set_selected(self.qubes_data.whonix_default)
self.choice_usb.set_selected(self.qubes_data.usbvm)
self.choice_usb_with_netvm.set_selected(self.qubes_data.usbvm_with_netvm)
def apply(self):
"""
@ -328,8 +264,16 @@ class QubesOsSpoke(FirstbootOnlySpokeMixIn, NormalSpoke):
"""
# nothing to do here
pass
self.qubes_data.skip = self.check_advanced.get_active()
self.qubes_data.system_vms = self.choice_system.get_selected()
self.qubes_data.default_vms = self.choice_default.get_selected()
self.qubes_data.whonix_vms = self.choice_whonix.get_selected()
self.qubes_data.whonix_default = self.choice_whonix_updates.get_selected()
self.qubes_data.usbvm = self.choice_usb.get_selected()
self.qubes_data.usbvm_with_netvm = self.choice_usb_with_netvm.get_selected()
self.qubes_data.seen = True
@property
def ready(self):
@ -354,7 +298,7 @@ class QubesOsSpoke(FirstbootOnlySpokeMixIn, NormalSpoke):
"""
return self.done
return self.qubes_data.seen
@property
def mandatory(self):
@ -388,274 +332,4 @@ class QubesOsSpoke(FirstbootOnlySpokeMixIn, NormalSpoke):
the values set in the GUI elements.
"""
for choice in QubesChoice.instances:
choice.store_selected()
self.thread_dialog = ThreadDialog("Qubes OS Setup", self.do_setup, (), transient_for=self.main_window)
self.thread_dialog.run()
self.thread_dialog.destroy()
def do_setup(self, *args):
try:
self.qubes_gid = grp.getgrnam('qubes').gr_gid
qubes_users = grp.getgrnam('qubes').gr_mem
if len(qubes_users) < 1:
self.showErrorMessage(_("You must create a user account to create default VMs."))
return
else:
self.qubes_user = qubes_users[0]
if self.check_advanced.get_active():
return
errors = []
os.setgid(self.qubes_gid)
os.umask(0o0007)
self.configure_default_kernel()
# Finish template(s) installation, because it wasn't fully possible
# from anaconda (it isn't possible to start a VM there).
# This is specific to firstboot, not general configuration.
for template in os.listdir('/var/lib/qubes/vm-templates'):
try:
self.configure_template(template,
'/var/lib/qubes/vm-templates/' + template)
except Exception as e:
errors.append((self.stage, str(e)))
self.configure_dom0()
self.configure_default_template()
self.configure_qubes()
if self.choice_network.get_selected():
self.configure_network()
if self.choice_usb.get_selected() and not self.choice_usb_with_net.get_selected():
# Workaround for #1464 (so qvm.start from salt can't be used)
self.run_command(['systemctl', 'start', 'qubes-vm@sys-usb.service'])
try:
self.configure_default_dvm()
except Exception as e:
errors.append((self.stage, str(e)))
if errors:
msg = ""
for (stage, error) in errors:
msg += "{} failed:\n{}\n\n".format(stage, error)
raise Exception(msg)
except Exception as e:
self.showErrorMessage(str(e))
finally:
self.thread_dialog.done()
self.done = True
def set_stage(self, stage):
self.stage = stage
if self.thread_dialog != None:
self.thread_dialog.set_text(stage)
def run_command(self, command, stdin=None, ignore_failure=False):
process_error = None
try:
sys_root = iutil.getSysroot()
cmd = iutil.startProgram(command, stderr=subprocess.PIPE, stdin=stdin, root=sys_root)
(stdout, stderr) = cmd.communicate()
stdout = stdout.decode("utf-8")
stderr = stderr.decode("utf-8")
if not ignore_failure and cmd.returncode != 0:
process_error = "{} failed:\nstdout: \"{}\"\nstderr: \"{}\"".format(command, stdout, stderr)
except Exception as e:
process_error = str(e)
if process_error:
self.logger.error(process_error)
raise Exception(process_error)
return (stdout, stderr)
def configure_default_kernel(self):
self.set_stage("Setting up default kernel")
installed_kernels = os.listdir('/var/lib/qubes/vm-kernels')
installed_kernels = [distutils.version.LooseVersion(x) for x in installed_kernels]
default_kernel = str(sorted(installed_kernels)[-1])
self.run_command(['/usr/bin/qubes-prefs', 'default-kernel', default_kernel])
def configure_dom0(self):
self.set_stage("Setting up administration VM (dom0)")
for service in [ 'rdisc', 'kdump', 'libvirt-guests', 'salt-minion' ]:
self.run_command(['systemctl', 'disable', '{}.service'.format(service) ], ignore_failure=True)
self.run_command(['systemctl', 'stop', '{}.service'.format(service) ], ignore_failure=True)
def configure_qubes(self):
self.set_stage('Executing qubes configuration')
try:
# get rid of initial entries (from package installation time)
os.rename('/var/log/salt/minion', '/var/log/salt/minion.install')
except OSError:
pass
# Refresh minion configuration to make sure all installed formulas are included
self.run_command(['qubesctl', 'saltutil.clear_cache'])
self.run_command(['qubesctl', 'saltutil.sync_all'])
for state in QubesChoice.get_states():
print("Setting up state: {}".format(state))
if state.startswith('pillar.'):
self.run_command(['qubesctl', 'top.enable',
state[len('pillar.'):], 'pillar=True'])
else:
self.run_command(['qubesctl', 'top.enable', state])
try:
self.run_command(['qubesctl', 'state.highstate'])
# After successful call disable all the states to not leave them
# enabled, to not interfere with later user changes (like assigning
# additional PCI devices)
for state in QubesChoice.get_states():
if not state.startswith('pillar.'):
self.run_command(['qubesctl', 'top.disable', state])
except Exception:
raise Exception(
("Qubes initial configuration failed. Login to the system and " +
"check /var/log/salt/minion for details. " +
"You can retry configuration by calling " +
"'sudo qubesctl state.highstate' in dom0 (you will get " +
"detailed state there)."))
def configure_default_template(self):
self.set_stage('Setting default template')
self.run_command(['/usr/bin/qubes-prefs', 'default-template', self.default_template])
def configure_default_dvm(self):
self.set_stage("Creating default DisposableVM")
dispvm_name = self.default_template + '-dvm'
self.run_command(['/usr/bin/qubes-prefs', 'default-dispvm',
dispvm_name])
def configure_network(self):
self.set_stage('Setting up networking')
default_netvm = 'sys-firewall'
updatevm = default_netvm
if self.choice_whonix_updates.get_selected():
updatevm = 'sys-whonix'
self.run_command(['/usr/bin/qvm-prefs', 'sys-firewall', 'netvm', 'sys-net'])
self.run_command(['/usr/bin/qubes-prefs', 'default-netvm', default_netvm])
self.run_command(['/usr/bin/qubes-prefs', 'updatevm', updatevm])
self.run_command(['/usr/bin/qubes-prefs', 'clockvm', 'sys-net'])
self.run_command(['/usr/bin/qvm-start', default_netvm])
def configure_template(self, template, path):
self.set_stage("Configuring TemplateVM {}".format(template))
self.run_command(['qvm-template-postprocess', '--really', 'post-install', template, path])
def showErrorMessage(self, text):
self.thread_dialog.run_in_ui_thread(self.showErrorMessageHelper, text)
def showErrorMessageHelper(self, text):
dlg = Gtk.MessageDialog(title="Error", message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK, text=text)
dlg.set_position(Gtk.WindowPosition.CENTER)
dlg.set_modal(True)
dlg.set_transient_for(self.thread_dialog)
dlg.run()
dlg.destroy()
class ThreadDialog(Gtk.Dialog):
def __init__(self, title, fun, args, transient_for=None):
Gtk.Dialog.__init__(self, title=title, transient_for=transient_for)
self.set_modal(True)
self.set_default_size(500, 100)
self.connect('delete-event', self.on_delete_event)
self.progress = Gtk.ProgressBar()
self.progress.set_pulse_step(100)
self.progress.set_text("")
self.progress.set_show_text(False)
self.label = Gtk.Label()
self.label.set_line_wrap(True)
self.label.set_text("")
self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.box.pack_start(self.progress, True, True, 0)
self.box.pack_start(self.label, True, True, 0)
self.get_content_area().pack_start(self.box, True, True, 0)
self.fun = fun
self.args = args
self.logger = logging.getLogger("anaconda")
self.thread = threading.Thread(target=self.fun, args=self.args)
def on_delete_event(self, widget=None, *args):
# Ignore the clicks on the close button by returning True.
self.logger.info("Caught delete-event")
return True
def set_text(self, text):
Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT, 0, self.label.set_text, text)
def done(self):
Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT, 100, self.done_helper, ())
def done_helper(self, *args):
self.logger.info("Joining thread.")
self.thread.join()
self.logger.info("Stopping self.")
self.response(Gtk.ResponseType.ACCEPT)
def run_in_ui_thread(self, fun, *args):
Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT, 0, fun, *args)
def run(self):
self.thread.start()
self.progress.pulse()
self.show_all()
ret = None
while ret in (None, Gtk.ResponseType.DELETE_EVENT):
ret = super(ThreadDialog, self).run()
return ret
if __name__ == "__main__":
import time
def hello_fun(*args):
global thread_dialog
thread_dialog.set_text("Hello, world! " * 30)
time.sleep(2)
thread_dialog.set_text("Goodbye, world!")
time.sleep(1)
thread_dialog.done()
return
logger = logging.getLogger("anaconda")
handler = logging.StreamHandler()
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
thread_dialog = ThreadDialog("Hello", hello_fun, ())
thread_dialog.run()

View File

@ -0,0 +1,360 @@
#
# The Qubes OS Project, https://www.qubes-os.org/
#
# Copyright (C) 2019 Marek Marczykowski-Górecki
# <marmarek@invisiblethingslab.com>
#
# This library 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 library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU General Public
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
#
import grp
import os
import distutils.version
import pyudev
import subprocess
from pyanaconda import iutil
from pyanaconda.addons import AddonData
from pykickstart.errors import KickstartValueError
from pyanaconda.progress import progress_message
import logging
log = logging.getLogger("anaconda")
__all__ = ['QubesData']
def is_package_installed(pkgname):
pkglist = subprocess.check_output(['rpm', '-qa', pkgname])
return bool(pkglist)
def usb_keyboard_present():
context = pyudev.Context()
keyboards = context.list_devices(subsystem='input', ID_INPUT_KEYBOARD='1')
return any([d.get('ID_USB_INTERFACES', False) for d in keyboards])
def started_from_usb():
def get_all_used_devices(dev):
stat = os.stat(dev)
if stat.st_rdev:
# XXX any better idea how to handle device-mapper?
sysfs_slaves = '/sys/dev/block/{}:{}/slaves'.format(
os.major(stat.st_rdev), os.minor(stat.st_rdev))
if os.path.exists(sysfs_slaves):
for slave_dev in os.listdir(sysfs_slaves):
for d in get_all_used_devices('/dev/{}'.format(slave_dev)):
yield d
else:
yield dev
context = pyudev.Context()
mounts = open('/proc/mounts').readlines()
for mount in mounts:
device = mount.split(' ')[0]
if not os.path.exists(device):
continue
for dev in get_all_used_devices(device):
udev_info = pyudev.Device.from_device_file(context, dev)
if udev_info.get('ID_USB_INTERFACES', False):
return True
return False
class QubesData(AddonData):
"""
Class providing and storing data for the Qubes initial setup addon
"""
bool_options = (
'system_vms', 'default_vms', 'whonix_vms', 'whonix_default', 'usbvm',
'usbvm_with_netvm', 'skip')
def __init__(self, name):
"""
:param name: name of the addon
:type name: str
"""
super(QubesData, self).__init__(name)
self.whonix_available = (
is_package_installed('qubes-template-whonix-gw*') and
is_package_installed('qubes-template-whonix-ws*'))
self.usbvm_available = (
not usb_keyboard_present() and not started_from_usb())
self.system_vms = True
self.default_vms = True
self.whonix_vms = self.whonix_available
self.whonix_default = False
self.usbvm = self.usbvm_available
self.usbvm_with_netvm = False
self.skip = False
# this is a hack, but initial-setup do not have progress hub or similar
# provision for handling lengthy self.execute() call, so we must do it
# ourselves
self.gui_mode = False
self.thread_dialog = None
# choose latest fedora template
fedora_tpls = sorted(
name for name in os.listdir('/var/lib/qubes/vm-templates')
if 'fedora' in name)
if fedora_tpls:
self.default_template = fedora_tpls[-1]
else:
print(
'ERROR: No Fedora template is installed, '
'cannot set default template!')
self.default_template = None
self.qubes_user = None
self.seen = False
def handle_header(self, lineno, args):
pass
def handle_line(self, line):
"""
:param line:
:return:
"""
try:
(param, value) = line.strip().split(maxsplit=1)
except ValueError:
raise KickstartValueError('invalid line: %s' % line)
if param in self.bool_options:
if value.lower() not in ('true', 'false'):
raise KickstartValueError(
'invalid value for bool property: %s' % line)
bool_value = value.lower() == 'true'
setattr(self, param, bool_value)
elif param == 'default_template':
self.default_template = value
else:
raise KickstartValueError('invalid parameter: %s' % param)
self.seen = True
def __str__(self):
section = "%addon {}\n".format(self.name)
for param in self.bool_options:
section += "{} {!s}\n".format(param, getattr(self, param))
section += 'default_template {}\n'.format(self.default_template)
section += '%end\n'
return section
def execute(self, storage, ksdata, instClass, users, payload):
if self.gui_mode:
from ..gui import ThreadDialog
self.thread_dialog = ThreadDialog(
"Qubes OS Setup", self.do_setup, ())
self.thread_dialog.run()
self.thread_dialog.destroy()
else:
self.do_setup()
def set_stage(self, stage):
if self.thread_dialog is not None:
self.thread_dialog.set_text(stage)
def do_setup(self):
qubes_gid = grp.getgrnam('qubes').gr_gid
qubes_users = grp.getgrnam('qubes').gr_mem
if len(qubes_users) < 1:
raise Exception(
"You must create a user account to create default VMs.")
else:
self.qubes_user = qubes_users[0]
if self.skip:
return
errors = []
os.setgid(qubes_gid)
os.umask(0o0007)
self.configure_default_kernel()
# Finish template(s) installation, because it wasn't fully possible
# from anaconda (it isn't possible to start a VM there).
# This is specific to firstboot, not general configuration.
for template in os.listdir('/var/lib/qubes/vm-templates'):
try:
self.configure_template(template,
'/var/lib/qubes/vm-templates/' + template)
except Exception as e:
errors.append(('Templates', str(e)))
self.configure_dom0()
self.configure_default_template()
self.configure_qubes()
if self.system_vms:
self.configure_network()
if self.usbvm and not self.usbvm_with_netvm:
# Workaround for #1464 (so qvm.start from salt can't be used)
self.run_command(['systemctl', 'start', 'qubes-vm@sys-usb.service'])
try:
self.configure_default_dvm()
except Exception as e:
errors.append(('Default DVM', str(e)))
if errors:
msg = ""
for (stage, error) in errors:
msg += "{} failed:\n{}\n\n".format(stage, error)
raise Exception(msg)
def run_command(self, command, stdin=None, ignore_failure=False):
process_error = None
try:
sys_root = iutil.getSysroot()
cmd = iutil.startProgram(command,
stderr=subprocess.PIPE,
stdin=stdin,
root=sys_root)
(stdout, stderr) = cmd.communicate()
stdout = stdout.decode("utf-8")
stderr = stderr.decode("utf-8")
if not ignore_failure and cmd.returncode != 0:
process_error = "{} failed:\nstdout: \"{}\"\nstderr: \"{}\"".format(command, stdout, stderr)
except Exception as e:
process_error = str(e)
if process_error:
log.error(process_error)
raise Exception(process_error)
return (stdout, stderr)
def configure_default_kernel(self):
self.set_stage("Setting up default kernel")
installed_kernels = os.listdir('/var/lib/qubes/vm-kernels')
installed_kernels = [distutils.version.LooseVersion(x) for x in installed_kernels]
default_kernel = str(sorted(installed_kernels)[-1])
self.run_command([
'/usr/bin/qubes-prefs', 'default-kernel', default_kernel])
def configure_dom0(self):
self.set_stage("Setting up administration VM (dom0)")
for service in ['rdisc', 'kdump', 'libvirt-guests', 'salt-minion']:
self.run_command(['systemctl', 'disable', '{}.service'.format(service) ], ignore_failure=True)
self.run_command(['systemctl', 'stop', '{}.service'.format(service) ], ignore_failure=True)
def configure_qubes(self):
self.set_stage('Executing qubes configuration')
states = []
if self.system_vms:
states.extend(
('qvm.sys-net', 'qvm.sys-firewall', 'qvm.default-dispvm'))
if self.default_vms:
states.extend(
('qvm.personal', 'qvm.work', 'qvm.untrusted', 'qvm.vault'))
if self.whonix_available and self.whonix_vms:
states.extend(
('qvm.sys-whonix', 'qvm.anon-whonix'))
if self.whonix_default:
states.append('qvm.updates-via-whonix')
if self.usbvm:
states.append('qvm.sys-usb')
if self.usbvm_with_netvm:
states.append('pillar.qvm.sys-net-as-usbvm')
try:
# get rid of initial entries (from package installation time)
os.rename('/var/log/salt/minion', '/var/log/salt/minion.install')
except OSError:
pass
# Refresh minion configuration to make sure all installed formulas are included
self.run_command(['qubesctl', 'saltutil.clear_cache'])
self.run_command(['qubesctl', 'saltutil.sync_all'])
for state in states:
print("Setting up state: {}".format(state))
if state.startswith('pillar.'):
self.run_command(['qubesctl', 'top.enable',
state[len('pillar.'):], 'pillar=True'])
else:
self.run_command(['qubesctl', 'top.enable', state])
try:
self.run_command(['qubesctl', 'state.highstate'])
# After successful call disable all the states to not leave them
# enabled, to not interfere with later user changes (like assigning
# additional PCI devices)
for state in states:
if not state.startswith('pillar.'):
self.run_command(['qubesctl', 'top.disable', state])
except Exception:
raise Exception(
("Qubes initial configuration failed. Login to the system and " +
"check /var/log/salt/minion for details. " +
"You can retry configuration by calling " +
"'sudo qubesctl state.highstate' in dom0 (you will get " +
"detailed state there)."))
def configure_default_template(self):
self.set_stage('Setting default template')
self.run_command(['/usr/bin/qubes-prefs', 'default-template', self.default_template])
def configure_default_dvm(self):
self.set_stage("Creating default DisposableVM")
dispvm_name = self.default_template + '-dvm'
self.run_command(['/usr/bin/qubes-prefs', 'default-dispvm',
dispvm_name])
def configure_network(self):
self.set_stage('Setting up networking')
default_netvm = 'sys-firewall'
updatevm = default_netvm
if self.whonix_default:
updatevm = 'sys-whonix'
self.run_command(['/usr/bin/qvm-prefs', 'sys-firewall', 'netvm', 'sys-net'])
self.run_command(['/usr/bin/qubes-prefs', 'default-netvm', default_netvm])
self.run_command(['/usr/bin/qubes-prefs', 'updatevm', updatevm])
self.run_command(['/usr/bin/qubes-prefs', 'clockvm', 'sys-net'])
self.run_command(['/usr/bin/qvm-start', default_netvm])
def configure_template(self, template, path):
self.set_stage("Configuring TemplateVM {}".format(template))
self.run_command([
'qvm-template-postprocess', '--really', 'post-install', template, path])