mirror of
https://github.com/peridotbuild/pv2.git
synced 2024-11-25 06:31:26 +00:00
e48a54db3a
* Util Module * Provides: color class (for specialty stdout logging) * Provides: constants classes for rpm, errors, and mock * Provides: error classes for generic error handling and future fault handler * Provides: generic classes for generic, repeatable use cases * Provides: rpmutil with rpm utilities that range from basic to advanced metadata handling * Add mock module * Can generate a usable mock config based on input provided * Can generate mock plugin configuration as provided * cache related plugins are hardcoded as disabled * Supports plugins: chroot scanning, embedding files, bind mounts * Can generate basic dnf configs with repo information * (Currently limited) Error handler * Runs mock commands (such as build, buildsrpm, init, shell) * Add modularity module (very limited, doesn't really do much) * Add peridotpb example (does nothing, will likely be its own thing) * Add MIT license
873 lines
32 KiB
Python
873 lines
32 KiB
Python
# -*- mode:python; coding:utf-8; -*-
|
|
# Louis Abel <label@rockylinux.org>
|
|
"""
|
|
Utility functions for mock configuration.
|
|
"""
|
|
|
|
import collections
|
|
import copy
|
|
import json
|
|
import re
|
|
import hashlib
|
|
from configparser import ConfigParser
|
|
from io import StringIO, IOBase
|
|
from pv2.util import error as err
|
|
from pv2.util import constants as const
|
|
from pv2.util import generic as generic_util
|
|
|
|
# List all classes in this module
|
|
__all__ = [
|
|
'DnfConfig',
|
|
'DnfRepoConfig',
|
|
'MockConfig',
|
|
'MockPluginConfig',
|
|
'MockBindMountPluginConfig',
|
|
'MockChrootScanPluginConfig',
|
|
'MockChrootFileConfig',
|
|
'MockConfigUtils',
|
|
'MockMacroConfig',
|
|
'MockMacroFileConfig',
|
|
]
|
|
|
|
# pylint: disable=too-few-public-methods
|
|
class MockConfigUtils:
|
|
"""
|
|
Mock config utilities. Provides basic things needed when making a mock
|
|
config.
|
|
"""
|
|
@staticmethod
|
|
def config_string(value):
|
|
"""
|
|
Converts a given value to a mock compatible string
|
|
|
|
Value should be:
|
|
* bool
|
|
* int
|
|
* string
|
|
* list
|
|
* tuple
|
|
* None
|
|
"""
|
|
|
|
# If the value being sent is none, a boolean, int, or tuple, just
|
|
# straight up return it as a string.
|
|
if value is None or isinstance(value, (bool, int, tuple)):
|
|
return str(value)
|
|
|
|
# If it's a string or a list, return it as a json string/list. We make
|
|
# sure we convert it properly and going through json makes sure it
|
|
# comes out right.
|
|
if isinstance(value, (str, list)):
|
|
return json.dumps(value)
|
|
|
|
# Error out if a value was sent that is not supported.
|
|
raise err.ProvidedValueError(f'{type(value)}: {value} is not supported.')
|
|
|
|
@staticmethod
|
|
def gen_config_string(name: str, status: bool) -> str:
|
|
"""
|
|
Generates a output string to enable a plugin
|
|
"""
|
|
config_name = copy.copy(name)
|
|
config_status = __class__.config_string(status)
|
|
output = f'config_opts["plugin_conf"]["{config_name}_enable"] = {config_status}\n'
|
|
return output
|
|
|
|
@staticmethod
|
|
def gen_config_string_with_opts(name: str, status: bool, opts: dict) -> str:
|
|
"""
|
|
Generates a output string to add options to an enabled plugin
|
|
"""
|
|
config_name = copy.copy(name)
|
|
config_status = __class__.config_string(status)
|
|
config_opts = copy.copy(opts)
|
|
output = f'config_opts["plugin_conf"]["{config_name}_enable"] = {config_status}\n'
|
|
if not status:
|
|
return output
|
|
|
|
output += f'config_opts["plugin_conf"]["{config_name}_opts"] = {{}}\n'
|
|
|
|
# If plugin options were provided, we try to go through and spit them
|
|
# out properly. Some documented plugins use nested dictionaries and the
|
|
# value being a string. This helps with that.
|
|
for key, option in sorted(config_opts):
|
|
key_config = __class__.config_string(key)
|
|
option_config = __class__.config_string(option)
|
|
# pylint: disable=line-too-long
|
|
output += f'config_opts["plugin_conf"]["{config_name}_opts"][{key_config}] = {option_config}\n'
|
|
|
|
return output
|
|
|
|
@staticmethod
|
|
def gen_config_option(option, value, append=False) -> str:
|
|
"""
|
|
Helps generate the 'config_opts' part of a mock configuration.
|
|
"""
|
|
outter = ''
|
|
option = __class__.config_string(option)
|
|
|
|
# If a dictionary, get all key value pairs and splay them out into
|
|
# strings (sending to config_string).
|
|
if isinstance(value, dict):
|
|
for key, val in sorted(value.items()):
|
|
key_name = __class__.config_string(key)
|
|
val_name = __class__.config_string(val)
|
|
outter += f'config_opts[{option}][{key_name}] = {val_name}\n'
|
|
# Some options/plugins use .append for whatever reason. Setting
|
|
# append to True will allow this portion to work and play it out into a
|
|
# string.
|
|
elif append:
|
|
value_str = __class__.config_string(value)
|
|
outter += f'config_opts[{option}].append({value_str})\n'
|
|
# Some options are just options in general, a key value string. This
|
|
# covers the rest.
|
|
else:
|
|
value_str = __class__.config_string(value)
|
|
# pylint: disable=consider-using-f-string
|
|
outter += f'config_opts[{option}] = {value_str}\n'
|
|
return outter
|
|
|
|
class DnfConfigurator:
|
|
"""
|
|
Base class for dnf configuration generation. Should only contain static
|
|
classes.
|
|
"""
|
|
@staticmethod
|
|
def gen_config_section(section, opts):
|
|
"""
|
|
Generate a config section using the config parser and data we're
|
|
receiving. This should be able to handle both [main] and repo sections.
|
|
"""
|
|
# A dnf configuration is key=value, sort of like an ini file.
|
|
# ConfigParser gets us close to that.
|
|
config = ConfigParser()
|
|
config.add_section(section)
|
|
for key, value in sorted(opts.items()):
|
|
|
|
# Continue if repositoryid was caught. We already added the section
|
|
# above.
|
|
if key == 'repositoryid':
|
|
continue
|
|
|
|
# Based on the key we received, we'll determine how the value will
|
|
# be presented. For example, for cases of the key/values being
|
|
# boolean options, regardless of what's received as the truthy
|
|
# value, we'll convert it to a string integer. The rest are
|
|
# strings in general.
|
|
if key in const.MockConstants.MOCK_DNF_BOOL_OPTIONS:
|
|
config.set(section, key, generic_util.gen_bool_option(value))
|
|
elif key in const.MockConstants.MOCK_DNF_STR_OPTIONS:
|
|
config.set(section, key, str(value))
|
|
elif key in const.MockConstants.MOCK_DNF_LIST_OPTIONS:
|
|
config.set(section, key, value.strip())
|
|
elif key == 'baseurl':
|
|
if isinstance(value, (list, tuple)):
|
|
value = "\n ".join(value)
|
|
config.set(section, key, value.strip())
|
|
else:
|
|
config.set(section, key, generic_util.trim_non_empty_string(key, value))
|
|
|
|
# Export the configuration we made into a file descriptor for use in
|
|
# DnfConfig.
|
|
file_descriptor = StringIO()
|
|
config.write(file_descriptor, space_around_delimiters=False)
|
|
file_descriptor.flush()
|
|
file_descriptor.seek(0)
|
|
return file_descriptor.read()
|
|
|
|
class DnfConfig(DnfConfigurator):
|
|
"""
|
|
This helps with the base configuration part of a mock config.
|
|
"""
|
|
# All these arguments are used. Everything made here is typically pushed
|
|
# into MockConfig.
|
|
# pylint: disable=too-many-locals,too-many-arguments,unused-argument
|
|
def __init__(
|
|
self,
|
|
debuglevel=1,
|
|
retries=20,
|
|
obsoletes=True,
|
|
gpgcheck=False,
|
|
assumeyes=True,
|
|
keepcache=True,
|
|
best=True,
|
|
syslog_ident='peridotbuilder',
|
|
syslog_device='',
|
|
metadata_expire=0,
|
|
install_weak_deps=False,
|
|
protected_packages='',
|
|
reposdir='/dev/null',
|
|
logfile='/var/log/yum.log',
|
|
mdpolicy='group:primary',
|
|
rpmverbosity='info',
|
|
repositories=None,
|
|
module_platform_id=None,
|
|
user_agent='peridotbuilder',
|
|
exclude=None,
|
|
):
|
|
if rpmverbosity not in const.MockConstants.MOCK_RPM_VERBOSITY:
|
|
raise err.ProvidedValueError(f'{rpmverbosity} is not set to a valid value')
|
|
# The repodata setup is a bit weird. What we do is we go through all
|
|
# "locals" for this class and build everything into a dictionary. We
|
|
# later send this and the repositories dictionary to gen_config_section.
|
|
self.__repodata = {}
|
|
for (key, value) in iter(list(locals().items())):
|
|
if key not in ['self', 'repositories'] and value is not None:
|
|
self.__repodata[key] = value
|
|
|
|
self.__repositories = {}
|
|
if repositories:
|
|
for repo in repositories:
|
|
self.add_repo_slot(repo)
|
|
|
|
def add_repo_slot(self, repo):
|
|
"""
|
|
Adds a repository as needed for mock.
|
|
|
|
DnfRepoConfig object is expected for repo.
|
|
"""
|
|
if not isinstance(repo, DnfRepoConfig):
|
|
raise err.ProvidedValueError(f'This type of repo is not supported: {type(repo)}')
|
|
if repo.name in self.__repositories:
|
|
raise err.ExistsValueError(f'Repository already added: {repo.name}')
|
|
self.__repositories[repo.name] = repo
|
|
|
|
def gen_config(self) -> str:
|
|
"""
|
|
Generates the configuration that will be used for mock.
|
|
|
|
Call this to generate the configuration.
|
|
"""
|
|
outter = 'config_opts["dnf.conf"] = """\n'
|
|
outter += self.gen_config_section('main', self.__repodata)
|
|
# Each "repo" instance as a gen_config() command as DnfRepoConfig has
|
|
# that method.
|
|
for repo_name in sorted(self.__repositories.keys()):
|
|
outter += self.__repositories[repo_name].gen_config()
|
|
outter += '"""\n'
|
|
return outter
|
|
|
|
class DnfRepoConfig(DnfConfigurator):
|
|
"""
|
|
This helps with the repo configs that would be in a mock config.
|
|
"""
|
|
# pylint: disable=too-many-arguments,unused-argument
|
|
def __init__(self,
|
|
repoid,
|
|
name,
|
|
priority,
|
|
baseurl=None,
|
|
enabled=True,
|
|
gpgcheck=None,
|
|
gpgkey=None,
|
|
sslverify=None,
|
|
module_hotfixes=None
|
|
):
|
|
"""
|
|
Basic dnf repo init, tailored for peridot usage. Mirror lists are *not*
|
|
supported in this class.
|
|
|
|
repoid: str
|
|
A unique name for the repository.
|
|
name: str
|
|
Human readable repo description
|
|
priority: str
|
|
Repository priority. Recommended to set if emulating koji tagging
|
|
and/or doing bootstrapping of some sort.
|
|
baseurl: str or list
|
|
A URL to the directory where the repo is located. repodata must be
|
|
there. Multiple URL's can be provided as a list.
|
|
enabled: bool or int
|
|
Enabled (True or 1) or disabled (False or 0) for this repository.
|
|
More than likely if you've added some extra repository, you want it
|
|
enabled. Otherwise, why are you adding it? For aesthetic reasons?
|
|
gpgcheck: bool or int
|
|
Perform a GPG check on packages if set to True/1.
|
|
gpgkey: str or None
|
|
Some URL or location of the repo gpg key
|
|
sslverify: str or None
|
|
Enable SSL certificate verification if set to 1.
|
|
"""
|
|
|
|
self.__repoconf = {}
|
|
for (key, value) in locals().items():
|
|
if key != 'self' and value is not None:
|
|
self.__repoconf[key] = value
|
|
|
|
def gen_config(self) -> str:
|
|
"""
|
|
Generates the dnf repo config
|
|
|
|
Returns a string
|
|
"""
|
|
section = generic_util.trim_non_empty_string(
|
|
'repoid',
|
|
self.__repoconf['repoid']
|
|
)
|
|
return self.gen_config_section(section, self.__repoconf)
|
|
|
|
@property
|
|
def name(self):
|
|
"""
|
|
Repo name
|
|
"""
|
|
return self.__repoconf['name']
|
|
|
|
|
|
# All mock classes
|
|
class MockConfig(MockConfigUtils):
|
|
"""
|
|
Mock configuration file generator
|
|
"""
|
|
# pylint: disable=too-many-locals,too-many-arguments,unused-argument
|
|
def __init__(
|
|
self,
|
|
target_arch,
|
|
root=None,
|
|
chroot_setup_cmd=None,
|
|
chroot_setup_cmd_pkgs=None,
|
|
dist=None,
|
|
releasever=None,
|
|
package_manager: str = 'dnf',
|
|
enable_networking: bool = False,
|
|
files=None,
|
|
macros=None,
|
|
dnf_config=None,
|
|
basedir=None,
|
|
print_main_output: bool = True,
|
|
target_vendor: str = 'redhat',
|
|
vendor: str = 'Default Vendor',
|
|
packager: str = 'Default Packager <packager@noone.home>',
|
|
distsuffix=None,
|
|
distribution=None,
|
|
**kwargs
|
|
):
|
|
"""
|
|
Mock config init
|
|
|
|
target_arch: string (config_opts['target_arch'])
|
|
files: list (optional)
|
|
dist: must be a string with starting characters . and alphanumeric character
|
|
macros: dict expected, key should start with '%'
|
|
target_vendor: typically 'redhat' and shouldn't be changed in most cases
|
|
vendor: packaging vendor, e.g. Rocky Enterprise Software Foundation
|
|
packager: the packager, e.g. Release Engineering <releng@rockylinux.org>
|
|
chroot_setup_cmd_pkgs: list of packages for the chroot
|
|
"""
|
|
|
|
# A dist value must be defined. This dist value is typically what we
|
|
# see as the %{dist} macro in RPM distributions. For EL and Fedora,
|
|
# they usually start with a "." and then continue with an alphanumeric
|
|
# character.
|
|
if not dist:
|
|
raise err.MissingValueError('The dist value is NOT defined')
|
|
if dist and not re.match(r'^\.[a-zA-Z0-9]', dist):
|
|
raise err.ProvidedValueError('The dist value does not start with a ' +
|
|
'. and alphanumeric character')
|
|
|
|
# A releasever value must be defined. This is basically the version of
|
|
# the EL we're building for.
|
|
if not releasever:
|
|
raise err.MissingValueError('The releasever value is NOT defined.')
|
|
if releasever and not re.match(r'^[0-9]+', releasever):
|
|
raise err.ProvidedValueError('The releasever value does not start ' +
|
|
'with a number.')
|
|
|
|
# Set chroot defaults if necessary. In the constants module, we have a
|
|
# list of the most basic package set required. In the event that
|
|
# someone is building a mock config to use, they can set the
|
|
# chroot_setup_cmd if they wish to something other than install
|
|
# (usually this is almost never the case). More importantly, the
|
|
# packages actually installed into the chroot can be set. Some projects
|
|
# in peridot can potentially dictate this to something other than the
|
|
# defaults.
|
|
if not chroot_setup_cmd:
|
|
chroot_setup_cmd = const.MockConstants.MOCK_DEFAULT_CHROOT_SETUP_CMD
|
|
if not chroot_setup_cmd_pkgs:
|
|
chroot_setup_cmd_pkgs = const.MockConstants.MOCK_DEFAULT_CHROOT_BUILD_PKGS
|
|
|
|
# Each mock chroot needs a name. We do not arbitrarily generate any.
|
|
# The admin must be specific on what they want.
|
|
if not root:
|
|
raise err.MissingValueError('The mock root name was not provided.')
|
|
|
|
# Here we are building the basic mock configuration. We push most of it
|
|
# into dictionaries and then later translate it all into strings.
|
|
legal_host_arches = self.determine_legal_host_arches(target_arch)
|
|
interpreted_dist = self.determine_dist_macro(dist)
|
|
chroot_pkgs = ' '.join(chroot_setup_cmd_pkgs)
|
|
chroot_setup_cmd_string = chroot_setup_cmd + ' ' + chroot_pkgs
|
|
default_macros = {
|
|
'%_rpmfilename': '%%{NAME}-%%{VERSION}-%%{RELEASE}.%%{ARCH}.rpm',
|
|
'%_host': f'{target_arch}-{target_vendor}-linux-gnu',
|
|
'%_host_cpu': target_arch,
|
|
'%_vendor': target_vendor,
|
|
'%_vendor_host': target_vendor,
|
|
'%vendor': vendor,
|
|
'%packager': packager,
|
|
}
|
|
self.__config_opts = {
|
|
'root': root,
|
|
'target_arch': target_arch,
|
|
'legal_host_arches': legal_host_arches,
|
|
'chroot_setup_cmd': chroot_setup_cmd_string,
|
|
'dist': dist.strip('.'),
|
|
'releasever': releasever,
|
|
'basedir': basedir,
|
|
'use_host_resolv': enable_networking,
|
|
'rpmbuild_networking': enable_networking,
|
|
'print_main_output': print_main_output,
|
|
'macros': default_macros,
|
|
}
|
|
self.__config_opts.update(**kwargs)
|
|
self.__extra_config_opts = collections.defaultdict(list)
|
|
self.__files = {}
|
|
self.__macros = {}
|
|
self.__plugins = {}
|
|
if files:
|
|
for chroot_file in files:
|
|
self.add_file(chroot_file)
|
|
|
|
# Set absolute default macros for each build. This is a partial carry
|
|
# over from peridot v1. We add these to an /etc/rpm/macros... file on
|
|
# purpose. Otherwise, if they are set as macros in config_opts, they
|
|
# are placed in /builddir/.rpmmacros, which cannot be overriden. Doing
|
|
# this ensures we can override these macros (e.g. for modules)
|
|
starter_macros = {
|
|
'%dist': interpreted_dist,
|
|
'%distribution': distribution,
|
|
}
|
|
self.add_macros(starter_macros, macro_file='/etc/rpm/macros.xx')
|
|
if macros:
|
|
self.add_macros(macros)
|
|
|
|
# Set the absolute disabled plugins for each build. These three are
|
|
# disabled on purpose. Do NOT alter these. Do NOT attempt to override
|
|
# them. There should never be a reason to ever have these enabled in a
|
|
# build system nor in development tools that use this module.
|
|
yum_cache_plugin = MockPluginConfig(name='yum_cache', enable=False)
|
|
root_cache_plugin = MockPluginConfig(name='root_cache', enable=False)
|
|
ccache_plugin = MockPluginConfig(name='ccache', enable=False)
|
|
self.add_plugin(yum_cache_plugin)
|
|
self.add_plugin(root_cache_plugin)
|
|
self.add_plugin(ccache_plugin)
|
|
|
|
self.__dnf_config = dnf_config
|
|
|
|
def add_file(self, chroot_file):
|
|
"""
|
|
Adds a chroot file to the configuration.
|
|
"""
|
|
if chroot_file.file in self.__files:
|
|
raise err.ProvidedValueError(f'file {chroot_file.file} is already added')
|
|
self.__files[chroot_file.file] = chroot_file
|
|
|
|
def add_macros(self, macro_set, macro_file='/etc/macros/macros.zz'):
|
|
"""
|
|
Adds a set of macros to a mock configuration. This generates a file
|
|
that will be placed into the mock chroot, rather than
|
|
/builddir/.rpmmacros made by config_opts.
|
|
"""
|
|
macro_data = ''
|
|
for key, value in macro_set.items():
|
|
if '%' not in key:
|
|
macro_name = f'%{key}'
|
|
else:
|
|
macro_name = key
|
|
|
|
if not value:
|
|
continue
|
|
|
|
macro_value = value
|
|
|
|
macro_data += f'{macro_name} {macro_value}\n'
|
|
|
|
macro_config = MockMacroFileConfig(content=macro_data, file=macro_file)
|
|
returned_content = macro_config.gen_config()
|
|
self.__macros[macro_file] = returned_content
|
|
|
|
def add_plugin(self, plugin):
|
|
"""
|
|
Adds a mock plugin to the configuration.
|
|
"""
|
|
if plugin.name in self.__plugins:
|
|
raise err.ProvidedValueError(f'plugin {plugin.name} is already configured')
|
|
self.__plugins[plugin.name] = plugin
|
|
|
|
def module_install(self, module_name):
|
|
"""
|
|
Adds a module to module_install
|
|
"""
|
|
if 'module_install' not in self.__config_opts:
|
|
self.__config_opts['module_install'] = []
|
|
|
|
if module_name in self.__config_opts['module_install']:
|
|
raise err.ExistsValueError(f'{module_name} is already provided in module_install')
|
|
|
|
self.__config_opts['module_install'].append(module_name)
|
|
|
|
def module_enable(self, module_name):
|
|
"""
|
|
Adds a module to module_enable
|
|
"""
|
|
if 'module_enable' not in self.__config_opts:
|
|
self.__config_opts['module_enable'] = []
|
|
|
|
if module_name in self.__config_opts['module_enable']:
|
|
raise err.ExistsValueError(f'{module_name} is already provided in module_enable')
|
|
|
|
self.__config_opts['module_enable'].append(module_name)
|
|
|
|
def add_config_opt(self, key: str, value: str):
|
|
"""
|
|
Use this to add additional options not covered by this module
|
|
"""
|
|
self.__extra_config_opts[key].append(value)
|
|
|
|
@staticmethod
|
|
def determine_dist_macro(dist: str) -> str:
|
|
"""
|
|
Return a string of the interpreted dist macro. This will typically
|
|
match current EL release packages.
|
|
"""
|
|
# We don't want a case where we are sending "~bootstrap" as the dist
|
|
# already. So we're stripping it and letting the build figure it out
|
|
# for itself. The macro with_bootstrap conditional should dictate it.
|
|
if "~bootstrap" in dist:
|
|
starting_dist = dist.replace('~bootstrap', '')
|
|
else:
|
|
starting_dist = dist
|
|
|
|
# This is the current dist value that is used in current EL's. It will
|
|
# likely change over time. This value is *also* provided in
|
|
# system-release, but having it here is to make sure it *is* here just
|
|
# in case. This is especially useful when bootstrapping from ELN or
|
|
# stream.
|
|
# pylint: disable=line-too-long,consider-using-f-string
|
|
dist_value = '%{{!?distprefix0:%{{?distprefix}}}}%{{expand:%{{lua:for i=0,9999 do print("%{{?distprefix" .. i .."}}") end}}}}{0}%{{?distsuffix}}%{{?with_bootstrap:~bootstrap}}'.format(starting_dist)
|
|
return dist_value
|
|
|
|
# pylint: disable=too-many-return-statements
|
|
@staticmethod
|
|
def determine_legal_host_arches(target_arch: str) -> tuple:
|
|
"""
|
|
Return a tuple of acceptable arches for a given architecture. This will
|
|
appear as a list in the final mock config.
|
|
"""
|
|
# The legal_host_arches is typically a tuple of supported arches for a
|
|
# given platform. Based on the target_arch sent, we'll set the legal
|
|
# arches.
|
|
|
|
# We can easily use "switch" here but we are accounting for python 3.9
|
|
# at this time, which does not have it.
|
|
if target_arch == "x86_64":
|
|
return const.MockConstants.MOCK_X86_64_LEGAL_ARCHES
|
|
|
|
if target_arch in ['i386', 'i486', 'i586', 'i686']:
|
|
return const.MockConstants.MOCK_I686_LEGAL_ARCHES
|
|
|
|
if target_arch in "aarch64":
|
|
return const.MockConstants.MOCK_AARCH64_LEGAL_ARCHES
|
|
|
|
if target_arch in "armv7hl":
|
|
return const.MockConstants.MOCK_ARMV7HL_LEGAL_ARCHES
|
|
|
|
if target_arch in "ppc64le":
|
|
return const.MockConstants.MOCK_PPC64LE_LEGAL_ARCHES
|
|
|
|
if target_arch in "s390x":
|
|
return const.MockConstants.MOCK_S390X_LEGAL_ARCHES
|
|
|
|
if target_arch in "riscv64":
|
|
return const.MockConstants.MOCK_RISCV64_LEGAL_ARCHES
|
|
|
|
# This shouldn't happen, but who knows
|
|
if target_arch in "noarch":
|
|
return const.MockConstants.MOCK_NOARCH_LEGAL_ARCHES
|
|
|
|
return err.ProvidedValueError(f'Legal arches not found for {target_arch}.')
|
|
|
|
def set_dnf_config(self, dnf_config):
|
|
"""
|
|
Adds a dnf config section
|
|
"""
|
|
self.__dnf_config = dnf_config
|
|
|
|
# Disabling until I can figure out a better way to handle this
|
|
# pylint: disable=too-many-branches
|
|
def export_mock_config(self, config_file, root=None):
|
|
"""
|
|
Exports the mock configuration to a file.
|
|
"""
|
|
if not root:
|
|
if self.__config_opts.get('root'):
|
|
root = self.__config_opts.get('root')
|
|
else:
|
|
raise err.MissingValueError('root value is missing. This should ' +
|
|
'not have happened and is likely the ' +
|
|
'result of this module being ' +
|
|
'modified and not tested.')
|
|
|
|
if not isinstance(config_file, str):
|
|
if isinstance(config_file, IOBase):
|
|
raise err.ProvidedValueError('config_file must be a string. it cannot ' \
|
|
'be an open file handle.')
|
|
raise err.ProvidedValueError('config_file must be a string.')
|
|
|
|
# This is where we'll write the file. We'll go through each
|
|
# configuration option, generate their configs as they're found, and
|
|
# write them. It should look close, if not identical to a typical mock
|
|
# configuration.
|
|
with open(config_file, 'w', encoding='utf-8') as file_descriptor:
|
|
try:
|
|
if root:
|
|
file_descriptor.write(self.gen_config_option('root', root))
|
|
for option, value in sorted(self.__config_opts.items()):
|
|
if option == 'root' or value is None:
|
|
continue
|
|
file_descriptor.write(self.gen_config_option(option, value))
|
|
for option, value_list in sorted(self.__extra_config_opts.items()):
|
|
for value in value_list:
|
|
file_descriptor.write(self.gen_config_option(option, value, append=True))
|
|
for plugin in self.__plugins.values():
|
|
file_descriptor.write(plugin.gen_config())
|
|
for macro_file in self.__macros.values():
|
|
file_descriptor.write(macro_file)
|
|
for chroot_file in self.__files.values():
|
|
file_descriptor.write(chroot_file.gen_config())
|
|
if self.__dnf_config:
|
|
file_descriptor.write(self.__dnf_config.gen_config())
|
|
except Exception as exc:
|
|
raise err.ConfigurationError('There was an error exporting the mock ' \
|
|
f'configuration: {exc}')
|
|
finally:
|
|
file_descriptor.close()
|
|
|
|
@property
|
|
def mock_config_hash(self):
|
|
"""
|
|
Creates a hash sum of the configuration. Could be used for tracking
|
|
and/or comparison purposes.
|
|
|
|
This may not currently work at this time.
|
|
"""
|
|
hasher = hashlib.sha256()
|
|
file_descriptor = StringIO()
|
|
self.export_mock_config(file_descriptor)
|
|
file_descriptor.seek(0)
|
|
hasher.update(file_descriptor.read().encode('utf-8'))
|
|
file_descriptor.close()
|
|
return hasher.hexdigest()
|
|
|
|
### Start Plugins
|
|
class MockPluginConfig(MockConfigUtils):
|
|
"""
|
|
Mock plugin configuration helper. For cases where some plugin doesn't have
|
|
some sort of class in this module.
|
|
"""
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
enable: bool,
|
|
**kwargs
|
|
):
|
|
"""
|
|
Plugin config init. Used to enable/disable plugins. Additional plugin
|
|
options can be defined in kwargs (may or may not work)
|
|
|
|
name: plugin name, string
|
|
enable: boolean
|
|
"""
|
|
self.name = copy.copy(name)
|
|
self.enable = enable
|
|
self.opts = copy.copy(kwargs)
|
|
|
|
def gen_config(self):
|
|
"""
|
|
Helps add a plugin configuration to mock
|
|
"""
|
|
plugin_name = self.name
|
|
config_string_status = self.enable
|
|
outter = self.gen_config_string_with_opts(
|
|
name=plugin_name,
|
|
status=config_string_status,
|
|
opts=self.opts
|
|
)
|
|
return outter
|
|
|
|
class MockBindMountPluginConfig(MockConfigUtils):
|
|
"""
|
|
Mock plugin configuration helper
|
|
"""
|
|
def __init__(
|
|
self,
|
|
enable: bool,
|
|
mounts: list
|
|
):
|
|
"""
|
|
Plugin config init. Used to enable/disable bind mount plugin.
|
|
|
|
enable: boolean
|
|
mounts: list of tuples
|
|
"""
|
|
self.name = 'bind_mount'
|
|
self.enable = enable
|
|
self.mounts = mounts
|
|
|
|
def gen_config(self):
|
|
"""
|
|
Helps add a plugin configuration to mock
|
|
"""
|
|
bind_config_status = self.config_string(self.enable)
|
|
|
|
# Documentation wants a ['dirs'] section added, so we're obliging.
|
|
outter = self.gen_config_string(name='bind_mount', status=bind_config_status)
|
|
|
|
if not self.enable or not self.mounts:
|
|
return outter
|
|
|
|
for local_path, mock_chroot_path in self.mounts:
|
|
# pylint: disable=line-too-long
|
|
outter += f'config_opts["plugin_conf"]["bind_mount_opts"]["dirs"].append(("{local_path}", "{mock_chroot_path}"))\n'
|
|
|
|
return outter
|
|
|
|
class MockChrootScanPluginConfig(MockConfigUtils):
|
|
"""
|
|
Helps setup the chroot scan plugin.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
enable,
|
|
**kwargs
|
|
):
|
|
"""
|
|
Inits the plugin configuration.
|
|
|
|
enable: bool
|
|
kwargs: additional options can be sent in here
|
|
"""
|
|
self.name = 'chroot_scan'
|
|
self.enable = enable
|
|
self.opts = copy.copy(kwargs)
|
|
|
|
def gen_config(self):
|
|
"""
|
|
Helps add a plugin configuration to mock
|
|
"""
|
|
chroot_config_status = self.enable
|
|
|
|
# This one is weird. The documentation specifically wants a "dict" as a
|
|
# string... Not really clear why. But we'll roll with it.
|
|
outter = self.gen_config_string(
|
|
name='chroot_scan',
|
|
status=chroot_config_status
|
|
)
|
|
|
|
opts_dict = {}
|
|
for key, option in sorted(self.opts.items()):
|
|
opts_dict[key] = option
|
|
|
|
outter += f'config_opts["plugin_conf"]["chroot_scan_opts"] = {opts_dict}\n'
|
|
return outter
|
|
|
|
class MockShowrcPluginConfig(MockConfigUtils):
|
|
"""
|
|
Helps enable the showrc plugin. Useful for showing defined rpm macros for a
|
|
build.
|
|
"""
|
|
|
|
def __init__(self, enable):
|
|
"""
|
|
Inits the plugin configuration.
|
|
|
|
enable: bool
|
|
"""
|
|
self.name = 'showrc'
|
|
self.enable = enable
|
|
|
|
def gen_config(self):
|
|
"""
|
|
Helps add a plugin configuration to mock
|
|
"""
|
|
showrc_config_status = self.enable
|
|
outter = f'config_opts["plugin_conf"]["showrc_enable"] = {showrc_config_status}\n'
|
|
return outter
|
|
|
|
### End Plugins
|
|
|
|
class MockChrootFileConfig:
|
|
"""
|
|
Helps embed files into a mock chroot. May be useful to trick builds if
|
|
necessary but also could help with things like secureboot if needed.
|
|
"""
|
|
def __init__(
|
|
self,
|
|
file: str,
|
|
content=None
|
|
):
|
|
"""
|
|
Create a file to embed into the mock root
|
|
"""
|
|
|
|
if not content:
|
|
raise err.MissingValueError('Macro content was not provided')
|
|
|
|
self.file = file
|
|
self._content = content
|
|
|
|
def gen_config(self):
|
|
"""
|
|
Return a string to be added to mock config
|
|
"""
|
|
return f'config_opts["files"]["{self.file}"] = """{self._content}\n"""\n\n'
|
|
|
|
class MockMacroConfig:
|
|
"""
|
|
Helps add macros into a mock configuration. This is a typical staple of
|
|
builds. In most cases, you won't need this and instead will use
|
|
MockMacroFileConfig.
|
|
"""
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
value: str
|
|
):
|
|
"""
|
|
init the class
|
|
"""
|
|
self.name = name
|
|
self.value = value
|
|
|
|
def gen_config(self):
|
|
"""
|
|
Generate the macro option
|
|
"""
|
|
return f'config_opts["macros"]["{self.name}"] = "{self.value}"'
|
|
|
|
class MockMacroFileConfig:
|
|
"""
|
|
Helps add macros into a mock configuration into a file instead.
|
|
"""
|
|
def __init__(
|
|
self,
|
|
file: str = '/etc/rpm/macros.zz',
|
|
content=None
|
|
):
|
|
"""
|
|
Create a macro file to embed into the mock root
|
|
"""
|
|
|
|
if not content:
|
|
raise err.MissingValueError('Macro content was not provided')
|
|
|
|
self.file = file
|
|
self._content = content
|
|
|
|
def gen_config(self):
|
|
"""
|
|
Return a string to be added to mock config
|
|
"""
|
|
return f'config_opts["files"]["{self.file}"] = """\n\n{self._content}\n"""\n\n'
|