From ce1b83fafbbf3b323874fbb363e85a2e5abab4e2 Mon Sep 17 00:00:00 2001 From: Jakub Jelen Date: Wed, 16 Mar 2022 21:48:04 +0100 Subject: [PATCH 14/39] Add actor for updating OpenSSH configuration to RHEL9 --- .../actors/opensshdropindirectory/actor.py | 29 ++++++++ .../libraries/opensshdropindirectory.py | 67 +++++++++++++++++++ .../test_opensshdropindirectory_prepend.py | 44 ++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 repos/system_upgrade/el8toel9/actors/opensshdropindirectory/actor.py create mode 100644 repos/system_upgrade/el8toel9/actors/opensshdropindirectory/libraries/opensshdropindirectory.py create mode 100644 repos/system_upgrade/el8toel9/actors/opensshdropindirectory/tests/test_opensshdropindirectory_prepend.py diff --git a/repos/system_upgrade/el8toel9/actors/opensshdropindirectory/actor.py b/repos/system_upgrade/el8toel9/actors/opensshdropindirectory/actor.py new file mode 100644 index 00000000..17a0c01a --- /dev/null +++ b/repos/system_upgrade/el8toel9/actors/opensshdropindirectory/actor.py @@ -0,0 +1,29 @@ +from leapp.actors import Actor +from leapp.libraries.actor import opensshdropindirectory +from leapp.models import InstalledRedHatSignedRPM, OpenSshConfig +from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag + + +class OpenSshDropInDirectory(Actor): + """ + The RHEL 9 provides default configuration file with an Include directive. + + If the configuration file was modified, it will not be replaced by the update + and we need to do couple of tweaks: + + * Insert Include directive as expected by the rest of the OS + * Verify the resulting configuration is valid + * The only potentially problematic option is "Subsystem", but it is kept in the + main sshd_config even in RHEL9 so there is no obvious upgrade path where it + could cause issues (unlike the Debian version). + + [1] https://bugzilla.mindrot.org/show_bug.cgi?id=3236 + """ + + name = 'open_ssh_drop_in_directory' + consumes = (OpenSshConfig, InstalledRedHatSignedRPM,) + produces = () + tags = (IPUWorkflowTag, ApplicationsPhaseTag,) + + def process(self): + opensshdropindirectory.process(self.consume(OpenSshConfig)) diff --git a/repos/system_upgrade/el8toel9/actors/opensshdropindirectory/libraries/opensshdropindirectory.py b/repos/system_upgrade/el8toel9/actors/opensshdropindirectory/libraries/opensshdropindirectory.py new file mode 100644 index 00000000..d55eee1c --- /dev/null +++ b/repos/system_upgrade/el8toel9/actors/opensshdropindirectory/libraries/opensshdropindirectory.py @@ -0,0 +1,67 @@ +from leapp.exceptions import StopActorExecutionError +from leapp.libraries.common.rpms import has_package +from leapp.libraries.stdlib import api +from leapp.models import InstalledRedHatSignedRPM + +# The main SSHD configuration file +SSHD_CONFIG = '/etc/ssh/sshd_config' + +# The include directive needed, taken from RHEL9 sshd_config with leapp comment +INCLUDE = 'Include /etc/ssh/sshd_config.d/*.conf' +INCLUDE_BLOCK = ''.join(('# Added by leapp during upgrade from RHEL8 to RHEL9\n', INCLUDE, '\n')) + + +def prepend_string_if_not_present(f, content, check_string): + """ + This reads the open file descriptor and checks for presense of the `check_string`. + If not present, the `content` is prepended to the original content of the file and + result is written. + Note, that this requires opened file for both reading and writing, for example with: + + with open(path, r+') as f: + """ + lines = f.readlines() + for line in lines: + if line.lstrip().startswith(check_string): + # The directive is present + return + + # prepend it otherwise, also with comment + f.seek(0) + f.write(''.join((content, ''.join(lines)))) + + +def process(openssh_messages): + """ + The main logic of the actor: + * read the configuration file message + * skip if no action is needed + * package not installed + * the configuration file was not modified + * insert the include directive if it is not present yet + """ + 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.'} + ) + + # If the package is not installed, there is no need to do anything + if not has_package(InstalledRedHatSignedRPM, 'openssh-server'): + return + + # If the configuration file was not modified, the rpm update will bring the new + # changes by itself + if not config.modified: + return + + # otherwise prepend the Include directive to the main sshd_config + api.current_logger().debug('Adding the Include directive to {}.' + .format(SSHD_CONFIG)) + try: + with open(SSHD_CONFIG, 'r+') as f: + prepend_string_if_not_present(f, INCLUDE_BLOCK, INCLUDE) + except (OSError, IOError) as error: + api.current_logger().error('Failed to modify the file {}: {} '.format(SSHD_CONFIG, error)) diff --git a/repos/system_upgrade/el8toel9/actors/opensshdropindirectory/tests/test_opensshdropindirectory_prepend.py b/repos/system_upgrade/el8toel9/actors/opensshdropindirectory/tests/test_opensshdropindirectory_prepend.py new file mode 100644 index 00000000..bccadf4b --- /dev/null +++ b/repos/system_upgrade/el8toel9/actors/opensshdropindirectory/tests/test_opensshdropindirectory_prepend.py @@ -0,0 +1,44 @@ +import pytest + +from leapp.libraries.actor.opensshdropindirectory import prepend_string_if_not_present + + +class MockFile(object): + def __init__(self, path, content=None): + self.path = path + self.content = content + self.error = False + + def readlines(self): + return self.content.splitlines(True) + + def seek(self, n): + self.content = '' + + def write(self, content): + self.content = content + + +testdata = ( + ('', 'Prepend', 'Prepend', + 'Prepend'), # only prepend + ('Text', '', '', + 'Text'), # only text + ('Text', 'Prepend', 'Prepend', + 'PrependText'), # prepended text + ('Prepend\nText\n', 'Prepend', 'Prepend', + 'Prepend\nText\n'), # already present + ('Text\n', '# Comment\nPrepend\n', 'Prepend', + '# Comment\nPrepend\nText\n'), # different prepend than check string + ('Prepend\nText\n', '# Comment\nPrepend\n', 'Prepend', + 'Prepend\nText\n'), # different prepend than check string, already present +) + + +@pytest.mark.parametrize('file_content,prepend,check_string,expected', testdata) +def test_prepend_string_if_not_present(file_content, prepend, check_string, expected): + f = MockFile('/etc/ssh/sshd_config', file_content) + + prepend_string_if_not_present(f, prepend, check_string) + + assert f.content == expected -- 2.35.3