243 lines
10 KiB
Python
243 lines
10 KiB
Python
|
#
|
||
|
# screen_access.py: anaconda thread management
|
||
|
#
|
||
|
# 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 <http://www.gnu.org/licenses/>.
|
||
|
#
|
||
|
# Author(s): Martin Kolman <mkolman@redhat.com>
|
||
|
#
|
||
|
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_"
|
||
|
|
||
|
import logging
|
||
|
log = logging.getLogger("anaconda")
|
||
|
|
||
|
from configparser import ConfigParser
|
||
|
from threading import RLock
|
||
|
|
||
|
from pyanaconda import startup_utils
|
||
|
from pyanaconda import iutil
|
||
|
from pyanaconda.flags import can_touch_runtime_system
|
||
|
|
||
|
|
||
|
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 can_touch_runtime_system(
|
||
|
msg="write user interaction config file",
|
||
|
touch_live=True):
|
||
|
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 = iutil.getSysroot() + CONFIG_FILE_PATH
|
||
|
|
||
|
with self._lock:
|
||
|
new_config_file = not os.path.exists(config_path)
|
||
|
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)
|
||
|
|
||
|
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 it 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
|
||
|
|
||
|
sam = None
|
||
|
|
||
|
def initSAM():
|
||
|
global sam
|
||
|
sam = ScreenAccessManager()
|