Update ELevate patch
This commit is contained in:
parent
7de04ec94d
commit
9a1e34c143
@ -1,5 +1,5 @@
|
||||
diff --git a/README.md b/README.md
|
||||
index 4de458b..8937111 100644
|
||||
index 4de458b..9975f06 100644
|
||||
--- a/README.md
|
||||
+++ b/README.md
|
||||
@@ -1,7 +1,61 @@
|
||||
@ -67,7 +67,7 @@ index 4de458b..8937111 100644
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -11,6 +65,13 @@
|
||||
@@ -11,6 +65,15 @@
|
||||
- Leapp framework: [https://github.com/oamg/leapp/issues/new/choose](https://github.com/oamg/leapp/issues/new/choose)
|
||||
- Leapp actors: [https://github.com/oamg/leapp-repository/issues/new/choose](https://github.com/oamg/leapp-repository/issues/new/choose)
|
||||
|
||||
@ -77,11 +77,13 @@ index 4de458b..8937111 100644
|
||||
+ - Leapp data: [https://github.com/AlmaLinux/leapp-data/issues/new/choose](https://github.com/AlmaLinux/leapp-data/issues/new/choose)
|
||||
+
|
||||
+### What data should be provided when making a report?
|
||||
+
|
||||
+Before gathering data, if possible, run the *leapp* command that encountered an issue with the `--debug` flag, e.g.: `leapp upgrade --debug`.
|
||||
+
|
||||
- When filing an issue, include:
|
||||
- Steps to reproduce the issue
|
||||
- *All files in /var/log/leapp*
|
||||
@@ -25,7 +86,232 @@
|
||||
@@ -25,7 +88,613 @@
|
||||
Then you may attach only the `leapp-logs.tgz` file.
|
||||
|
||||
### Where can I seek help?
|
||||
@ -266,57 +268,438 @@ index 4de458b..8937111 100644
|
||||
+Changes you want to submit upstream should be sent through pull requests to repositories https://github.com/AlmaLinux/leapp-repository and https://github.com/AlmaLinux/leapp-data.
|
||||
+The standard GitHub contribution process applies - fork the repository, make your changes inside of it, then submit the pull request to be reviewed.
|
||||
+
|
||||
+### Example
|
||||
+Suppose you would like to create an actor to copy a pre-supplied repository file into the upgraded system’s /etc/yum.repos.d.
|
||||
+### Custom actor example
|
||||
+
|
||||
+"Actors" in Leapp terminology are Python scripts that run during the upgrade process.
|
||||
+Actors are a core concept of the framework, and the entire process is built from them.
|
||||
+
|
||||
+Custom actors are the actors that are added by third-party developers, and are not present in the upstream Leapp repository.
|
||||
+
|
||||
+Actors can gather data, communicate with each other and modify the system during the upgrade.
|
||||
+
|
||||
+Let's examine how an upgrade problem might be resolved with a custom actor.
|
||||
+
|
||||
+#### Problem
|
||||
+
|
||||
+If you ever ran `leapp preupgrade` on unprepared systems before, you likely have seen the following message:
|
||||
+
|
||||
+First, you would create the new actor:
|
||||
+```
|
||||
+cd ./leapp-repository/repos/system_upgrade/common
|
||||
+snactor new-actor addcustomrepositories
|
||||
+Upgrade has been inhibited due to the following problems:
|
||||
+ 1. Inhibitor: Possible problems with remote login using root account
|
||||
+```
|
||||
+
|
||||
+Since you'd want to run this actor after the upgrade has successfully completed, you’d use the FirstBootPhase tag in its tags field.
|
||||
+It's caused by the change in default behaviour for permitting root logins between RHEL 7 and 8.
|
||||
+In RHEL 8 logging in as root via password authentication is no longer allowed by default, which means that some machines can become inaccessible after the upgrade.
|
||||
+
|
||||
+Some configurations require an administrator's intervention to resolve this issue, but SSHD configurations where no `PermitRootLogin` options were explicitly set can be modified to preserve the RHEL 7 default behaviour and not require manual modification.
|
||||
+
|
||||
+Let's create a custom actor to handle such cases for us.
|
||||
+
|
||||
+#### Creating an actor
|
||||
+
|
||||
+Actors are contained in ["repositories"](https://leapp.readthedocs.io/en/latest/leapp-repositories.html) - subfolders containing compartmentalized code and resources that the Leapp framework will use during the upgrade.
|
||||
+
|
||||
+> Do not confuse Leapp repositories with Git repositories - these are two different concepts, independent of one another.
|
||||
+
|
||||
+Inside the `leapp-repository` GitHub repo, Leapp repositories are contained inside the `repos` subfolder.
|
||||
+
|
||||
+Everything related to system upgrade proper is inside the `system_upgrade` folder.
|
||||
+`el7toel8` contains resources used when upgrading from RHEL 7 to RHEL 8, `el8toel9` - RHEL 8 to 9, `common` - shared resources.
|
||||
+
|
||||
+Since the change in system behaviour we're looking to mitigate occurs between RHEL 7 and 8, the appopriate repository to place the actor in is `el7toel8`.
|
||||
+
|
||||
+You can [create new actors](https://leapp.readthedocs.io/en/latest/first-actor.html) by using the `snactor` tool provided by Leapp, or manually.
|
||||
+
|
||||
+`snactor new-actor ACTOR_NAME`
|
||||
+
|
||||
+The bare-bones actor code consists of a file named `actor.py` contained inside the `actors/<actor_name>` subfolder of a Leapp repository.
|
||||
+
|
||||
+In this case, then, it should be located in a directory like `leapp-repository/repos/system_upgrade/el7toel8/actors/opensshmodifypermitroot`
|
||||
+
|
||||
+If you used snactor to create it, you'll see contents like the following:
|
||||
+
|
||||
+```python
|
||||
+ tags = (IPUWorkflowTag, FirstBootPhaseTag)
|
||||
+```
|
||||
+from leapp.actors import Actor
|
||||
+
|
||||
+If you wanted to ensure that the actor only runs on AlmaLinux systems, and not on any target OS variant, you could check the release name by importing the corresponding function from leapp API:
|
||||
+
|
||||
+```python
|
||||
+from leapp.libraries.common.config import version
|
||||
+class OpenSSHModifyPermitRoot(Actor):
|
||||
+ """
|
||||
+ No documentation has been provided for the open_ssh_actor_example actor.
|
||||
+ """
|
||||
+
|
||||
+ name = 'openssh_modify_permit_root'
|
||||
+ consumes = ()
|
||||
+ produces = ()
|
||||
+ tags = ()
|
||||
+
|
||||
+ def process(self):
|
||||
+ # We only want to run this actor on AlmaLinux systems.
|
||||
+ # current_version returns a tuple (release_name, version_value).
|
||||
+ if (version.current_version()[0] == "almalinux"):
|
||||
+ copy_custom_repo_files()
|
||||
+ pass
|
||||
+```
|
||||
+
|
||||
+Files that are accessible by all actors in the repository should be placed into the files/ subdirectory. For the main leapp repository, that directory has the path leapp-repository/repos/system_upgrade/common/files.
|
||||
+#### Configuring the actor
|
||||
+
|
||||
+Create the subfolder `custom-repos` to place repo files into, then finalize the actor’s code:
|
||||
+Actors' `consumes` and `produces` attributes define types of [*messages*](https://leapp.readthedocs.io/en/latest/messaging.html) these actors receive or send.
|
||||
+
|
||||
+For instance, during the initial upgrade stages several standard actors gather system information and *produce* messages with gathered data to other actors.
|
||||
+
|
||||
+> Messages are defined by *message models*, which are contained inside Leapp repository's `models` subfolder, just like all actors are contained in `actors`.
|
||||
+
|
||||
+Actors' `tags` attributes define the [phase of the upgrade](https://leapp.readthedocs.io/en/latest/working-with-workflows.html) during which that actor gets executed.
|
||||
+
|
||||
+> The list of all phases can be found in file `leapp-repository/repos/system_upgrade/common/workflows/inplace_upgrade.py`.
|
||||
+
|
||||
+##### Receiving messages
|
||||
+
|
||||
+Leapp already provides information about the OpenSSH configuration through the `OpenSshConfigScanner` actor. This actor provides a message with a message model `OpenSshConfig`.
|
||||
+
|
||||
+Instead of opening and reading the configuration file in our own actor, we can simply read the provided message to see if we can safely alter the configuration automatically.
|
||||
+
|
||||
+To begin with, import the message model from `leapp.models`:
|
||||
+
|
||||
+```python
|
||||
+import os
|
||||
+import os.path
|
||||
+import shutil
|
||||
+from leapp.models import OpenSshConfig
|
||||
+```
|
||||
+
|
||||
+> It doesn't matter in which Leapp repository the model is located. Leapp will gather all availabile data inside its submodules.
|
||||
+
|
||||
+Add the message model to the list of messages to be received:
|
||||
+
|
||||
+```python
|
||||
+consumes = (OpenSshConfig, )
|
||||
+```
|
||||
+
|
||||
+The actor now will be able to read messages of this format provided by other actors that were executed prior to its own execution.
|
||||
+
|
||||
+##### Sending messages
|
||||
+
|
||||
+To ensure that the user knows about the automatic configuration change that will occur, we can send a *report*.
|
||||
+
|
||||
+> Reports are a built-in type of Leapp messages that are added to the `/var/log/leapp/leapp-report.txt` file at the end of the upgrade process.
|
||||
+
|
||||
+To start off with, add a Report message model to the `produces` attribute of the actor.
|
||||
+
|
||||
+```python
|
||||
+produces = (Report, )
|
||||
+```
|
||||
+
|
||||
+Don't forget to import the model type from `leapp.models`.
|
||||
+
|
||||
+All done - now we're ready to make use of the models inside the actor's code.
|
||||
+
|
||||
+
|
||||
+##### Running phase
|
||||
+
|
||||
+Both workflow and phase tags are imported from leapp.tags:
|
||||
+
|
||||
+```python
|
||||
+from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
|
||||
+```
|
||||
+
|
||||
+All actors to be run during the upgrade must contain the upgrade workflow tag. It looks as follows:
|
||||
+
|
||||
+```python
|
||||
+tags = (IPUWorkflowTag, )
|
||||
+```
|
||||
+
|
||||
+To define the upgrade phase during which an actor will run, set the appropriate tag in the `tags` attribute.
|
||||
+
|
||||
+Standard actor `OpenSshPermitRootLoginCheck` that blocks the upgrade if it detects potential problems in SSH configuration, runs during the *checks* phase, and has the `ChecksPhaseTag` inside its `tags`.
|
||||
+
|
||||
+Therefore, we want to run our new actor before it. We can select an earlier phase from the list of phases - or we can mark our actor to run *before other actors* in the phase with a modifier as follows:
|
||||
+
|
||||
+```python
|
||||
+tags = (ChecksPhaseTag.Before, IPUWorkflowTag, )
|
||||
+```
|
||||
+
|
||||
+All phases have built-in `.Before` and `.After` stages that can be used this way. Now our actor is guaranteed to be run before the `OpenSshPermitRootLoginCheck` actor.
|
||||
+
|
||||
+
|
||||
+#### Actor code
|
||||
+
|
||||
+With configuration done, it's time to write the actual code of the actor that will be executed during the upgrade.
|
||||
+
|
||||
+The entry point for it is the actor's `process` function.
|
||||
+
|
||||
+First, let's start by reading the SSH config message we've set the actor to receive.
|
||||
+
|
||||
+```python
|
||||
+# Importing from Leapp built-ins.
|
||||
+from leapp.exceptions import StopActorExecutionError
|
||||
+from leapp.libraries.stdlib import api
|
||||
+
|
||||
+CUSTOM_REPOS_FOLDER = 'custom-repos'
|
||||
+REPO_ROOT_PATH = "/etc/yum.repos.d"
|
||||
+def process(self):
|
||||
+ # Retreive the OpenSshConfig message.
|
||||
+
|
||||
+def copy_custom_repo_files():
|
||||
+ custom_repo_dir = api.get_common_folder_path(CUSTOM_REPOS_FOLDER)
|
||||
+ # Actors have `consume` and `produce` methods that work with messages.
|
||||
+ # `consume` expects a message type that is listed inside the `consumes` attribute.
|
||||
+ openssh_messages = self.consume(OpenSshConfig)
|
||||
+
|
||||
+ for repofile in os.listdir(custom_repo_dir):
|
||||
+ full_repo_path = os.path.join(custom_repo_dir, repofile)
|
||||
+ shutil.copy(full_repo_path, REPO_ROOT_PATH)
|
||||
+ # The return value of self.consume is a generator of messages of the provided type.
|
||||
+ config = next(openssh_messages, None)
|
||||
+ # We expect to get only one message of this type. If there's more than one, something's wrong.
|
||||
+ if list(openssh_messages):
|
||||
+ # api.current_logger lets you pass messages into Leapp's log. By default, they will
|
||||
+ # be displayed in `/var/log/leapp/leapp-preupgrade.log`
|
||||
+ # or `/var/log/leapp/leapp-upgrade.log`, depending on which command you ran.
|
||||
+ api.current_logger().warning('Unexpectedly received more than one OpenSshConfig message.')
|
||||
+ # If the config message is not present, the standard actor failed to read it.
|
||||
+ # Stop here.
|
||||
+ if not config:
|
||||
+ # StopActorExecutionError is a Leapp built-in exception type that halts the actor execution.
|
||||
+ # By default this will also halt the upgrade phase and the upgrade process in general.
|
||||
+ raise StopActorExecutionError(
|
||||
+ 'Could not check openssh configuration', details={'details': 'No OpenSshConfig facts found.'}
|
||||
+ )
|
||||
+```
|
||||
+
|
||||
+Next, let's read the received message and see if we can modify the configuration.
|
||||
+
|
||||
+```python
|
||||
+import errno
|
||||
+
|
||||
+CONFIG = '/etc/ssh/sshd_config'
|
||||
+CONFIG_BACKUP = '/etc/ssh/sshd_config.leapp_backup'
|
||||
+
|
||||
+ # The OpenSshConfig model has a permit_root_login attribute that contains
|
||||
+ # all instances of PermitRootLogin option present in the config.
|
||||
+ # See leapp-repository/repos/system_upgrade/el7toel8/models/opensshconfig.py
|
||||
+
|
||||
+ # We can only safely modify the config to preserve the default behaviour if no
|
||||
+ # explicit PermitRootLogin option was set anywhere in the config.
|
||||
+ if not config.permit_root_login:
|
||||
+ try:
|
||||
+ # Read the config into memory to prepare for its modification.
|
||||
+ with open(CONFIG, 'r') as fd:
|
||||
+ sshd_config = fd.readlines()
|
||||
+
|
||||
+ # These are the lines we want to add to the configuration file.
|
||||
+ permit_autoconf = [
|
||||
+ "# Automatically added by Leapp to preserve RHEL7 default\n",
|
||||
+ "# behaviour after migration.\n",
|
||||
+ "# Placed on top of the file to avoid being included into Match blocks.\n",
|
||||
+ "PermitRootLogin yes\n"
|
||||
+ "\n",
|
||||
+ ]
|
||||
+ permit_autoconf.extend(sshd_config)
|
||||
+ # Write the changed config into the file.
|
||||
+ with open(CONFIG, 'w') as fd:
|
||||
+ fd.writelines(permit_autoconf)
|
||||
+ # Write the backup file with the old configuration.
|
||||
+ with open(CONFIG_BACKUP, 'w') as fd:
|
||||
+ fd.writelines(sshd_config)
|
||||
+
|
||||
+ # Handle errors.
|
||||
+ except IOError as err:
|
||||
+ if err.errno != errno.ENOENT:
|
||||
+ error = 'Failed to open sshd_config: {}'.format(str(err))
|
||||
+ api.current_logger().error(error)
|
||||
+ return
|
||||
+```
|
||||
+
|
||||
+The functional part of the actor itself is done. Now, let's add a report to let the user know
|
||||
+the machine's SSH configuration has changed.
|
||||
+
|
||||
+```python
|
||||
+# These Leapp imports are required to create reports.
|
||||
+from leapp import reporting
|
||||
+from leapp.models import Report
|
||||
+from leapp.reporting import create_report
|
||||
+
|
||||
+# Tags signify the categories the report and the associated issue are related to.
|
||||
+COMMON_REPORT_TAGS = [
|
||||
+ reporting.Tags.AUTHENTICATION,
|
||||
+ reporting.Tags.SECURITY,
|
||||
+ reporting.Tags.NETWORK,
|
||||
+ reporting.Tags.SERVICES
|
||||
+]
|
||||
+
|
||||
+ # Related resources are listed in the report to help resolving the issue.
|
||||
+ resources = [
|
||||
+ reporting.RelatedResource('package', 'openssh-server'),
|
||||
+ reporting.RelatedResource('file', '/etc/ssh/sshd_config')
|
||||
+ reporting.RelatedResource('file', '/etc/ssh/sshd_config.leapp_backup')
|
||||
+ ]
|
||||
+ # This function creates and submits the actual report message.
|
||||
+ # Normally you'd need to call self.produce() to send messages,
|
||||
+ # but reports are a special case that gets handled automatically.
|
||||
+ create_report([
|
||||
+ # Report title and summary.
|
||||
+ reporting.Title('SSH configuration automatically modified to permit root login'),
|
||||
+ reporting.Summary(
|
||||
+ 'Your OpenSSH configuration file does not explicitly state '
|
||||
+ 'the option PermitRootLogin in sshd_config file. '
|
||||
+ 'Its default is "yes" in RHEL7, but will change in '
|
||||
+ 'RHEL8 to "prohibit-password", which may affect your ability '
|
||||
+ 'to log onto this machine after the upgrade. '
|
||||
+ 'To prevent this from occuring, the PermitRootLogin option '
|
||||
+ 'has been explicity set to "yes" to preserve the default behaivour '
|
||||
+ 'after migration.'
|
||||
+ 'The original configuration file has been backed up to'
|
||||
+ '/etc/ssh/sshd_config.leapp_backup'
|
||||
+ ),
|
||||
+ # Reports are ordered by severity in the list.
|
||||
+ reporting.Severity(reporting.Severity.MEDIUM),
|
||||
+ reporting.Tags(COMMON_REPORT_TAGS),
|
||||
+ # Remediation section contains hints on how to resolve the reported (potential) problem.
|
||||
+ reporting.Remediation(
|
||||
+ hint='If you would prefer to configure the root login policy yourself, '
|
||||
+ 'consider setting the PermitRootLogin option '
|
||||
+ 'in sshd_config explicitly.'
|
||||
+ )
|
||||
+ ] + resources) # Resources are added to the list of data for the report.
|
||||
+```
|
||||
+
|
||||
+The actor code is now complete. The final version with less verbose comments will look something like this:
|
||||
+
|
||||
+```python
|
||||
+from leapp import reporting
|
||||
+from leapp.actors import Actor
|
||||
+from leapp.exceptions import StopActorExecutionError
|
||||
+from leapp.libraries.stdlib import api
|
||||
+from leapp.models import OpenSshConfig, Report
|
||||
+from leapp.reporting import create_report
|
||||
+from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
|
||||
+
|
||||
+import errno
|
||||
+
|
||||
+CONFIG = '/etc/ssh/sshd_config'
|
||||
+CONFIG_BACKUP = '/etc/ssh/sshd_config.leapp_backup'
|
||||
+
|
||||
+COMMON_REPORT_TAGS = [
|
||||
+ reporting.Tags.AUTHENTICATION,
|
||||
+ reporting.Tags.SECURITY,
|
||||
+ reporting.Tags.NETWORK,
|
||||
+ reporting.Tags.SERVICES
|
||||
+]
|
||||
+
|
||||
+
|
||||
+class OpenSSHModifyPermitRoot(Actor):
|
||||
+ """
|
||||
+ OpenSSH doesn't allow root logins with password by default on RHEL8.
|
||||
+
|
||||
+ Check the values of PermitRootLogin in OpenSSH server configuration file
|
||||
+ and see if it was set explicitly.
|
||||
+ If not, adding an explicit "PermitRootLogin yes" will preserve the current
|
||||
+ default behaviour.
|
||||
+ """
|
||||
+
|
||||
+ name = 'openssh_modify_permit_root'
|
||||
+ consumes = (OpenSshConfig, )
|
||||
+ produces = (Report, )
|
||||
+ tags = (ChecksPhaseTag.Before, IPUWorkflowTag, )
|
||||
+
|
||||
+ def process(self):
|
||||
+ # Retreive the OpenSshConfig message.
|
||||
+ openssh_messages = self.consume(OpenSshConfig)
|
||||
+ config = next(openssh_messages, None)
|
||||
+ if list(openssh_messages):
|
||||
+ api.current_logger().warning('Unexpectedly received more than one OpenSshConfig message.')
|
||||
+ if not config:
|
||||
+ raise StopActorExecutionError(
|
||||
+ 'Could not check openssh configuration', details={'details': 'No OpenSshConfig facts found.'}
|
||||
+ )
|
||||
+
|
||||
+ # Read and modify the config.
|
||||
+ # Only act if there's no explicit PermitRootLogin option set anywhere in the config.
|
||||
+ if not config.permit_root_login:
|
||||
+ try:
|
||||
+ with open(CONFIG, 'r') as fd:
|
||||
+ sshd_config = fd.readlines()
|
||||
+
|
||||
+ permit_autoconf = [
|
||||
+ "# Automatically added by Leapp to preserve RHEL7 default\n",
|
||||
+ "# behaviour after migration.\n",
|
||||
+ "# Placed on top of the file to avoid being included into Match blocks.\n",
|
||||
+ "PermitRootLogin yes\n"
|
||||
+ "\n",
|
||||
+ ]
|
||||
+ permit_autoconf.extend(sshd_config)
|
||||
+ with open(CONFIG, 'w') as fd:
|
||||
+ fd.writelines(permit_autoconf)
|
||||
+ with open(CONFIG_BACKUP, 'w') as fd:
|
||||
+ fd.writelines(sshd_config)
|
||||
+
|
||||
+ except IOError as err:
|
||||
+ if err.errno != errno.ENOENT:
|
||||
+ error = 'Failed to open sshd_config: {}'.format(str(err))
|
||||
+ api.current_logger().error(error)
|
||||
+ return
|
||||
+
|
||||
+ # Create a report letting the user know what happened.
|
||||
+ resources = [
|
||||
+ reporting.RelatedResource('package', 'openssh-server'),
|
||||
+ reporting.RelatedResource('file', '/etc/ssh/sshd_config'),
|
||||
+ reporting.RelatedResource('file', '/etc/ssh/sshd_config.leapp_backup')
|
||||
+ ]
|
||||
+ create_report([
|
||||
+ reporting.Title('SSH configuration automatically modified to permit root login'),
|
||||
+ reporting.Summary(
|
||||
+ 'Your OpenSSH configuration file does not explicitly state '
|
||||
+ 'the option PermitRootLogin in sshd_config file. '
|
||||
+ 'Its default is "yes" in RHEL7, but will change in '
|
||||
+ 'RHEL8 to "prohibit-password", which may affect your ability '
|
||||
+ 'to log onto this machine after the upgrade. '
|
||||
+ 'To prevent this from occuring, the PermitRootLogin option '
|
||||
+ 'has been explicity set to "yes" to preserve the default behaivour '
|
||||
+ 'after migration.'
|
||||
+ 'The original configuration file has been backed up to'
|
||||
+ '/etc/ssh/sshd_config.leapp_backup'
|
||||
+ ),
|
||||
+ reporting.Severity(reporting.Severity.MEDIUM),
|
||||
+ reporting.Tags(COMMON_REPORT_TAGS),
|
||||
+ reporting.Remediation(
|
||||
+ hint='If you would prefer to configure the root login policy yourself, '
|
||||
+ 'consider setting the PermitRootLogin option '
|
||||
+ 'in sshd_config explicitly.'
|
||||
+ )
|
||||
+ ] + resources)
|
||||
+```
|
||||
+
|
||||
+Due to this actor's small size, the entire code can be fit inside the `process` function.
|
||||
+If it grows beyond manageable size, or you want to run unit tests on its components, it's advised to move out all of the functional parts from the `process` function into the *actor library*.
|
||||
+
|
||||
+#### Libraries
|
||||
+
|
||||
+Larger actors can import code from [common libraries](https://leapp.readthedocs.io/en/latest/best-practices.html#move-generic-functionality-to-libraries) or define their own "libraries" and run code from them inside the `process` function.
|
||||
+
|
||||
+In such cases, the directory layout looks like this:
|
||||
+```
|
||||
+actors
|
||||
++ example_actor_name
|
||||
+| + libraries
|
||||
+| + example_actor_name.py
|
||||
+| + actor.py
|
||||
+...
|
||||
+```
|
||||
+
|
||||
+and importing code from them looks like this:
|
||||
+
|
||||
+`from leapp.libraries.actor.example_actor_name import example_lib_function`
|
||||
+
|
||||
+This is also the main way of [writing unit-testable code](https://leapp.readthedocs.io/en/latest/best-practices.html#write-unit-testable-code), since the code contained inside the `process` function cannot be unit-tested normally.
|
||||
+
|
||||
+In this actor format, you would move all of the actual actor code into the associated library, leaving only preparation and function calls inside the `process` function.
|
||||
+
|
||||
+#### Debugging
|
||||
+
|
||||
+The Leapp utility `snactor` can also be used for unit-testing the created actors.
|
||||
+
|
||||
+It is capable of saving the output of actors as locally stored messages, so that they can be consumed by other actors that are being developed.
|
||||
+
|
||||
+For example, to test our new actor, we need the OpenSshConfig message, which is produced by the OpenSshConfigScanner standard actor. To make the data consumable, run the actor producing the data with the –save-output option:
|
||||
+
|
||||
+`snactor run --save-output OpenSshConfigScanner`
|
||||
+
|
||||
+The output of the actor is stored in the local repository data file, and it can be used by other actors. To flush all saved messages from the repository database, run `snactor messages clear`.
|
||||
+
|
||||
+With the input messages available and stored, the actor being developed can be tested.
|
||||
+
|
||||
+`snactor run --print-output OpenSshModifyPermitRoot`
|
||||
+
|
||||
+#### Additional information
|
||||
|
||||
-You can reach us at IRC: `#leapp` on freenode.
|
||||
+Refer to existing actors and [Leapp documentation](https://leapp.readthedocs.io/) to use more complex functionality in your actors.
|
||||
+For more information about Leapp and additional tutorials, visit the [official Leapp documentation](https://leapp.readthedocs.io/en/latest/tutorials.html).
|
||||
diff --git a/commands/command_utils.py b/commands/command_utils.py
|
||||
index da62c50..a8e7d76 100644
|
||||
--- a/commands/command_utils.py
|
||||
@ -330,6 +713,128 @@ index da62c50..a8e7d76 100644
|
||||
|
||||
|
||||
def check_version(version):
|
||||
diff --git a/commands/upgrade/__init__.py b/commands/upgrade/__init__.py
|
||||
index c9c2741..7646459 100644
|
||||
--- a/commands/upgrade/__init__.py
|
||||
+++ b/commands/upgrade/__init__.py
|
||||
@@ -86,7 +86,7 @@ def upgrade(args, breadcrumbs):
|
||||
workflow = repositories.lookup_workflow('IPUWorkflow')(auto_reboot=args.reboot)
|
||||
util.process_whitelist_experimental(repositories, workflow, configuration, logger)
|
||||
util.warn_if_unsupported(configuration)
|
||||
- with beautify_actor_exception():
|
||||
+ with util.format_actor_exceptions(logger):
|
||||
logger.info("Using answerfile at %s", answerfile_path)
|
||||
workflow.load_answers(answerfile_path, userchoices_path)
|
||||
|
||||
@@ -99,13 +99,16 @@ def upgrade(args, breadcrumbs):
|
||||
logger.info("Answerfile will be created at %s", answerfile_path)
|
||||
workflow.save_answers(answerfile_path, userchoices_path)
|
||||
report_errors(workflow.errors)
|
||||
+ util.log_errors(workflow.errors, logger)
|
||||
report_inhibitors(context)
|
||||
+ util.log_inhibitors(context, logger)
|
||||
util.generate_report_files(context, report_schema)
|
||||
report_files = util.get_cfg_files('report', cfg)
|
||||
log_files = util.get_cfg_files('logs', cfg)
|
||||
report_info(report_files, log_files, answerfile_path, fail=workflow.failure)
|
||||
|
||||
if workflow.failure:
|
||||
+ logger.error("Upgrade workflow failed, check log for details")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
diff --git a/commands/upgrade/util.py b/commands/upgrade/util.py
|
||||
index 75ffa6a..379d480 100644
|
||||
--- a/commands/upgrade/util.py
|
||||
+++ b/commands/upgrade/util.py
|
||||
@@ -2,18 +2,23 @@ import functools
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
+import sys
|
||||
import shutil
|
||||
import tarfile
|
||||
from datetime import datetime
|
||||
+from contextlib import contextmanager
|
||||
|
||||
from leapp.cli.commands import command_utils
|
||||
from leapp.cli.commands.config import get_config
|
||||
-from leapp.exceptions import CommandError
|
||||
+from leapp.exceptions import CommandError, LeappRuntimeError
|
||||
from leapp.repository.scan import find_and_scan_repositories
|
||||
from leapp.utils import audit
|
||||
from leapp.utils.audit import get_checkpoints, get_connection, get_messages
|
||||
-from leapp.utils.output import report_unsupported
|
||||
+from leapp.utils.output import report_unsupported, pretty_block_text
|
||||
from leapp.utils.report import fetch_upgrade_report_messages, generate_report_file
|
||||
+from leapp.models import ErrorModel
|
||||
+
|
||||
+
|
||||
|
||||
|
||||
def disable_database_sync():
|
||||
@@ -236,3 +241,61 @@ def process_report_schema(args, configuration):
|
||||
raise CommandError('--report-schema version can not be greater that the '
|
||||
'actual {} one.'.format(default_report_schema))
|
||||
return args.report_schema or default_report_schema
|
||||
+
|
||||
+
|
||||
+# TODO: This and the following functions should eventually be placed into the
|
||||
+# leapp.utils.output module.
|
||||
+def pretty_block_log(string, logger_level, width=60):
|
||||
+ log_str = "\n{separator}\n{text}\n{separator}\n".format(
|
||||
+ separator="=" * width,
|
||||
+ text=string.center(width))
|
||||
+ logger_level(log_str)
|
||||
+
|
||||
+
|
||||
+@contextmanager
|
||||
+def format_actor_exceptions(logger):
|
||||
+ try:
|
||||
+ try:
|
||||
+ yield
|
||||
+ except LeappRuntimeError as e:
|
||||
+ # TODO: This only reports the actor that raised an exception
|
||||
+ # and the return code.
|
||||
+ # The traceback gets eaten on the framework level, and is only
|
||||
+ # seen in stderr. Changing that will require modifying the framework
|
||||
+ # code itself.
|
||||
+ msg = '{} - Please check the above details'.format(e.message)
|
||||
+ sys.stderr.write("\n")
|
||||
+ sys.stderr.write(pretty_block_text(msg, color="", width=len(msg)))
|
||||
+ logger.error(e.message)
|
||||
+ finally:
|
||||
+ pass
|
||||
+
|
||||
+
|
||||
+def log_errors(errors, logger):
|
||||
+ if errors:
|
||||
+ pretty_block_log("ERRORS", logger.info)
|
||||
+
|
||||
+ for error in errors:
|
||||
+ model = ErrorModel.create(json.loads(error['message']['data']))
|
||||
+ logger.error("{time} [{severity}] Actor: {actor}\nMessage: {message}\n".format(
|
||||
+ severity=model.severity.upper(),
|
||||
+ message=model.message, time=model.time, actor=model.actor))
|
||||
+ if model.details:
|
||||
+ print('Summary:')
|
||||
+ details = json.loads(model.details)
|
||||
+ for detail in details:
|
||||
+ print(' {k}: {v}'.format(
|
||||
+ k=detail.capitalize(),
|
||||
+ v=details[detail].rstrip().replace('\n', '\n' + ' ' * (6 + len(detail)))))
|
||||
+
|
||||
+
|
||||
+def log_inhibitors(context_id, logger):
|
||||
+ from leapp.reporting import Flags # pylint: disable=import-outside-toplevel
|
||||
+ reports = fetch_upgrade_report_messages(context_id)
|
||||
+ inhibitors = [report for report in reports if Flags.INHIBITOR in report.get('flags', [])]
|
||||
+ if inhibitors:
|
||||
+ pretty_block_log("UPGRADE INHIBITED", logger.error)
|
||||
+ logger.error('Upgrade has been inhibited due to the following problems:')
|
||||
+ for position, report in enumerate(inhibitors, start=1):
|
||||
+ logger.error('{idx:5}. Inhibitor: {title}'.format(idx=position, title=report['title']))
|
||||
+ logger.info('Consult the pre-upgrade report for details and possible remediation.')
|
||||
diff --git a/packaging/leapp-repository.spec b/packaging/leapp-repository.spec
|
||||
index af4b31d..0d8f6c8 100644
|
||||
--- a/packaging/leapp-repository.spec
|
||||
@ -471,12 +976,13 @@ index 0000000..2c935d7
|
||||
+ ])
|
||||
diff --git a/repos/system_upgrade/cloudlinux/actors/checkrhnclienttools/actor.py b/repos/system_upgrade/cloudlinux/actors/checkrhnclienttools/actor.py
|
||||
new file mode 100644
|
||||
index 0000000..142ec22
|
||||
index 0000000..a1c1cee
|
||||
--- /dev/null
|
||||
+++ b/repos/system_upgrade/cloudlinux/actors/checkrhnclienttools/actor.py
|
||||
@@ -0,0 +1,57 @@
|
||||
@@ -0,0 +1,58 @@
|
||||
+from leapp.actors import Actor
|
||||
+from leapp import reporting
|
||||
+from leapp.reporting import Report
|
||||
+from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
|
||||
+from leapp.libraries.common.cllaunch import run_on_cloudlinux
|
||||
+
|
||||
@ -494,7 +1000,7 @@ index 0000000..142ec22
|
||||
+
|
||||
+ name = 'check_rhn_client_tools_version'
|
||||
+ consumes = ()
|
||||
+ produces = ()
|
||||
+ produces = (Report,)
|
||||
+ tags = (ChecksPhaseTag, IPUWorkflowTag)
|
||||
+
|
||||
+ minimal_version = Version('2.0.2')
|
||||
@ -724,6 +1230,130 @@ index 0000000..cd6801b
|
||||
+ except OSError as e:
|
||||
+ if e.errno != errno.ENOENT:
|
||||
+ raise
|
||||
diff --git a/repos/system_upgrade/cloudlinux/actors/detectcontrolpanel/actor.py b/repos/system_upgrade/cloudlinux/actors/detectcontrolpanel/actor.py
|
||||
new file mode 100644
|
||||
index 0000000..7b947f9
|
||||
--- /dev/null
|
||||
+++ b/repos/system_upgrade/cloudlinux/actors/detectcontrolpanel/actor.py
|
||||
@@ -0,0 +1,44 @@
|
||||
+from leapp import reporting
|
||||
+from leapp.actors import Actor
|
||||
+from leapp.reporting import Report
|
||||
+from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
|
||||
+
|
||||
+from leapp.libraries.common.cllaunch import run_on_cloudlinux
|
||||
+from leapp.libraries.actor.detectcontrolpanel import detect_panel, UNKNOWN_NAME
|
||||
+
|
||||
+
|
||||
+class DetectControlPanel(Actor):
|
||||
+ """
|
||||
+ Check for a presence of a control panel, and inhibit the upgrade if one is found.
|
||||
+ """
|
||||
+
|
||||
+ name = 'detect_control_panel'
|
||||
+ consumes = ()
|
||||
+ produces = (Report,)
|
||||
+ tags = (ChecksPhaseTag, IPUWorkflowTag)
|
||||
+
|
||||
+ @run_on_cloudlinux
|
||||
+ def process(self):
|
||||
+ panel = detect_panel()
|
||||
+
|
||||
+ if panel:
|
||||
+ summary_info = "Detected panel: {}".format(panel)
|
||||
+ if panel == UNKNOWN_NAME:
|
||||
+ summary_info = "Legacy custom panel script detected in CloudLinux configuration"
|
||||
+
|
||||
+ # Block the upgrade on any systems with a panel detected.
|
||||
+ reporting.create_report([
|
||||
+ reporting.Title("The upgrade process should not be run on systems with a control panel present."),
|
||||
+ reporting.Summary(
|
||||
+ "Systems with a control panel present are not supported at the moment."
|
||||
+ " No control panels are currently included in the Leapp database, which"
|
||||
+ " makes loss of functionality after the upgrade extremely likely."
|
||||
+ " {}.".format(summary_info)),
|
||||
+ reporting.Severity(reporting.Severity.HIGH),
|
||||
+ reporting.Tags([
|
||||
+ reporting.Tags.OS_FACTS
|
||||
+ ]),
|
||||
+ reporting.Flags([
|
||||
+ reporting.Flags.INHIBITOR
|
||||
+ ])
|
||||
+ ])
|
||||
diff --git a/repos/system_upgrade/cloudlinux/actors/detectcontrolpanel/libraries/detectcontrolpanel.py b/repos/system_upgrade/cloudlinux/actors/detectcontrolpanel/libraries/detectcontrolpanel.py
|
||||
new file mode 100644
|
||||
index 0000000..15b797a
|
||||
--- /dev/null
|
||||
+++ b/repos/system_upgrade/cloudlinux/actors/detectcontrolpanel/libraries/detectcontrolpanel.py
|
||||
@@ -0,0 +1,68 @@
|
||||
+import os
|
||||
+import os.path
|
||||
+
|
||||
+from leapp.libraries.stdlib import api
|
||||
+
|
||||
+
|
||||
+CPANEL_NAME = 'cPanel'
|
||||
+DIRECTADMIN_NAME = 'DirectAdmin'
|
||||
+PLESK_NAME = 'Plesk'
|
||||
+ISPMANAGER_NAME = 'ISPManager'
|
||||
+INTERWORX_NAME = 'InterWorx'
|
||||
+UNKNOWN_NAME = 'Unknown'
|
||||
+INTEGRATED_NAME = 'Integrated'
|
||||
+
|
||||
+CLSYSCONFIG = '/etc/sysconfig/cloudlinux'
|
||||
+
|
||||
+
|
||||
+def lvectl_custompanel_script():
|
||||
+ """
|
||||
+ Retrives custom panel script for lvectl from CL config file
|
||||
+ :return: Script path or None if script filename wasn't found in config
|
||||
+ """
|
||||
+ config_param_name = 'CUSTOM_GETPACKAGE_SCRIPT'
|
||||
+ try:
|
||||
+ # Try to determine the custom script name
|
||||
+ if os.path.exists(CLSYSCONFIG):
|
||||
+ with open(CLSYSCONFIG, 'r') as f:
|
||||
+ file_lines = f.readlines()
|
||||
+ for line in file_lines:
|
||||
+ line = line.strip()
|
||||
+ if line.startswith(config_param_name):
|
||||
+ line_parts = line.split('=')
|
||||
+ if len(line_parts) == 2 and line_parts[0].strip() == config_param_name:
|
||||
+ script_name = line_parts[1].strip()
|
||||
+ if os.path.exists(script_name):
|
||||
+ return script_name
|
||||
+ except (OSError, IOError, IndexError):
|
||||
+ # Ignore errors - what's important is that the script wasn't found
|
||||
+ pass
|
||||
+ return None
|
||||
+
|
||||
+
|
||||
+def detect_panel():
|
||||
+ """
|
||||
+ This function will try to detect control panels supported by CloudLinux
|
||||
+ :return: Detected control panel name or None
|
||||
+ """
|
||||
+ panel_name = None
|
||||
+ if os.path.isfile('/opt/cpvendor/etc/integration.ini'):
|
||||
+ panel_name = INTEGRATED_NAME
|
||||
+ elif os.path.isfile('/usr/local/cpanel/cpanel'):
|
||||
+ panel_name = CPANEL_NAME
|
||||
+ elif os.path.isfile('/usr/local/directadmin/directadmin') or\
|
||||
+ os.path.isfile('/usr/local/directadmin/custombuild/build'):
|
||||
+ panel_name = DIRECTADMIN_NAME
|
||||
+ elif os.path.isfile('/usr/local/psa/version'):
|
||||
+ panel_name = PLESK_NAME
|
||||
+ # ispmanager must have:
|
||||
+ # v5: /usr/local/mgr5/ directory,
|
||||
+ # v4: /usr/local/ispmgr/bin/ispmgr file
|
||||
+ elif os.path.isfile('/usr/local/ispmgr/bin/ispmgr') or os.path.isdir('/usr/local/mgr5'):
|
||||
+ panel_name = ISPMANAGER_NAME
|
||||
+ elif os.path.isdir('/usr/local/interworx'):
|
||||
+ panel_name = INTERWORX_NAME
|
||||
+ # Check if the CL config has a legacy custom script for a control panel
|
||||
+ elif lvectl_custompanel_script():
|
||||
+ panel_name = UNKNOWN_NAME
|
||||
+ return panel_name
|
||||
diff --git a/repos/system_upgrade/cloudlinux/actors/enableyumspacewalkplugin/actor.py b/repos/system_upgrade/cloudlinux/actors/enableyumspacewalkplugin/actor.py
|
||||
new file mode 100644
|
||||
index 0000000..1e353b1
|
||||
@ -2584,3 +3214,124 @@ index 923bf80..9972204 100755
|
||||
+ )
|
||||
|
||||
print("{} processed, {} changed, {} errors".format(processed, changed, errors))
|
||||
diff --git a/repos/system_upgrade/el7toel8/actors/opensshpermitrootlogincheck/actor.py b/repos/system_upgrade/el7toel8/actors/opensshpermitrootlogincheck/actor.py
|
||||
index f13a767..0ca198f 100644
|
||||
--- a/repos/system_upgrade/el7toel8/actors/opensshpermitrootlogincheck/actor.py
|
||||
+++ b/repos/system_upgrade/el7toel8/actors/opensshpermitrootlogincheck/actor.py
|
||||
@@ -1,7 +1,7 @@
|
||||
from leapp import reporting
|
||||
from leapp.actors import Actor
|
||||
from leapp.exceptions import StopActorExecutionError
|
||||
-from leapp.libraries.actor.opensshpermitrootlogincheck import semantics_changes
|
||||
+from leapp.libraries.actor.opensshpermitrootlogincheck import semantics_changes, add_permitrootlogin_conf
|
||||
from leapp.libraries.stdlib import api
|
||||
from leapp.models import OpenSshConfig, Report
|
||||
from leapp.reporting import create_report
|
||||
@@ -39,28 +39,32 @@ class OpenSshPermitRootLoginCheck(Actor):
|
||||
|
||||
resources = [
|
||||
reporting.RelatedResource('package', 'openssh-server'),
|
||||
- reporting.RelatedResource('file', '/etc/ssh/sshd_config')
|
||||
+ reporting.RelatedResource('file', '/etc/ssh/sshd_config'),
|
||||
+ reporting.RelatedResource('file', '/etc/ssh/sshd_config.leapp_backup')
|
||||
]
|
||||
- # When the configuration does not contain the PermitRootLogin directive and
|
||||
- # the configuration file was locally modified, it will not get updated by
|
||||
- # RPM and the user might be locked away from the server. Warn the user here.
|
||||
- if not config.permit_root_login and config.modified:
|
||||
+ if not config.permit_root_login:
|
||||
+ add_permitrootlogin_conf()
|
||||
create_report([
|
||||
- reporting.Title('Possible problems with remote login using root account'),
|
||||
+ reporting.Title('SSH configuration automatically modified to permit root login'),
|
||||
reporting.Summary(
|
||||
- 'OpenSSH configuration file does not explicitly state '
|
||||
- 'the option PermitRootLogin in sshd_config file, '
|
||||
- 'which will default in RHEL8 to "prohibit-password".'
|
||||
+ 'Your OpenSSH configuration file does not explicitly state '
|
||||
+ 'the option PermitRootLogin in sshd_config file. '
|
||||
+ 'Its default is "yes" in RHEL7, but will change in '
|
||||
+ 'RHEL8 to "prohibit-password", which may affect your ability '
|
||||
+ 'to log onto this machine after the upgrade. '
|
||||
+ 'To prevent this from occuring, the PermitRootLogin option '
|
||||
+ 'has been explicity set to "yes" to preserve the default behaivour '
|
||||
+ 'after migration.'
|
||||
+ 'The original configuration file has been backed up to'
|
||||
+ '/etc/ssh/sshd_config.leapp_backup'
|
||||
),
|
||||
- reporting.Severity(reporting.Severity.HIGH),
|
||||
+ reporting.Severity(reporting.Severity.MEDIUM),
|
||||
reporting.Tags(COMMON_REPORT_TAGS),
|
||||
reporting.Remediation(
|
||||
- hint='If you depend on remote root logins using '
|
||||
- 'passwords, consider setting up a different '
|
||||
- 'user for remote administration or adding '
|
||||
- '"PermitRootLogin yes" to sshd_config.'
|
||||
- ),
|
||||
- reporting.Flags([reporting.Flags.INHIBITOR])
|
||||
+ hint='If you would prefer to configure the root login policy yourself, '
|
||||
+ 'consider setting the PermitRootLogin option '
|
||||
+ 'in sshd_config explicitly.'
|
||||
+ )
|
||||
] + resources)
|
||||
|
||||
# Check if there is at least one PermitRootLogin other than "no"
|
||||
@@ -68,7 +72,7 @@ class OpenSshPermitRootLoginCheck(Actor):
|
||||
# This usually means some more complicated setup depending on the
|
||||
# default value being globally "yes" and being overwritten by this
|
||||
# match block
|
||||
- if semantics_changes(config):
|
||||
+ elif semantics_changes(config):
|
||||
create_report([
|
||||
reporting.Title('OpenSSH configured to allow root login'),
|
||||
reporting.Summary(
|
||||
@@ -76,7 +80,7 @@ class OpenSshPermitRootLoginCheck(Actor):
|
||||
'blocks, but not explicitly enabled in global or '
|
||||
'"Match all" context. This update changes the '
|
||||
'default to disable root logins using paswords '
|
||||
- 'so your server migth get inaccessible.'
|
||||
+ 'so your server might become inaccessible.'
|
||||
),
|
||||
reporting.Severity(reporting.Severity.HIGH),
|
||||
reporting.Tags(COMMON_REPORT_TAGS),
|
||||
diff --git a/repos/system_upgrade/el7toel8/actors/opensshpermitrootlogincheck/libraries/opensshpermitrootlogincheck.py b/repos/system_upgrade/el7toel8/actors/opensshpermitrootlogincheck/libraries/opensshpermitrootlogincheck.py
|
||||
index 0cb9081..7a962b7 100644
|
||||
--- a/repos/system_upgrade/el7toel8/actors/opensshpermitrootlogincheck/libraries/opensshpermitrootlogincheck.py
|
||||
+++ b/repos/system_upgrade/el7toel8/actors/opensshpermitrootlogincheck/libraries/opensshpermitrootlogincheck.py
|
||||
@@ -1,3 +1,5 @@
|
||||
+import errno
|
||||
+from leapp.libraries.stdlib import api
|
||||
|
||||
|
||||
def semantics_changes(config):
|
||||
@@ -13,3 +15,30 @@ def semantics_changes(config):
|
||||
globally_enabled = True
|
||||
|
||||
return not globally_enabled and in_match_disabled
|
||||
+
|
||||
+
|
||||
+def add_permitrootlogin_conf():
|
||||
+ CONFIG = '/etc/ssh/sshd_config'
|
||||
+ CONFIG_BACKUP = '/etc/ssh/sshd_config.leapp_backup'
|
||||
+ try:
|
||||
+ with open(CONFIG, 'r') as fd:
|
||||
+ sshd_config = fd.readlines()
|
||||
+
|
||||
+ permit_autoconf = [
|
||||
+ "# Automatically added by Leapp to preserve RHEL7 default\n",
|
||||
+ "# behaviour after migration.\n",
|
||||
+ "# Placed on top of the file to avoid being included into Match blocks.\n",
|
||||
+ "PermitRootLogin yes\n"
|
||||
+ "\n",
|
||||
+ ]
|
||||
+ permit_autoconf.extend(sshd_config)
|
||||
+ with open(CONFIG, 'w') as fd:
|
||||
+ fd.writelines(permit_autoconf)
|
||||
+ with open(CONFIG_BACKUP, 'w') as fd:
|
||||
+ fd.writelines(sshd_config)
|
||||
+
|
||||
+ except IOError as err:
|
||||
+ if err.errno != errno.ENOENT:
|
||||
+ error = 'Failed to open sshd_config: {}'.format(str(err))
|
||||
+ api.current_logger().error(error)
|
||||
+ return
|
||||
|
@ -40,9 +40,10 @@ py2_byte_compile "%1" "%2"}
|
||||
# to create such an rpm. Instead, we are going to introduce new naming for
|
||||
# RHEL 8+ packages to be consistent with other leapp projects in future.
|
||||
|
||||
Epoch: 1
|
||||
Name: leapp-repository
|
||||
Version: 0.16.0
|
||||
Release: 6%{?dist}.elevate.3
|
||||
Release: 6%{?dist}.elevate.4
|
||||
Summary: Repositories for leapp
|
||||
|
||||
License: ASL 2.0
|
||||
|
Loading…
Reference in New Issue
Block a user