268 lines
9.2 KiB
Python
268 lines
9.2 KiB
Python
|
#
|
||
|
# Copyright (C) 2018 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.
|
||
|
#
|
||
|
# Author(s): Vendula Poncova <vponcova@redhat.com>
|
||
|
#
|
||
|
import os
|
||
|
|
||
|
from pyanaconda.anaconda_loggers import get_module_logger
|
||
|
from pyanaconda.core.configuration.base import create_parser, read_config, get_option, \
|
||
|
ConfigurationError
|
||
|
|
||
|
log = get_module_logger(__name__)
|
||
|
|
||
|
|
||
|
__all__ = ["ProfileLoader"]
|
||
|
|
||
|
|
||
|
class ProfileData(object):
|
||
|
"""A class that represents a profile."""
|
||
|
|
||
|
def __init__(self):
|
||
|
"""Create the profile data."""
|
||
|
self.config_path = ""
|
||
|
self.profile_id = ""
|
||
|
self.base_profile = ""
|
||
|
self.os_id = ""
|
||
|
self.variant_id = ""
|
||
|
|
||
|
def load_from_file(self, config_path):
|
||
|
"""Load information about a profile from the given configuration file.
|
||
|
|
||
|
:param config_path: a path to a configuration file
|
||
|
:raises: ConfigurationError if a profile cannot be loaded
|
||
|
"""
|
||
|
# Set up the parser.
|
||
|
parser = create_parser()
|
||
|
self._create_profile_section(parser)
|
||
|
self._create_profile_detection_section(parser)
|
||
|
|
||
|
# Read the profile sections.
|
||
|
self.config_path = config_path
|
||
|
read_config(parser, config_path)
|
||
|
|
||
|
self._read_profile_section(parser)
|
||
|
self._read_profile_detection_section(parser)
|
||
|
|
||
|
if not self.profile_id:
|
||
|
raise ConfigurationError("The profile id is not specified!")
|
||
|
|
||
|
def _create_profile_section(self, parser):
|
||
|
"""Create the [Profile] section.
|
||
|
|
||
|
:param parser: a configuration parser
|
||
|
"""
|
||
|
section_name = "Profile"
|
||
|
parser.add_section(section_name)
|
||
|
parser.set(section_name, "profile_id", "")
|
||
|
parser.set(section_name, "base_profile", "")
|
||
|
|
||
|
def _read_profile_section(self, parser):
|
||
|
"""Read the [Profile] section.
|
||
|
|
||
|
:param parser: a configuration parser
|
||
|
"""
|
||
|
section_name = "Profile"
|
||
|
self.profile_id = get_option(parser, section_name, "profile_id")
|
||
|
self.base_profile = get_option(parser, section_name, "base_profile")
|
||
|
|
||
|
def _create_profile_detection_section(self, parser):
|
||
|
"""Create the [Profile Detection] section.
|
||
|
|
||
|
:param parser: a configuration parser
|
||
|
"""
|
||
|
section_name = "Profile Detection"
|
||
|
parser.add_section(section_name)
|
||
|
parser.set(section_name, "os_id", "")
|
||
|
parser.set(section_name, "variant_id", "")
|
||
|
|
||
|
def _read_profile_detection_section(self, parser):
|
||
|
"""Read the [Profile Detection] section.
|
||
|
|
||
|
:param parser: a configuration parser
|
||
|
"""
|
||
|
section_name = "Profile Detection"
|
||
|
self.os_id = get_option(parser, section_name, "os_id")
|
||
|
self.variant_id = get_option(parser, section_name, "variant_id")
|
||
|
|
||
|
|
||
|
class ProfileLoader(object):
|
||
|
"""A class for loading information about profiles from configuration files."""
|
||
|
|
||
|
def __init__(self):
|
||
|
"""Create a new loader."""
|
||
|
self._profiles = {}
|
||
|
|
||
|
def load_profiles(self, config_dir):
|
||
|
"""Load information about profiles from the given configuration directory.
|
||
|
|
||
|
Invalid configuration files will be skipped.
|
||
|
|
||
|
:param config_dir: a path to a directory
|
||
|
"""
|
||
|
log.info("Loading information about profiles from %s.", config_dir)
|
||
|
|
||
|
for file_name in sorted(os.listdir(config_dir)):
|
||
|
if not file_name.endswith(".conf"):
|
||
|
continue
|
||
|
|
||
|
config_path = os.path.join(config_dir, file_name)
|
||
|
|
||
|
try:
|
||
|
self.load_profile(config_path)
|
||
|
except ConfigurationError as e:
|
||
|
log.error("Skipping an invalid configuration at %s: %s", config_path, e)
|
||
|
|
||
|
def load_profile(self, config_path):
|
||
|
"""Load information about a profile from the given configuration file.
|
||
|
|
||
|
:param config_path: a path to a configuration file
|
||
|
:raises: ConfigurationError if a profile cannot be loaded
|
||
|
"""
|
||
|
data = ProfileData()
|
||
|
|
||
|
# Load the profile.
|
||
|
data.load_from_file(config_path)
|
||
|
profile_id = data.profile_id
|
||
|
|
||
|
if profile_id in self._profiles:
|
||
|
raise ConfigurationError("The '{}' profile was already loaded.".format(profile_id))
|
||
|
|
||
|
# Add the profile.
|
||
|
log.info("Found the '%s' profile at %s.", profile_id, config_path)
|
||
|
self._profiles[profile_id] = data
|
||
|
|
||
|
def check_profile(self, profile_id):
|
||
|
"""Check if the specified profile is supported.
|
||
|
|
||
|
:param profile_id: an id of the profile
|
||
|
:return: True if the profile is supported, otherwise False
|
||
|
"""
|
||
|
if profile_id not in self._profiles:
|
||
|
log.warning("No support for the '%s' profile.", profile_id)
|
||
|
return False
|
||
|
|
||
|
try:
|
||
|
self._get_profile_bases(profile_id)
|
||
|
except ConfigurationError as e:
|
||
|
log.warning("Invalid support for the '%s' profile: %s", profile_id, e)
|
||
|
return False
|
||
|
|
||
|
return True
|
||
|
|
||
|
def detect_profile(self, os_id, variant_id=None):
|
||
|
"""Find a profile that matches the specified values.
|
||
|
|
||
|
:param str os_id: an id of the operating system or None
|
||
|
:param str variant_id: an id of a specific variant of the operating system or None
|
||
|
:return: a product id or None
|
||
|
"""
|
||
|
log.debug("Detecting a profile for ID=%s, VARIANT_ID=%s.", os_id, variant_id)
|
||
|
|
||
|
# Collect keys and profiles for the detection.
|
||
|
profiles = {}
|
||
|
|
||
|
for profile_id, data in self._profiles.items():
|
||
|
key = (data.os_id, data.variant_id)
|
||
|
|
||
|
# The profile is not detectable.
|
||
|
if not any(key):
|
||
|
continue
|
||
|
|
||
|
profiles[key] = profile_id
|
||
|
|
||
|
# Find a profile with the best match.
|
||
|
profile_id = profiles.get((os_id, variant_id)) or profiles.get((os_id, ""))
|
||
|
|
||
|
if profile_id:
|
||
|
log.info("The '%s' profile is detected.", profile_id)
|
||
|
|
||
|
return profile_id
|
||
|
|
||
|
def collect_configurations(self, profile_id):
|
||
|
"""Collect configuration files of the given profile.
|
||
|
|
||
|
The configuration files should be processed in the given order.
|
||
|
|
||
|
:param profile_id: an id of the profile
|
||
|
:return: a list of paths to configuration files
|
||
|
"""
|
||
|
return self._get_profile_configs(profile_id)
|
||
|
|
||
|
def _get_profile_config(self, profile_id):
|
||
|
"""Get the configuration path of the profile.
|
||
|
|
||
|
:param profile_id: an id of the profile
|
||
|
:return: a path to a configuration file
|
||
|
"""
|
||
|
return self._profiles.get(profile_id).config_path
|
||
|
|
||
|
def _get_profile_configs(self, profile_id):
|
||
|
"""Get a list of configuration paths of the profile.
|
||
|
|
||
|
The configuration files should be processed in the given order.
|
||
|
|
||
|
:param profile_id: an id of the profile
|
||
|
:return: a list of paths to a configuration files
|
||
|
"""
|
||
|
profile_ids = reversed(self._get_profile_bases(profile_id))
|
||
|
return [self._get_profile_config(i) for i in profile_ids]
|
||
|
|
||
|
def _get_profile_base(self, profile_id):
|
||
|
"""Get the base of the profile.
|
||
|
|
||
|
:param profile_id: an id of the profile
|
||
|
:return: an id of the base profile
|
||
|
"""
|
||
|
return self._profiles.get(profile_id).base_profile
|
||
|
|
||
|
def _get_profile_bases(self, profile_id):
|
||
|
"""Return a list of bases of the given profile.
|
||
|
|
||
|
The profiles are ordered by the "is based on" relation.
|
||
|
|
||
|
If the profile A is based on the profile B and the profile B
|
||
|
is based on the profile C, then the related profiles of the
|
||
|
profile A are: A, B, C
|
||
|
|
||
|
:param profile_id: an id of the profile
|
||
|
:return: a list of ids of the base profiles
|
||
|
:raises: ConfigurationError if the dependencies cannot be resolved
|
||
|
"""
|
||
|
current_id = profile_id
|
||
|
visited = set()
|
||
|
profiles = []
|
||
|
|
||
|
while current_id:
|
||
|
if current_id not in self._profiles:
|
||
|
raise ConfigurationError(
|
||
|
"Dependencies of the '{}' profile cannot be resolved "
|
||
|
"due to an unknown '{}' profile.".format(profile_id, current_id)
|
||
|
)
|
||
|
|
||
|
if current_id in visited:
|
||
|
raise ConfigurationError(
|
||
|
"Dependencies of the '{}' profile cannot be resolved "
|
||
|
"due to a conflict with '{}'.".format(profile_id, current_id)
|
||
|
)
|
||
|
|
||
|
visited.add(current_id)
|
||
|
profiles.append(current_id)
|
||
|
current_id = self._get_profile_base(current_id)
|
||
|
|
||
|
return profiles
|