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 all 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 : False
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/
rhui_target_repositories_to_use:
- 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
4 changes: 4 additions & 0 deletions 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,6 +9,7 @@
RequiredTargetUserspacePackages,
RHUIInfo,
RpmTransactionTasks,
TargetRepositories,
TargetUserSpacePreupgradeTasks
)
from leapp.reporting import Report
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 @@ -2,17 +2,29 @@
import os
from collections import namedtuple

import leapp.configs.common.rhui as rhui_config_lib
from leapp import reporting
from leapp.configs.common.rhui import ( # Import all config fields so we are not using their name attributes directly
RhuiCloudProvider,
RhuiCloudVariant,
RhuiSourcePkgs,
RhuiTargetPkgs,
RhuiTargetRepositoriesToUse,
RhuiUpgradeFiles,
RhuiUseConfig
)
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common import rhsm, rhui
from leapp.libraries.common.config import version
from leapp.libraries.stdlib import api
from leapp.models import (
CopyFile,
CustomTargetRepository,
DNFPluginTask,
InstalledRPM,
RHUIInfo,
RpmTransactionTasks,
TargetRepositories,
TargetRHUIPostInstallTasks,
TargetRHUIPreInstallTasks,
TargetRHUISetupInfo,
Expand Down Expand Up @@ -291,11 +303,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 +328,85 @@ 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]

nonexisting_files_to_copy = []
for source_path in config_upgrade_files:
if not os.path.exists(source_path):
nonexisting_files_to_copy.append(source_path)

if nonexisting_files_to_copy:
details_lines = ['The following files were not found:']
# Use .format and put backticks around paths so that weird unicode spaces will be easily seen
details_lines.extend(' - `{0}`'.format(path) for path in nonexisting_files_to_copy)
details = '\n'.join(details_lines)

reason = 'RHUI config lists nonexisting files in its `{0}` field.'.format(RhuiUpgradeFiles.name)
raise StopActorExecutionError(reason, details={'details': details})

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=rhui_config_dict[RhuiSourcePkgs.name],
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[RhuiTargetRepositoriesToUse.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 stop_with_err_if_config_missing_fields(config):
required_fields = [
RhuiTargetRepositoriesToUse,
RhuiCloudProvider,
# RhuiCloudVariant, <- this is not required
RhuiSourcePkgs,
RhuiTargetPkgs,
RhuiUpgradeFiles,
]

missing_fields = tuple(field for field in required_fields if not config[field.name])
if missing_fields:
field_names = (field.name for field in missing_fields)
missing_fields_str = ', '.join(field_names)
details = 'The following required RHUI config fields are missing or they are set to an empty value: {}'
details = details.format(missing_fields_str)
raise StopActorExecutionError('Provided RHUI config is missing values for required fields.',
details={'details': details})


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.')
stop_with_err_if_config_missing_fields(rhui_config)
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 +432,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
Loading