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 # if X server is not yet running
#import gui #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 _ = lambda x: x
N_ = lambda x: x N_ = lambda x: x
import distutils.version
import functools
import grp
import logging import logging
import os
import os.path
import pyudev
import subprocess
import threading
import gi import gi
@ -46,10 +38,7 @@ gi.require_version('Gdk', '3.0')
gi.require_version('GLib', '2.0') gi.require_version('GLib', '2.0')
from gi.repository import Gtk 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.categories.system import SystemCategory
from pyanaconda.ui.gui.spokes import NormalSpoke from pyanaconda.ui.gui.spokes import NormalSpoke
from pyanaconda.ui.common import FirstbootOnlySpokeMixIn 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 # export only the spoke, no helper functions, classes or constants
__all__ = ["QubesOsSpoke"] __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): class QubesChoice(object):
instances = [] instances = []
def __init__(self, label, states, depend=None, extra_check=None, def __init__(self, label, depend=None, extra_check=None,
replace=None, indent=False): indent=False):
self.widget = Gtk.CheckButton(label=label) self.widget = Gtk.CheckButton(label=label)
self.states = states
self.depend = depend self.depend = depend
self.extra_check = extra_check self.extra_check = extra_check
self.selected = None self.selected = None
self.replace = replace
self._can_be_sensitive = True self._can_be_sensitive = True
@ -129,6 +80,11 @@ class QubesChoice(object):
if self.selected is not None if self.selected is not None
else self.widget.get_sensitive() and self.widget.get_active()) 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): def store_selected(self):
self.selected = self.get_selected() self.selected = self.get_selected()
@ -146,27 +102,14 @@ class QubesChoice(object):
choice.set_sensitive(not selected and choice.set_sensitive(not selected and
(choice.depend is None or choice.depend.get_selected())) (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): class DisabledChoice(QubesChoice):
def __init__(self, label): def __init__(self, label):
super(DisabledChoice, self).__init__(label, ()) super(DisabledChoice, self).__init__(label)
self.widget.set_sensitive(False) self.widget.set_sensitive(False)
self._can_be_sensitive = False self._can_be_sensitive = False
class QubesOsSpoke(FirstbootOnlySpokeMixIn, NormalSpoke): class QubesOsSpoke(FirstbootOnlySpokeMixIn, NormalSpoke):
""" """
Since this class inherits from the FirstbootOnlySpokeMixIn, it will 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.main_box = self.builder.get_object("mainBox")
self.thread_dialog = None self.thread_dialog = None
self.qubes_user = None self.qubes_data = self.data.addons.org_qubes_os_initial_setup
self.qubes_gid = None
self.default_template = 'fedora-30'
self.set_stage("Start-up")
self.done = False
self.__init_qubes_choices() self.__init_qubes_choices()
def __init_qubes_choices(self): def __init_qubes_choices(self):
self.choice_network = QubesChoice( self.choice_system = QubesChoice(
_('Create default system qubes (sys-net, sys-firewall, default DispVM)'), _('Create default system qubes (sys-net, sys-firewall, default DispVM)'))
('qvm.sys-net', 'qvm.sys-firewall', 'qvm.default-dispvm'))
self.choice_default = QubesChoice( self.choice_default = QubesChoice(
_('Create default application qubes ' _('Create default application qubes '
'(personal, work, untrusted, vault)'), '(personal, work, untrusted, vault)'),
('qvm.personal', 'qvm.work', 'qvm.untrusted', 'qvm.vault'), depend=self.choice_system)
depend=self.choice_network)
if (is_package_installed('qubes-template-whonix-gw*') and if self.qubes_data.whonix_available:
is_package_installed('qubes-template-whonix-ws*')):
self.choice_whonix = QubesChoice( self.choice_whonix = QubesChoice(
_('Create Whonix Gateway and Workstation qubes ' _('Create Whonix Gateway and Workstation qubes '
'(sys-whonix, anon-whonix)'), '(sys-whonix, anon-whonix)'),
('qvm.sys-whonix', 'qvm.anon-whonix'), depend=self.choice_system)
depend=self.choice_network)
else: else:
self.choice_whonix = DisabledChoice(_("Whonix not installed")) self.choice_whonix = DisabledChoice(_("Whonix not installed"))
self.choice_whonix_updates = QubesChoice( self.choice_whonix_updates = QubesChoice(
_('Enable system and template updates over the Tor anonymity ' _('Enable system and template updates over the Tor anonymity '
'network using Whonix'), 'network using Whonix'),
('qvm.updates-via-whonix',),
depend=self.choice_whonix, depend=self.choice_whonix,
indent=True) indent=True)
if not usb_keyboard_present() and not started_from_usb(): if self.qubes_data.usbvm_available:
self.choice_usb = QubesChoice( self.choice_usb = QubesChoice(
_('Create USB qube holding all USB controllers (sys-usb)'), _('Create USB qube holding all USB controllers (sys-usb)'))
('qvm.sys-usb',))
else: else:
self.choice_usb = DisabledChoice( self.choice_usb = DisabledChoice(
_('USB qube configuration disabled - you are using USB ' _('USB qube configuration disabled - you are using USB '
'keyboard or USB disk')) '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"), _("Use sys-net qube for both networking and USB devices"),
('pillar.qvm.sys-net-as-usbvm',),
depend=self.choice_usb, depend=self.choice_usb,
indent=True indent=True
) )
@ -289,7 +220,7 @@ class QubesOsSpoke(FirstbootOnlySpokeMixIn, NormalSpoke):
self.check_advanced.set_active(False) 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) self.choice_default.widget.set_active(True)
if self.choice_whonix.widget.get_sensitive(): if self.choice_whonix.widget.get_sensitive():
self.choice_whonix.widget.set_active(True) self.choice_whonix.widget.set_active(True)
@ -307,6 +238,7 @@ class QubesOsSpoke(FirstbootOnlySpokeMixIn, NormalSpoke):
""" """
NormalSpoke.initialize(self) NormalSpoke.initialize(self)
self.qubes_data.gui_mode = True
def refresh(self): def refresh(self):
""" """
@ -318,8 +250,12 @@ class QubesOsSpoke(FirstbootOnlySpokeMixIn, NormalSpoke):
""" """
# nothing to do here self.choice_system.set_selected(self.qubes_data.system_vms)
pass 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): def apply(self):
""" """
@ -328,8 +264,16 @@ class QubesOsSpoke(FirstbootOnlySpokeMixIn, NormalSpoke):
""" """
# nothing to do here self.qubes_data.skip = self.check_advanced.get_active()
pass
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 @property
def ready(self): def ready(self):
@ -354,7 +298,7 @@ class QubesOsSpoke(FirstbootOnlySpokeMixIn, NormalSpoke):
""" """
return self.done return self.qubes_data.seen
@property @property
def mandatory(self): def mandatory(self):
@ -388,274 +332,4 @@ class QubesOsSpoke(FirstbootOnlySpokeMixIn, NormalSpoke):
the values set in the GUI elements. 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 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])