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:
parent
0ec125b967
commit
556b584f74
@ -2,4 +2,4 @@
|
||||
# if X server is not yet running
|
||||
#import gui
|
||||
|
||||
import tui
|
||||
#import tui
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
360
qubes-anaconda-addon/org_qubes_os_initial_setup/ks/qubes.py
Normal file
360
qubes-anaconda-addon/org_qubes_os_initial_setup/ks/qubes.py
Normal 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])
|
||||
|
Loading…
Reference in New Issue
Block a user