Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RHUI and RPM Transaction configuration #1142

Draft
wants to merge 28 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a473fe9
Initial work to demonstrate an actor config framework.
abadger Nov 7, 2023
cece1e3
Remove rpm transaction configs from repository level.
abadger Sep 20, 2024
9f02c14
rework RHUI config
Oct 3, 2024
f9258af
add source client back to RHUI config
Oct 3, 2024
52e32de
check_rhui: read config
Oct 3, 2024
cfee78b
Fix broken imports
dkubek Oct 4, 2024
5499427
rhui: change how configs are accessed
Oct 5, 2024
ac879af
add example rhui config
Oct 5, 2024
a8f1372
spec: make actor_conf.d before trying to place there stuff
Oct 6, 2024
13508f6
cmd(preupgrade): load actor config
Oct 6, 2024
f8cbdc9
make all rhui config fields required
Oct 15, 2024
e437148
rename target repositories field
Oct 15, 2024
bea0d31
fix typo in rhui config docs
Oct 16, 2024
70f7d5b
fix linter complaints in rhui config
Oct 16, 2024
07a24f7
testutils: add support for configs
Oct 16, 2024
7486ce0
checkrhui: fix tests
Oct 16, 2024
0d044b2
tmp: install correct leapp branch to run tests
Oct 16, 2024
35dd900
checkrhui: fix linter errors in tests
Oct 16, 2024
7eaa78a
fixup! testutils: add support for configs
Oct 16, 2024
418f023
fix imports
Oct 16, 2024
55bdb5c
userspacegen(rhui): remove repofiles only if now owned by an RPM
Oct 20, 2024
61ae067
check_rhui(tests): add config tests
Oct 20, 2024
f82750e
check_rhui: error out if config contains nonexisting files
Oct 20, 2024
50879db
checkrhui: use config.src_client
Oct 20, 2024
aea50bd
check_rhui(tests): check invalid files in the config trigger error
Oct 20, 2024
892c35e
fixup! userspacegen(rhui): remove repofiles only if now owned by an RPM
Oct 20, 2024
ddd773a
fixup! check_rhui(tests): add config tests
Oct 20, 2024
356a5b3
check_rhui(tests): resort imports
Oct 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions commands/preupgrade/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import sys
import uuid

from leapp.actors import config as actor_config
from leapp.cli.commands import command_utils
from leapp.cli.commands.config import get_config
from leapp.cli.commands.upgrade import breadcrumbs, util
Expand Down Expand Up @@ -60,6 +61,13 @@ def preupgrade(args, breadcrumbs):
raise CommandError(exc.message)

workflow = repositories.lookup_workflow('IPUWorkflow')()

# Read the Actor Config and validate it against the schemas saved in the configuration.
actor_config_schemas = tuple(actor.config_schemas for actor in repositories.actors)
actor_config_schemas = actor_config.normalize_schemas(actor_config_schemas)
actor_config_path = cfg.get('actor_config', 'path')
actor_config.load(actor_config_path, actor_config_schemas)

util.warn_if_unsupported(configuration)
util.process_whitelist_experimental(repositories, workflow, configuration, logger)
with beautify_actor_exception():
Expand Down
12 changes: 12 additions & 0 deletions commands/upgrade/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import sys
import uuid

from leapp.actors import config as actor_config
from leapp.cli.commands import command_utils
from leapp.cli.commands.config import get_config
from leapp.cli.commands.upgrade import breadcrumbs, util
Expand Down Expand Up @@ -90,6 +91,17 @@ def upgrade(args, breadcrumbs):
except LeappError as exc:
raise CommandError(exc.message)
workflow = repositories.lookup_workflow('IPUWorkflow')(auto_reboot=args.reboot)

# Read the Actor Config and validate it against the schemas saved in the
# configuration.
actor_config_schemas = tuple(actor.config_schemas for actor in repositories.actors)
actor_config_schemas = actor_config.normalize_schemas(actor_config_schemas)
actor_config_path = cfg.get('actor_config', 'path')
# Note: actor_config.load() stores the loaded actor config into a global
# variable which can then be accessed by functions in that file. Is this
# the right way to store that information?
actor_config.load(actor_config_path, actor_config_schemas)
Comment on lines +100 to +103
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that we are doing it this way because we want to load the configs before anything else is done. However, this leads to the problem that whether config is loaded is not within the power of the framework. Instead of having it here in the command, wouldn't it be better to have load() in some early stage of workflow.run()? That way we can be sure that configs are loaded from the point of view of the framework

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't remember if Petr and I discussed workflow.run() as a possible place for this. A quick look seems to show that workflows don't have access to the RepositoryManager. Does this mean we cannot lookup information about all of the available Actors from within a workflow? Or just that we would have to access them in another way?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workflow should collect all discovered actors during its initialization (https://github.com/oamg/leapp/blob/7565b5c8cd5a098522565ff0ed5278eb2b04b7a8/leapp/workflows/__init__.py#L167). I have used this before and thought it might be helpful again here.

To me, loading the configuration just after workflow starts and before any actor is processed is a place where I would put it personally. However, there might be reasons not to do this if it has not come up in a discussion. Have you thought about this? @pirat89 Or am I missing something

Copy link
Member

@pirat89 pirat89 Sep 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure - I mean, we looked for a place where we could put it and that seemed good - especially in case we would like to have in future repository-shared configs (configs dir defined in the repository level); which we decided last time that is not way we want to go (at least now). We already have such a handling in case of answerfiles, but you are right that having the possibility to keep it implemented just inside the framework side (without the need to know additional magic that should be specified in CLI commands on the repositoriy side) sounds much better to me. Also, remember on snactor! We need a way how to execute an actor itself with snactor without the need to run the workflow. (it can be done later, but it should be done still for the upcoming version)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dkubek The code you're pointing to looks like it just looks at the actors available in all the phases that the workflow runs rather than all of the actors in the repositories?

Although I'm not sure if we need all of them or not.

  • If we get all the actors, we can warn the user that there is unrecognized configuration. This will catch typos and configuration of old versions which is no longer valid.
  • If we only get the actors used by the workflow, then we wll have false positives if we emit warnings for that (so I would probably not emit a warning on unrecognized config entries).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Also, I agree with you that if we can put this into framework code, it is the better place).


util.process_whitelist_experimental(repositories, workflow, configuration, logger)
util.warn_if_unsupported(configuration)
with beautify_actor_exception():
Expand Down
12 changes: 12 additions & 0 deletions etc/leapp/actor_conf.d/rhui.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
rhui:
use_config : True
source_clients: [ rh-amazon-rhui-client ]
target_clients: [ rh-amazon-rhui-client ]
cloud_provider: aws
upgrade_files:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example is looking good! I also know of some use cases that need to specify the CA certificate used for the dnf calls to the RHUI repo, but perhaps that configuration is encapsulated in the leapp-aws.repo file...?

Copy link
Member

@MichalHe MichalHe Oct 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean the sslcacert, sslclientcert and sslclientkey fields in repository definition? If yes, then this is doable with the proposed configuration. You just specify all the ssl* fields in a repofile provided in upgrade_files as you would normally do. Then you need to list the certificates/keys also in upgrade_files, together with information on where to place them. For example, your repofile /root/my-upgrade-repos.repo would contain a repository with:

sslclientcert=/etc/pki/rhui/product/content-rhel8.crt

Then you need tell leapp that it should grab the content-rhel8.crt somewhere from the source system and place it at /etc/pki/rhui/product/content-rhel8.crt.

The configuration would then look like:

rhui:
  use_config : True
  source_clients: [  rhel8-client ]   # what client to remove from the source system
  target_clients: [ rhel9-client ]  # what client should be installed during the upgrade
  cloud_provider: aws  # for leapp to use internally 
  upgrade_files:
    /root/my-upgrade-repos.repo : /etc/yum.repos.d  # during upgrade, place this repofile into /etc/yum.repos.d
    /root/content-rhel8.crt : /etc/pki/rhui/product/  # during the upgrade, place this key here. Same path as in sslclientcert in the repo
   #  .... other keys/certs mentioned in the repofile, or otherwise important for leapp to access target OS content

This should enable basically anyone running a RHUI system to upgrade, assuming that provider's rhui clients do not use some eccentric python dependencies (although that should be fine too, I just haven't tested these setups).

You get all the necessary upgrade_files by spawning a RHEL VM running the target OS, and copying the files, keys and certs provided by the RHUI client. There are also other ways, but this is likely the simplest one. If you are upgrading an entire fleet, you naturally need to spawn the target machine only once.

Also note that if you rename the repositories in the repofile for some reason, and then use these new names in enabled_target_repositories, the DNF transaction check leapp does might fail. Therefore, use the same repoids as the target client uses and everything should be fine. If you would like to test this, please ping me with any questions. It is always nice to get feedback before we merge this 😀.

/usr/share/leapp-repository/repositories/system_upgrade/common/files/rhui/leapp-rhui-aws/rhui-client-config-server-9.crt : /etc/pki/rhui/product/
/usr/share/leapp-repository/repositories/system_upgrade/common/files/rhui/leapp-rhui-aws/rhui-client-config-server-9.key : /etc/pki/rhui/private/
/usr/share/leapp-repository/repositories/system_upgrade/common/files/rhui/leapp-rhui-aws/leapp-aws.repo : /etc/yum.repos.d/
enabled_target_repositories:
- rhel-9-appstream-rhui-rpms
- rhel-9-baseos-rhui-rpms
5 changes: 5 additions & 0 deletions packaging/leapp-repository.spec
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ install -m 0755 -d %{buildroot}%{_sysconfdir}/leapp/files/
install -m 0644 etc/leapp/transaction/* %{buildroot}%{_sysconfdir}/leapp/transaction
install -m 0644 etc/leapp/files/* %{buildroot}%{_sysconfdir}/leapp/files

# Actor configuration
install -m 0755 -d %{buildroot}%{_sysconfdir}/leapp/actor_conf.d/
install -m 0644 etc/leapp/actor_conf.d/* %{buildroot}%{_sysconfdir}/leapp/actor_conf.d

# install CLI commands for the leapp utility on the expected path
install -m 0755 -d %{buildroot}%{leapp_python_sitelib}/leapp/cli/
cp -r commands %{buildroot}%{leapp_python_sitelib}/leapp/cli/
Expand Down Expand Up @@ -293,6 +297,7 @@ done;
%{_sysconfdir}/leapp/transaction/*
%{repositorydir}/*
%{leapp_python_sitelib}/leapp/cli/commands/*
%{_sysconfdir}/leapp/actor_conf.d/*


%files -n %{lpr_name}-deps
Expand Down
6 changes: 5 additions & 1 deletion repos/system_upgrade/common/actors/cloud/checkrhui/actor.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from leapp.actors import Actor
from leapp.configs.common.rhui import all_rhui_cfg
from leapp.libraries.actor import checkrhui as checkrhui_lib
from leapp.models import (
CopyFile,
Expand All @@ -8,7 +9,8 @@
RequiredTargetUserspacePackages,
RHUIInfo,
RpmTransactionTasks,
TargetUserSpacePreupgradeTasks
TargetRepositories,
TargetUserSpacePreupgradeTasks,
)
from leapp.reporting import Report
from leapp.tags import FactsPhaseTag, IPUWorkflowTag
Expand All @@ -21,13 +23,15 @@ class CheckRHUI(Actor):
"""

name = 'checkrhui'
config_schemas = all_rhui_cfg
consumes = (InstalledRPM,)
produces = (
KernelCmdlineArg,
RHUIInfo,
RequiredTargetUserspacePackages,
Report, DNFPluginTask,
RpmTransactionTasks,
TargetRepositories,
TargetUserSpacePreupgradeTasks,
CopyFile,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,32 @@
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common import rhsm, rhui
from leapp.libraries.common.config import version
import leapp.configs.common.rhui as rhui_config_lib
from leapp.configs.common.rhui import ( # Import all config fields so we are not using their name attributes directly
RhuiEnabledTargetRepositories,
RhuiCloudProvider,
RhuiCloudVariant,
RhuiSourcePkgs,
RhuiTargetPkgs,
RhuiUpgradeFiles,
RhuiUseConfig,
)
from leapp.libraries.stdlib import api
from leapp.models import (
CopyFile,
CustomTargetRepository,
DNFPluginTask,
InstalledRPM,
RHUIInfo,
RpmTransactionTasks,
TargetRepositories,
TargetRHUIPostInstallTasks,
TargetRHUIPreInstallTasks,
TargetRHUISetupInfo,
TargetUserSpacePreupgradeTasks
)


MatchingSetup = namedtuple('MatchingSetup', ['family', 'description'])


Expand Down Expand Up @@ -291,11 +304,11 @@ def produce_rhui_info_to_setup_target(rhui_family, source_setup_desc, target_set
api.produce(rhui_info)


def produce_rpms_to_install_into_target(source_setup, target_setup):
to_install = sorted(target_setup.clients - source_setup.clients)
to_remove = sorted(source_setup.clients - target_setup.clients)
def produce_rpms_to_install_into_target(source_clients, target_clients):
to_install = sorted(target_clients - source_clients)
to_remove = sorted(source_clients - target_clients)

api.produce(TargetUserSpacePreupgradeTasks(install_rpms=sorted(target_setup.clients)))
api.produce(TargetUserSpacePreupgradeTasks(install_rpms=sorted(target_clients)))
if to_install or to_remove:
api.produce(RpmTransactionTasks(to_install=to_install, to_remove=to_remove))

Expand All @@ -316,7 +329,49 @@ def inform_about_upgrade_with_rhui_without_no_rhsm():
return False


def emit_rhui_setup_tasks_based_on_config(rhui_config_dict):
config_upgrade_files = rhui_config_dict[RhuiUpgradeFiles.name]
files_to_copy_into_overlay = [CopyFile(src=key, dst=value) for key, value in config_upgrade_files.items()]
preinstall_tasks = TargetRHUIPreInstallTasks(files_to_copy_into_overlay=files_to_copy_into_overlay)

target_client_setup_info = TargetRHUISetupInfo(
preinstall_tasks=preinstall_tasks,
postinstall_tasks=TargetRHUIPostInstallTasks(),
bootstrap_target_client=False, # We don't need to install the client into overlay - user provided all files
)

rhui_info = RHUIInfo(
provider=rhui_config_dict[RhuiCloudProvider.name],
variant=rhui_config_dict[RhuiCloudVariant.name],
src_client_pkg_names=list(),
target_client_pkg_names=rhui_config_dict[RhuiTargetPkgs.name],
target_client_setup_info=target_client_setup_info
)
api.produce(rhui_info)


def request_configured_repos_to_be_enabled(rhui_config):
config_repos_to_enable = rhui_config[RhuiEnabledTargetRepositories.name]
custom_repos = [CustomTargetRepository(repoid=repoid) for repoid in config_repos_to_enable]
if custom_repos:
target_repos = TargetRepositories(custom_repos=custom_repos, rhel_repos=[])
api.produce(target_repos)


def process():
rhui_config = api.current_actor().config[rhui_config_lib.RHUI_CONFIG_SECTION]

if rhui_config[RhuiUseConfig.name]:
api.current_logger().info('Skipping RHUI upgrade auto-configuration - using provided config instead.')
emit_rhui_setup_tasks_based_on_config(rhui_config)

src_clients = set(rhui_config[RhuiSourcePkgs.name])
target_clients = set(rhui_config[RhuiTargetPkgs.name])
produce_rpms_to_install_into_target(src_clients, target_clients)

request_configured_repos_to_be_enabled(rhui_config)
return

installed_rpm = itertools.chain(*[installed_rpm_msg.items for installed_rpm_msg in api.consume(InstalledRPM)])
installed_pkgs = {rpm.name for rpm in installed_rpm}

Expand All @@ -342,7 +397,9 @@ def process():
# Instruction on how to access the target content
produce_rhui_info_to_setup_target(src_rhui_setup.family, src_rhui_setup.description, target_setup_desc)

produce_rpms_to_install_into_target(src_rhui_setup.description, target_setup_desc)
source_clients = src_rhui_setup.description.clients
target_clients = target_setup_desc.clients
produce_rpms_to_install_into_target(source_clients, target_clients)

if src_rhui_setup.family.provider == rhui.RHUIProvider.AWS:
# We have to disable Amazon-id plugin in the initramdisk phase as there is no network
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from leapp.actors import Actor
from leapp.configs.actor.rpm import Transaction_ToInstall, Transaction_ToKeep, Transaction_ToRemove
from leapp.libraries.actor.rpmtransactionconfigtaskscollector import load_tasks
from leapp.models import DistributionSignedRPM, RpmTransactionTasks
from leapp.tags import FactsPhaseTag, IPUWorkflowTag

CONFIGURATION_BASE_PATH = '/etc/leapp/transaction'


class RpmTransactionConfigTasksCollector(Actor):
"""
Expand All @@ -13,11 +12,11 @@ class RpmTransactionConfigTasksCollector(Actor):
After collecting task data from /etc/leapp/transaction directory, a message with relevant data
will be produced.
"""

config_schemas = (Transaction_ToInstall, Transaction_ToKeep, Transaction_ToRemove)
name = 'rpm_transaction_config_tasks_collector'
consumes = (DistributionSignedRPM,)
produces = (RpmTransactionTasks,)
tags = (FactsPhaseTag, IPUWorkflowTag)

def process(self):
self.produce(load_tasks(CONFIGURATION_BASE_PATH, self.log))
self.produce(load_tasks(self.config, self.log))
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""
Configuration keys for dnf transactions.
"""

from leapp.actors.config import Config
from leapp.models import fields


# * Nested containers?
# * Duplication of default value in type_ and Config. If we eliminate that, we need to extract
# default from the type_ for the documentation.
# * We probably want to allow dicts in Config. But IIRC, dicts were
# specifically excluded for model fields. Do we need something that restricts
# where fields are valid?
# * Test that type validation is strict. For instance, giving an integer like 644 to
# a field.String() is an error.
class Transaction_ToInstall(Config):
section = "transaction"
name = "to_install"
type_ = fields.List(fields.String(), default=[])
default = []
description = """
List of packages to be added to the upgrade transaction.
Signed packages which are already installed will be skipped.
"""


class Transaction_ToKeep(Config):
section = "transaction"
name = "to_keep"
type_ = fields.List(fields.String(), default=[
"leapp",
"python2-leapp",
"python3-leapp",
"leapp-repository",
"snactor",
])
default = [
"leapp",
"python2-leapp",
"python3-leapp",
"leapp-repository",
"snactor",
]
description = """
List of packages to be kept in the upgrade transaction. The default is
leapp, python2-leapp, python3-leapp, leapp-repository, snactor. If you
override this, remember to include the default values if applicable.
"""


class Transaction_ToRemove(Config):
section = "transaction"
name = "to_remove"
type_ = fields.List(fields.String(), default=[
"initial-setup",
])
default = ["initial-setup"]
description = """
List of packages to be removed from the upgrade transaction. The default
is initial-setup which should be removed to avoid it asking for EULA
acceptance during upgrade. If you override this, remember to include the
default values if applicable.
"""
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
from leapp.libraries.stdlib import api
from leapp.models import DistributionSignedRPM, RpmTransactionTasks

# Deprecated. This is the old, pre-actor config method of customizing which
# packages to keep, remove, and install.
_CONFIGURATION_BASE_PATH = '/etc/leapp/transaction'


def load_tasks_file(path, logger):
# Loads the given file and converts it to a deduplicated list of strings that are stripped
Expand All @@ -18,21 +22,35 @@ def load_tasks_file(path, logger):
return []


def load_tasks(base_dir, logger):
def load_tasks(config, logger, base_dir=_CONFIGURATION_BASE_PATH):
# Loads configuration files to_install, to_keep, and to_remove from the given base directory
rpms = next(api.consume(DistributionSignedRPM))
rpm_names = [rpm.name for rpm in rpms.items]
to_install = load_tasks_file(os.path.join(base_dir, 'to_install'), logger)
# we do not want to put into rpm transaction what is already installed (it will go to "to_upgrade" bucket)
to_keep = frozenset(config['transaction']['to_keep'])
to_keep = to_keep.union(load_tasks_file(
os.path.join(base_dir, 'to_keep'), logger))
to_keep = list(to_keep)

to_remove = frozenset(config['transaction']['to_remove'])
to_remove = to_remove.union(load_tasks_file(
os.path.join(base_dir, 'to_remove'), logger))
to_remove = list(to_remove)

to_install = frozenset(config['transaction']['to_install'])
to_install = to_install.union(load_tasks_file(
os.path.join(base_dir, 'to_install'), logger))
# we do not want to put into rpm transaction what is already installed
# (it will go to "to_upgrade" bucket)
to_install_filtered = [pkg for pkg in to_install if pkg not in rpm_names]

filtered = set(to_install) - set(to_install_filtered)
filtered = to_install.difference(to_install_filtered)
if filtered:
api.current_logger().debug(
'The following packages from "to_install" file will be ignored as they are already installed:'
'\n- ' + '\n- '.join(filtered))
'The following packages from "to_install" file will be ignored as'
' they are already installed:\n- ' + '\n- '.join(filtered))

return RpmTransactionTasks(
to_install=to_install_filtered,
to_keep=load_tasks_file(os.path.join(base_dir, 'to_keep'), logger),
to_remove=load_tasks_file(os.path.join(base_dir, 'to_remove'), logger))
to_keep=to_keep,
to_remove=to_remove
)
Loading
Loading