diff --git a/anaconda.py b/anaconda.py index 9e95b8af6a3..eaa714992b2 100755 --- a/anaconda.py +++ b/anaconda.py @@ -594,14 +594,6 @@ def _earlyExceptionHandler(ty, value, traceback): threadMgr.add(AnacondaThread(name=constants.THREAD_WAIT_FOR_CONNECTING_NM, target=wait_for_connecting_NM_thread)) - # initialize the screen access manager before launching the UI - from pyanaconda import screen_access - screen_access.initSAM() - # try to open any existing config file - # (might be created by pre-anaconda helper tools, injected during image - # generation, etc.) - screen_access.sam.open_config_file() - # now start the interface display.setup_display(anaconda, opts) if anaconda.gui_startup_failed: diff --git a/data/anaconda.conf b/data/anaconda.conf index 612ae9f844c..2cc02d51c08 100644 --- a/data/anaconda.conf +++ b/data/anaconda.conf @@ -140,6 +140,9 @@ default_help_pages = # Is the partitioning with blivet-gui supported? blivet_gui_supported = True +# A list of spokes to hide in UI. +# FIXME: Use other identification then names of the spokes. +hidden_spokes = [License] # A path to EULA (if any) diff --git a/data/liveinst/liveinst b/data/liveinst/liveinst index 7a632fa3ae0..29fa95dcfc8 100755 --- a/data/liveinst/liveinst +++ b/data/liveinst/liveinst @@ -74,7 +74,7 @@ fi if [ -f /etc/os-release ]; then variantid=$( grep VARIANT_ID /etc/os-release | tail -1 | cut -d= -f2) if [ "$variantid" = "workstation" ]; then - export ANACONDA_PRODUCTVARIANT="Workstation" + export ANACONDA_PRODUCTVARIANT="Workstation Live" fi fi diff --git a/data/product.d/fedora-silverblue.conf b/data/product.d/fedora-silverblue.conf index 15401c2bbc1..c0bf3454ceb 100644 --- a/data/product.d/fedora-silverblue.conf +++ b/data/product.d/fedora-silverblue.conf @@ -6,4 +6,4 @@ variant_name = Silverblue [Base Product] product_name = Fedora -variant_name = Workstation +variant_name = Workstation Live diff --git a/data/product.d/fedora-workstation-live.conf b/data/product.d/fedora-workstation-live.conf new file mode 100644 index 00000000000..bfd7581275b --- /dev/null +++ b/data/product.d/fedora-workstation-live.conf @@ -0,0 +1,15 @@ +# Anaconda configuration file for Fedora Workstation Live. + +[Product] +product_name = Fedora +variant_name = Workstation Live + +[Base Product] +product_name = Fedora +variant_name = Workstation + +[User Interface] +hidden_spokes = + NetworkSpoke + PasswordSpoke + UserSpoke diff --git a/pyanaconda/core/configuration/system.py b/pyanaconda/core/configuration/system.py index 77fae1f9880..3dc008b1c02 100644 --- a/pyanaconda/core/configuration/system.py +++ b/pyanaconda/core/configuration/system.py @@ -167,11 +167,6 @@ def provides_resolver_config(self): """Can we copy /etc/resolv.conf to the target system?""" return self._is_boot_iso - @property - def provides_user_interaction_config(self): - """Can we read /etc/sysconfig/anaconda?""" - return self._is_boot_iso or self._is_live_os - @property def provides_web_browser(self): """Can we redirect users to web pages?""" diff --git a/pyanaconda/core/configuration/ui.py b/pyanaconda/core/configuration/ui.py index 04b2ceff98a..b009d2d877e 100644 --- a/pyanaconda/core/configuration/ui.py +++ b/pyanaconda/core/configuration/ui.py @@ -45,3 +45,11 @@ def default_help_pages(self): def blivet_gui_supported(self): """Is the partitioning with blivet-gui supported?""" return self._get_option("blivet_gui_supported", bool) + + @property + def hidden_spokes(self): + """A list of spokes to hide in UI. + + :return: a list of strings + """ + return self._get_option("hidden_spokes", str).split() diff --git a/pyanaconda/installation.py b/pyanaconda/installation.py index 7bee9f0dead..9ea204fb318 100644 --- a/pyanaconda/installation.py +++ b/pyanaconda/installation.py @@ -40,7 +40,6 @@ from pyanaconda.core import util from pyanaconda import timezone from pyanaconda import network -from pyanaconda import screen_access from pyanaconda.core.i18n import N_ from pyanaconda.threading import threadMgr from pyanaconda.ui.lib.entropy import wait_for_entropy @@ -113,11 +112,6 @@ def doConfiguration(storage, payload, ksdata): task_proxy = SERVICES.get_proxy(dbus_task) os_config.append(Task(task_proxy.Name, sync_run_task, (task_proxy,))) - # The user interaction config file needs to be synchronized with - # current state of the Services module. - os_config.append(Task("Synchronize user interaction config file state", - screen_access.sam.update_config_file_state)) - os_config.append(Task("Configure keyboard", ksdata.keyboard.execute)) os_config.append(Task("Configure timezone", ksdata.timezone.execute)) @@ -212,18 +206,6 @@ def doConfiguration(storage, payload, ksdata): # write anaconda related configs & kickstarts write_configs.append(Task("Store kickstarts", _writeKS, (ksdata,))) - # Write out the user interaction config file. - # - # But make sure it's not written out in the image and directory installation mode, - # as that might result in spokes being inadvertently hidden when the actual installation - # starts from the generate image or directory contents. - if conf.target.is_image: - log.info("Not writing out user interaction config file due to image install mode.") - elif conf.target.is_directory: - log.info("Not writing out user interaction config file due to directory install mode.") - else: - write_configs.append(Task("Store user interaction config", screen_access.sam.write_out_config_file)) - # only add write_configs to the main queue if we actually store some kickstarts/configs if write_configs.task_count: configuration_queue.append(write_configs) diff --git a/pyanaconda/modules/services/installation.py b/pyanaconda/modules/services/installation.py index 953d02fa174..9e08f34736f 100644 --- a/pyanaconda/modules/services/installation.py +++ b/pyanaconda/modules/services/installation.py @@ -16,16 +16,20 @@ # Red Hat, Inc. # import os +from configparser import ConfigParser from pyanaconda.core import util - +from pyanaconda.core.configuration.anaconda import conf from pyanaconda.modules.common.task import Task from pyanaconda.modules.services.constants import SetupOnBootAction +from pyanaconda.startup_utils import get_anaconda_version_string from pyanaconda.anaconda_loggers import get_module_logger log = get_module_logger(__name__) -__all__ = ["ConfigureInitialSetupTask", "ConfigureServicesTask"] + +__all__ = ["ConfigureInitialSetupTask", "ConfigurePostInstallationToolsTask", + "ConfigureServicesTask"] class ConfigureInitialSetupTask(Task): @@ -37,7 +41,7 @@ def __init__(self, sysroot, setup_on_boot): """Create a new Initial Setup configuration task. :param str sysroot: a path to the root of the target system - :param enum setup_on_boot: setup-on-boot mode for Initial Setup + :param Enum setup_on_boot: setup-on-boot mode for Initial Setup Modes are defined by the SetupOnBoot enum as distinct integers. @@ -90,6 +94,57 @@ def run(self): self._disable_service() +class ConfigurePostInstallationToolsTask(Task): + """Installation task for configuration of post-installation tools.""" + + def __init__(self, sysroot, tools_enabled): + """Create a new task. + + :param str sysroot: a path to the root of the target system + :param bool tools_enabled: are the post-installation tools enabled? + """ + super().__init__() + self._sysroot = sysroot + self._tools_enabled = tools_enabled + + @property + def name(self): + return "Configure post-installation tools" + + def run(self): + """Run the task. """ + if conf.target.is_image: + log.info("Not writing out user interaction config file due to image install mode.") + elif conf.target.is_directory: + log.info("Not writing out user interaction config file due to directory install mode.") + else: + parser = self._get_config() + self._write_config(parser) + + def _get_config(self): + """Generate the user interaction configuration.""" + parser = ConfigParser(delimiters=("=", ), comment_prefixes=("#", )) + parser.add_section("General") + parser["General"]["post_install_tools_disabled"] = "1" if not self._tools_enabled else "0" + return parser + + def _write_config(self, parser): + """Write the user interaction config file.""" + path = os.path.join(self._sysroot, "etc/sysconfig/anaconda") + log.info("Writing out user interaction config at %s", path) + + try: + with open(path, "wt") as f: + f.write( + "# This file has been generated by the Anaconda Installer " + "{}\n\n".format(get_anaconda_version_string()) + ) + + parser.write(f) + except OSError: + log.exception("Can't write user interaction config file.") + + class ConfigureServicesTask(Task): """Installation task for service configuration. diff --git a/pyanaconda/modules/services/services.py b/pyanaconda/modules/services/services.py index 630ec398cdc..b15a802e55d 100644 --- a/pyanaconda/modules/services/services.py +++ b/pyanaconda/modules/services/services.py @@ -28,8 +28,7 @@ from pyanaconda.modules.services.kickstart import ServicesKickstartSpecification from pyanaconda.modules.services.services_interface import ServicesInterface from pyanaconda.modules.services.installation import ConfigureInitialSetupTask, \ - ConfigureServicesTask - + ConfigureServicesTask, ConfigurePostInstallationToolsTask from pyanaconda.anaconda_loggers import get_module_logger log = get_module_logger(__name__) @@ -241,13 +240,19 @@ def install_with_tasks(self, sysroot): :returns: list of object paths of installation tasks """ tasks = [ - ConfigureInitialSetupTask(sysroot=sysroot, setup_on_boot=self.setup_on_boot), + ConfigureInitialSetupTask( + sysroot=sysroot, + setup_on_boot=self.setup_on_boot + ), + ConfigurePostInstallationToolsTask( + sysroot=sysroot, + tools_enabled=self.post_install_tools_enabled + ), ConfigureServicesTask( sysroot=sysroot, disabled_services=self.disabled_services, enabled_services=self.enabled_services ), - ] paths = [ diff --git a/pyanaconda/screen_access.py b/pyanaconda/screen_access.py deleted file mode 100644 index de7ff4dc3a8..00000000000 --- a/pyanaconda/screen_access.py +++ /dev/null @@ -1,257 +0,0 @@ -# -# screen_access.py: screen access management for the Anaconda UI -# -# Copyright (C) 2016 -# Red Hat, Inc. All rights reserved. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# Author(s): Martin Kolman -# -import os -CONFIG_FILE_NAME = "anaconda" -CONFIG_FILE_PATH = os.path.join("/etc/sysconfig", CONFIG_FILE_NAME) -CONFIG_TRUE = "1" -CONFIG_FALSE = "0" -CONFIG_BOOLEAN_TABLE = { - CONFIG_TRUE: True, - CONFIG_FALSE: False -} -CONFIG_VISITED_KEY = "visited" -CONFIG_GENERAL_SECTION = "General" -CONFIG_DISABLE_POSTINST_TOOLS_KEY = "post_install_tools_disabled" -CONFIG_OPTION_PREFIX = "changed_" - -from pyanaconda.anaconda_loggers import get_module_logger -log = get_module_logger(__name__) - -from configparser import ConfigParser -from threading import RLock - -from pyanaconda import startup_utils -from pyanaconda.core import util -from pyanaconda.core.configuration.anaconda import conf -from pyanaconda.modules.common.constants.services import SERVICES - - -class ScreenAccessManager(object): - """A singleton that takes care about spoke visits and option changes. - - The visits and option changes are reflected in the user interaction config file. - For specification of the user interaction config file see the - user-interaction-config-file-spec.rst file in the docs directory of the - Anaconda source code checkout. - """ - - def __init__(self): - self._lock = RLock() - self._config = ConfigParser(delimiters=("="), comment_prefixes=("#")) - - def open_config_file(self, config_path=None): - """Try to open an existing config file.""" - with self._lock: - # Don't load the user interaction config from - # default path if no path is specified in image or - # directory installation modes. - # The config would be taken from the host system, - # which is certainly not what we would want to happen. - # But load the config if a path is specified, - # so that it is possible to hide spokes in - # image and directory installation modes. - if config_path is None and conf.system.provides_user_interaction_config: - config_path = CONFIG_FILE_PATH - if config_path and os.path.exists(config_path): - log.info("parsing existing user interaction config file in %s", config_path) - with open(config_path, "rt") as f: - self._config.read_file(f) - - def write_out_config_file(self, config_path=None): - """Write the user interaction config file to persistent storage. - - we always read the config file from the top level filesystem, as: - -> on live media the file will be there - -> on non-live media the config file (if any) will be injected to the top level - -> filesystem by image generation tools or by an updates/product image - - - on the other hand the "output" config file needs to always end on the installed - system, so that post installation setup tools (such as Initial Setup or - Gnome Initial Setup) can use it - -> therefore we always write the config file to the sysroot path - """ - - if config_path is None: - config_path = util.getSysroot() + CONFIG_FILE_PATH - - with self._lock: - new_config_file = not os.path.exists(config_path) - try: - with open(config_path, "wt") as f: - if new_config_file: - # we are creating a new file, so add a header that it was created by Anaconda, - # including its version number - f.write(self._get_new_config_header()) - self._config.write(f) - except OSError: - log.exception("Can't write user interaction config file.") - - def _get_new_config_header(self): - """Generate a header for use when Anaconda generates the user interaction config file. - - :returns: an appropriate user config file header - :rtype: str - """ - - return "# This file has been generated by the Anaconda Installer %s\n\n" % \ - startup_utils.get_anaconda_version_string() - - def _parse_bool_option(self, section_name, option_name): - """Read a boolean option from the user config file. - - Make sure to convert the value used for boolean values (1 & 0) to Python booleans (True & False) - and throw SyntaxError if an invalid value is specified and return None if the option has not been found. - - :param str section_name: section where to look for the option - :param str option_name: name of the option to check - :returns: True/False for 1/0 and None id the option has not been found in the section - :rtype: bool or None - """ - - option_value = self._config.get(section_name, option_name, fallback=None) - if option_value is None: - return option_value - # the option has some content - option_boolean = CONFIG_BOOLEAN_TABLE.get(option_value) - if option_boolean is None: - # parsing failed for the option - raise SyntaxError - else: - return option_boolean - - @property - def post_install_tools_disabled(self): - """Report if post install tools should be marked as disabled in the user interaction config file.""" - - with self._lock: - if self._config.has_section(CONFIG_GENERAL_SECTION): - try: - # convert to bool in case the option is not set (the call returns None) - return bool(self._parse_bool_option(CONFIG_GENERAL_SECTION, CONFIG_DISABLE_POSTINST_TOOLS_KEY)) - except SyntaxError: - log.error("Can't check if post install tools have been disabled due to config file syntax error.") - else: - return False - - @post_install_tools_disabled.setter - def post_install_tools_disabled(self, tools_disabled): - """Controls if post install tools should be marked as disabled in the user interaction config file.""" - - with self._lock: - if not self._config.has_section(CONFIG_GENERAL_SECTION): - self._config.add_section(CONFIG_GENERAL_SECTION) - if tools_disabled: - self._config.set(CONFIG_GENERAL_SECTION, CONFIG_DISABLE_POSTINST_TOOLS_KEY, CONFIG_TRUE) - else: - self._config.set(CONFIG_GENERAL_SECTION, CONFIG_DISABLE_POSTINST_TOOLS_KEY, CONFIG_FALSE) - - def mark_screen_visited(self, screen_name): - """Mark the given screen as visited - - :param str screen_name: screen to mark as visited - """ - - with self._lock: - if not self._config.has_section(screen_name): - self._config.add_section(screen_name) - self._config.set(screen_name, CONFIG_VISITED_KEY, CONFIG_TRUE) - - def get_screen_visited(self, screen_name): - """Report if the given screen has been marked as visited - - Note that this takes into account both this installation run - and any pre-installation tools run recorded in the user interaction config file. - - :param str screen_name: name of a screen to check - :returns: True if the screen has already been visited, False otherwise - :rtype: bool - """ - - with self._lock: - if self._config.has_section(screen_name): - try: - # convert to bool in case the option is not set (the call returns None) - return bool(self._parse_bool_option(screen_name, CONFIG_VISITED_KEY)) - except SyntaxError: - log.error("Can't check if %s screen has been visited due to config file syntax error.", - screen_name) - return False - - def mark_screen_option_changed(self, screen_name, option_name): - """Mark option on a screen as changed by the user. - - Note that to mark an option as changed the screen needs to be marked - as visited (section needs to exist and have the visited key set to 1). - - :param str screen_name: name of the screen - :param str option_name: name of the option to mark as changed - """ - - with self._lock: - if self._config.has_section(screen_name): - if self.get_screen_visited(screen_name): - self._config.set(screen_name, CONFIG_OPTION_PREFIX + option_name, CONFIG_TRUE) - log.warning("Attempt to set option %s as changed for screen %s that has not been visited.", - CONFIG_OPTION_PREFIX + option_name, screen_name) - - else: - log.warning("Attempt to set option %s as changed for an unknown screen %s.", - CONFIG_OPTION_PREFIX + option_name, screen_name) - - def get_screen_option_changed(self, screen_name, option_name): - """Report if the given option on the specified screen has been changed by the user. - - If the screen or option is unknown or if its syntax is incorrect then the option - is reported as not changed. - - :param str screen_name: name of the screen - :param str option_name: name of the option to check - :returns: True if the option was changed, False otherwise - :rtype: bool - """ - - with self._lock: - try: - if self._config.has_section(screen_name): - return bool(self._parse_bool_option(screen_name, CONFIG_OPTION_PREFIX + option_name)) - except SyntaxError: - log.error("Can't check if options %s in section %s has been changed due to config file syntax error.", - option_name, screen_name) - return False - - def update_config_file_state(self): - """Synchornize user interaction config file state. - - At the moment this means simply checking if post installation setup tools should - be enabled/disabled based on kickstart and state of the Services module. - - It is assumed this method will be called at an appropriate time before the config - file is written out to the target system. - """ - services_proxy = SERVICES.get_proxy() - if not services_proxy.PostInstallToolsEnabled: - self.post_install_tools_disabled = True - -sam = None - -def initSAM(): - global sam - sam = ScreenAccessManager() diff --git a/pyanaconda/ui/common.py b/pyanaconda/ui/common.py index 3194cdae4c5..c3d9158f920 100644 --- a/pyanaconda/ui/common.py +++ b/pyanaconda/ui/common.py @@ -19,9 +19,9 @@ from abc import ABCMeta, abstractproperty +from pyanaconda.core.configuration.anaconda import conf from pyanaconda.core.constants import ANACONDA_ENVIRON, FIRSTBOOT_ENVIRON, SETUP_ON_BOOT_RECONFIG from pyanaconda.modules.common.constants.services import SERVICES -from pyanaconda import screen_access from pyanaconda.core.util import collect from pyanaconda.core.signal import Signal from pyanaconda import lifecycle @@ -222,7 +222,6 @@ def __init__(self, storage, payload): # connect default callbacks for the signals self.entered.connect(self.entry_logger) - self.entered.connect(self._mark_screen_visited) self.exited.connect(self.exit_logger) @abstractproperty @@ -327,11 +326,6 @@ def status(self): """ raise NotImplementedError - - def _mark_screen_visited(self, spoke_instance): - """Report the spoke screen as visited to the Spoke Access Manager.""" - screen_access.sam.mark_screen_visited(spoke_instance.__class__.__name__) - def entry_logger(self, spoke_instance): """Log immediately before this spoke is about to be displayed on the screen. Subclasses may override this method if they want to log @@ -624,10 +618,11 @@ def collect_spokes(mask_paths, category): # filter out any spokes from the candidates that have already been visited by the user before # (eq. before Anaconda or Initial Setup started) and should not be visible again visible_spokes = [] + hidden_spokes = conf.ui.hidden_spokes for candidate in candidate_spokes: - if screen_access.sam.get_screen_visited(candidate.__name__): - log.info("Spoke %s will not be displayed because it has already been visited before.", - candidate.__name__) + if candidate.__name__ in hidden_spokes: + log.info("Spoke %s will not be displayed because it is hidden by " + "the Anaconda configuration file.", candidate.__name__) else: visible_spokes.append(candidate) spokes.extend(visible_spokes) diff --git a/pyanaconda/ui/gui/spokes/storage.py b/pyanaconda/ui/gui/spokes/storage.py index a34ea90d114..184171b7b53 100644 --- a/pyanaconda/ui/gui/spokes/storage.py +++ b/pyanaconda/ui/gui/spokes/storage.py @@ -78,7 +78,6 @@ reset_bootloader from pyanaconda.storage.snapshot import on_disk_storage from pyanaconda.storage.format_dasd import DasdFormatting -from pyanaconda.screen_access import sam from pyanaconda.modules.common.errors.configuration import StorageConfigurationError, \ BootloaderConfigurationError from pyanaconda.modules.common.constants.objects import DISK_SELECTION, DISK_INITIALIZATION, \ @@ -333,7 +332,7 @@ def __init__(self, *args, **kwargs): # hide radio buttons for spokes that have been marked as visited by the # user interaction config file - if sam.get_screen_visited("CustomPartitioningSpoke"): + if "CustomPartitioningSpoke" in conf.ui.hidden_spokes: self._custom_part_radio_button.set_visible(False) self._custom_part_radio_button.set_no_show_all(True) @@ -343,7 +342,7 @@ def __init__(self, *args, **kwargs): def _enable_blivet_gui(self, supported): if supported: self._blivet_gui_radio_button.connect("toggled", self._method_radio_button_toggled) - if sam.get_screen_visited("BlivetGuiSpoke"): + if "BlivetGuiSpoke" in conf.ui.hidden_spokes: self._blivet_gui_radio_button.set_visible(False) self._blivet_gui_radio_button.set_no_show_all(True) else: diff --git a/tests/nosetests/pyanaconda_tests/module_services_test.py b/tests/nosetests/pyanaconda_tests/module_services_test.py index 0d7523e2482..b60eef61b34 100644 --- a/tests/nosetests/pyanaconda_tests/module_services_test.py +++ b/tests/nosetests/pyanaconda_tests/module_services_test.py @@ -17,12 +17,18 @@ # # Red Hat Author(s): Vendula Poncova # +import os +import tempfile import unittest +from textwrap import dedent +from unittest.mock import patch + from mock import Mock from pyanaconda.core.constants import GRAPHICAL_TARGET, SETUP_ON_BOOT_DISABLED, \ SETUP_ON_BOOT_RECONFIG, SETUP_ON_BOOT_ENABLED, SETUP_ON_BOOT_DEFAULT from pyanaconda.modules.common.constants.services import SERVICES +from pyanaconda.modules.services.installation import ConfigurePostInstallationToolsTask from pyanaconda.modules.services.services import ServicesModule from pyanaconda.modules.services.services_interface import ServicesInterface from tests.nosetests.pyanaconda_tests import check_kickstart_interface @@ -194,3 +200,75 @@ def firstboot_reconfig_kickstart_test(self): self._test_kickstart(ks_in, ks_out) self.assertEqual(self.services_interface.SetupOnBoot, SETUP_ON_BOOT_RECONFIG) self.assertEqual(self.services_interface.PostInstallToolsEnabled, True) + + +class ServicesTasksTestCase(unittest.TestCase): + """Test the services tasks.""" + + @patch('pyanaconda.modules.services.installation.get_anaconda_version_string') + def enable_post_install_tools_test(self, version_getter): + version_getter.return_value = "1.0" + + content = dedent(""" + # This file has been generated by the Anaconda Installer 1.0 + + [General] + post_install_tools_disabled = 0 + """) + + with tempfile.TemporaryDirectory() as sysroot: + os.makedirs(os.path.join(sysroot, "etc/sysconfig")) + + ConfigurePostInstallationToolsTask( + sysroot=sysroot, + tools_enabled=True + ).run() + + with open(os.path.join(sysroot, "etc/sysconfig/anaconda")) as f: + self.assertEqual(f.read().strip(), content.strip()) + + @patch('pyanaconda.modules.services.installation.get_anaconda_version_string') + def disable_post_install_tools_test(self, version_getter): + version_getter.return_value = "1.0" + + content = dedent(""" + # This file has been generated by the Anaconda Installer 1.0 + + [General] + post_install_tools_disabled = 1 + """) + + print(content) + + with tempfile.TemporaryDirectory() as sysroot: + os.makedirs(os.path.join(sysroot, "etc/sysconfig")) + + ConfigurePostInstallationToolsTask( + sysroot=sysroot, + tools_enabled=False + ).run() + + with open(os.path.join(sysroot, "etc/sysconfig/anaconda")) as f: + self.assertEqual(f.read().strip(), content.strip()) + + @patch('pyanaconda.modules.services.installation.conf') + def skip_post_install_tools_test(self, conf): + with tempfile.TemporaryDirectory() as sysroot: + os.makedirs(os.path.join(sysroot, "etc/sysconfig")) + + task = ConfigurePostInstallationToolsTask( + sysroot=sysroot, + tools_enabled=True + ) + + conf.target.is_directory = False + conf.target.is_image = True + task.run() + + self.assertFalse(os.path.isfile(os.path.join(sysroot, "etc/sysconfig/anaconda"))) + + conf.target.is_directory = True + conf.target.is_image = False + task.run() + + self.assertFalse(os.path.isfile(os.path.join(sysroot, "etc/sysconfig/anaconda"))) diff --git a/tests/nosetests/pyanaconda_tests/product_test.py b/tests/nosetests/pyanaconda_tests/product_test.py index 57f1c80ae50..00743ee45d5 100644 --- a/tests/nosetests/pyanaconda_tests/product_test.py +++ b/tests/nosetests/pyanaconda_tests/product_test.py @@ -81,6 +81,10 @@ def fedora_products_test(self): "Fedora", "Workstation", ["fedora.conf", "fedora-workstation.conf"] ) + self._check_default_product( + "Fedora", "Workstation Live", + ["fedora.conf", "fedora-workstation.conf", "fedora-workstation-live.conf"] + ) self._check_default_product( "Fedora", "AtomicHost", ["fedora.conf", "fedora-atomic-host.conf"] @@ -88,7 +92,8 @@ def fedora_products_test(self): ) self._check_default_product( "Fedora", "Silverblue", - ["fedora.conf", "fedora-workstation.conf", "fedora-silverblue.conf"] + ["fedora.conf", "fedora-workstation.conf", "fedora-workstation-live.conf", + "fedora-silverblue.conf"] ) def rhel_products_test(self): diff --git a/tests/nosetests/pyanaconda_tests/screen_access_manager_test.py b/tests/nosetests/pyanaconda_tests/screen_access_manager_test.py deleted file mode 100644 index e52d79e4d76..00000000000 --- a/tests/nosetests/pyanaconda_tests/screen_access_manager_test.py +++ /dev/null @@ -1,253 +0,0 @@ -# vim:set fileencoding=utf-8 -# -# Copyright (C) 2016 Red Hat, Inc. -# -# This copyrighted material is made available to anyone wishing to use, -# modify, copy, or redistribute it subject to the terms and conditions of -# the GNU General Public License v.2, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY expressed or implied, including the implied warranties of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -# Public License for more details. You should have received a copy of the -# GNU General Public License along with this program; if not, write to the -# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the -# source code or documentation are not subject to the GNU General Public -# License and may only be used or replicated with the express permission of -# Red Hat, Inc. -# -# Red Hat Author(s): Martin Kolman -# - -import unittest -import tempfile -import shutil -import os -from configparser import ConfigParser - -from pyanaconda import screen_access - -class ScreenAccessManagerTest(unittest.TestCase): - """Test the spoke access manager. - - Check that it can correctly handle existing user interaction config files, - correctly handle spoke visits and option changes, state querying and - that the config files it creates are valid. - """ - - def _get_config_path(self): - test_dir = tempfile.mkdtemp() - config_path = os.path.join(test_dir, screen_access.CONFIG_FILE_NAME) - return test_dir, config_path - - def _parse_config_file(self, config_path): - config = ConfigParser() - with open(config_path, "rt") as f: - config.read_file(f) - return config - - def file_header_test(self): - """Test that the Anaconda generated config file header looks fine.""" - screen_access.initSAM() - sam = screen_access.sam - header = sam._get_new_config_header() - # check that the header starts with a hash - self.assertEqual(header[0], "#") - # check that it has a newline at the end - self.assertEqual(header[-1], "\n") - - def no_interaction_test(self): - """Test that SAM can handle no user interaction taking place.""" - screen_access.initSAM() - sam = screen_access.sam - - try: - test_dir, config_path = self._get_config_path() - sam.write_out_config_file(os.path.join(test_dir, screen_access.CONFIG_FILE_NAME)) - self.assertTrue(os.path.exists(config_path)) - # the config should have the "created by Anaconda" header, so it should - # be non-zero - self.assertGreater(os.path.getsize(config_path), 0) - finally: - shutil.rmtree(test_dir) - - def spoke_visited_test(self): - """Test that SAM correctly marks a spoke as visited in the output config.""" - - screen_access.initSAM() - sam = screen_access.sam - try: - test_dir, config_path = self._get_config_path() - # mark a screen as visited - sam.mark_screen_visited("FooSpoke") - # it should be safe to mark a screen as visited multiple times - sam.mark_screen_visited("FooSpoke") - # write the config file - sam.write_out_config_file(os.path.join(test_dir, screen_access.CONFIG_FILE_NAME)) - # the resulting config file needs to exist and be non-empty - self.assertTrue(os.path.exists(config_path)) - self.assertGreater(os.path.getsize(config_path), 0) - # parse the file and check if it contains the FooSpoke section - # and visited key set to 0 - - config = self._parse_config_file(config_path) - self.assertTrue(config.has_section("FooSpoke")) - self.assertEqual(config.get("FooSpoke", screen_access.CONFIG_VISITED_KEY), screen_access.CONFIG_TRUE) - finally: - shutil.rmtree(test_dir) - - def spoke_option_changed_test(self): - """Test that SAM correctly marks option as changed in the output config.""" - screen_access.initSAM() - sam = screen_access.sam - try: - test_dir, config_path = self._get_config_path() - # mark a screen as visited - sam.mark_screen_visited("FooSpoke") - # mark some options on it as changed - sam.mark_screen_option_changed("FooSpoke", "BarOption") - sam.mark_screen_option_changed("FooSpoke", "BazOption") - # it should be possible to mark an option as changed multiple - # times so that Anaconda doesn't have to check before marking - # an option as changed - sam.mark_screen_option_changed("FooSpoke", "BarOption") - sam.mark_screen_option_changed("FooSpoke", "BazOption") - # check if SAM correctly reports the options as changed - self.assertTrue(sam.get_screen_option_changed("FooSpoke", "BarOption")) - self.assertTrue(sam.get_screen_option_changed("FooSpoke", "BazOption")) - - # try marking something as changed on a spoke - # that has not been visited - sam.mark_screen_option_changed("UnvisitedSpoke", "UnaccessibleOption") - # the option should still be marked as not changed - self.assertFalse(sam.get_screen_option_changed("UnvisitedSpoke", "UnaccessibleOption")) - - # now write out the config and check if it is valid - sam.write_out_config_file(os.path.join(test_dir, screen_access.CONFIG_FILE_NAME)) - - config = self._parse_config_file(config_path) - self.assertTrue(config.has_section("FooSpoke")) - self.assertEqual(config.get("FooSpoke", screen_access.CONFIG_VISITED_KEY), screen_access.CONFIG_TRUE) - self.assertEqual(config.get("FooSpoke", screen_access.CONFIG_OPTION_PREFIX + "BarOption"), screen_access.CONFIG_TRUE) - self.assertEqual(config.get("FooSpoke", screen_access.CONFIG_OPTION_PREFIX + "BazOption"), screen_access.CONFIG_TRUE) - # make sure there is no "UnvisitedSpoke" in the config - self.assertFalse(config.has_section("UnvisitedSpoke")) - self.assertEqual(config.get("UnivisitedSpoke", screen_access.CONFIG_OPTION_PREFIX + "UnaccessibleOption", fallback=None), None) - finally: - shutil.rmtree(test_dir) - - def disale_post_install_tools_test(self): - """Test that SAM can correctly disable post install tools via the user interaction config.""" - screen_access.initSAM() - sam = screen_access.sam - - try: - test_dir, config_path = self._get_config_path() - # post install tools should be enabled by default - self.assertFalse(sam.post_install_tools_disabled) - # set post install tools to disabled - sam.post_install_tools_disabled = True - # check if SAM correctly reports the change - self.assertTrue(sam.post_install_tools_disabled) - sam.write_out_config_file(os.path.join(test_dir, screen_access.CONFIG_FILE_NAME)) - # check if the option is correctly reflected in the - # user interaction config file - config = self._parse_config_file(config_path) - self.assertTrue(config.has_section(screen_access.CONFIG_GENERAL_SECTION)) - self.assertEqual(config.get(screen_access.CONFIG_GENERAL_SECTION, screen_access.CONFIG_DISABLE_POSTINST_TOOLS_KEY), - screen_access.CONFIG_TRUE) - finally: - shutil.rmtree(test_dir) - - def existing_config_test(self): - """Test that SAM can correctly parse an existing user interaction config.""" - screen_access.initSAM() - sam = screen_access.sam - try: - test_dir, config_path = self._get_config_path() - third_party_config = """ -# Generated by some third party tool - -[General] -# disable post install tools -post_install_tools_disabled=1 - -[FooScreen] -visited=1 - -[BarScreen] -visited=1 -# some options have been changed -changed_some_option=1 -changed_other_option=1 -# these options use wrong boolean syntax -# (only 1 and 0 is allowed by the spec) -changed_bad_syntax_1=yes -changed_bad_syntax_2=true -changed_bad_syntax_3=True -changed_bad_syntax_4=no -changed_bad_syntax_5=false -changed_bad_syntax_6=False -changed_bad_syntax_7=2 -changed_bad_syntax_8=something - -# a random keys that should be ignored -not_an_option=1 -not_changed=abc -""" - with open(config_path, "wt") as f: - f.write(third_party_config) - - sam.open_config_file(config_path) - # check that SAM has correctly parsed the config file - self.assertTrue(sam.post_install_tools_disabled) - self.assertTrue(sam.get_screen_visited("FooScreen")) - self.assertTrue(sam.get_screen_visited("BarScreen")) - self.assertTrue(sam.get_screen_option_changed("BarScreen", "some_option")) - self.assertTrue(sam.get_screen_option_changed("BarScreen", "other_option")) - # options using bad boolean syntax should be ignored - self.assertFalse(sam.get_screen_option_changed("BarScreen", "bad_syntax_1")) - self.assertFalse(sam.get_screen_option_changed("BarScreen", "bad_syntax_2")) - self.assertFalse(sam.get_screen_option_changed("BarScreen", "bad_syntax_3")) - self.assertFalse(sam.get_screen_option_changed("BarScreen", "bad_syntax_4")) - self.assertFalse(sam.get_screen_option_changed("BarScreen", "bad_syntax_5")) - self.assertFalse(sam.get_screen_option_changed("BarScreen", "bad_syntax_6")) - self.assertFalse(sam.get_screen_option_changed("BarScreen", "bad_syntax_7")) - self.assertFalse(sam.get_screen_option_changed("BarScreen", "bad_syntax_8")) - # those other two keys don̈́'t have the changed prefix, so should not be parsed - # as changed options - self.assertFalse(sam.get_screen_option_changed("BarScreen", "not_an_option")) - self.assertFalse(sam.get_screen_option_changed("BarScreen", "not_changed")) - - # now try changing more stuff and then check if it correctly propagates - # to the resulting user interaction config file - - # lets say the user changed something else on the BarScreen - sam.mark_screen_option_changed("BarScreen", "option_changed_in_anaconda") - # and then visited the BazScreen and also changed something on it - sam.mark_screen_visited("BazScreen") - sam.mark_screen_option_changed("BazScreen", "baz_option_1") - sam.mark_screen_option_changed("BazScreen", "baz_option_2") - - # write out the config file and check if it is valid - sam.write_out_config_file(os.path.join(test_dir, screen_access.CONFIG_FILE_NAME)) - config = self._parse_config_file(config_path) - - self.assertTrue(config.has_section(screen_access.CONFIG_GENERAL_SECTION)) - self.assertTrue(config.get(screen_access.CONFIG_GENERAL_SECTION, - screen_access.CONFIG_DISABLE_POSTINST_TOOLS_KEY), - screen_access.CONFIG_TRUE) - - self.assertTrue(config.has_section("FooScreen")) - self.assertEqual(config.get("FooScreen", screen_access.CONFIG_VISITED_KEY), screen_access.CONFIG_TRUE) - - self.assertTrue(config.has_section("BarScreen")) - self.assertEqual(config.get("BarScreen", screen_access.CONFIG_OPTION_PREFIX + "some_option"), screen_access.CONFIG_TRUE) - self.assertEqual(config.get("BarScreen", screen_access.CONFIG_OPTION_PREFIX + "other_option"), screen_access.CONFIG_TRUE) - self.assertEqual(config.get("BarScreen", screen_access.CONFIG_OPTION_PREFIX + "option_changed_in_anaconda"), screen_access.CONFIG_TRUE) - - self.assertTrue(config.has_section("BazScreen")) - self.assertEqual(config.get("BazScreen", screen_access.CONFIG_OPTION_PREFIX + "baz_option_1"), screen_access.CONFIG_TRUE) - self.assertEqual(config.get("BazScreen", screen_access.CONFIG_OPTION_PREFIX + "baz_option_2"), screen_access.CONFIG_TRUE) - finally: - shutil.rmtree(test_dir)