Skip to content

Commit

Permalink
[RHELC-560, RHELC-166] Create a BackupController Framework and a rpm …
Browse files Browse the repository at this point in the history
…gpg key plugin (#516)

* Create a BackupController Framework and a rpm gpg key plugin

The BackupController framework is a pluggable way to make changes to the
system that we can revert at a later date.  It consists of
a BackupController object which we instantiate once per convert2rhel
run.  The API of the BackupController consists of:

* a push() method.  When a RestorableChange is pushed onto the
  BackupController's stack, the BackupController will call the
  RestorableChange's enable() method and then store the change on the
  BackupController's stack.
* a pop() method.  This will pop the last RestorableChange off of the
  stack, call the change's restore() method, and then return the change
  to the caller.
* a pop_all() method. This will call the restore() method on all of the
  RestorableChanges() and then return the list of all the changes to the
  caller.

RestorableChanges are objects which implement the following API:
* An enable() method.  When this is called, it should backup any data
  that would be needed to rollback the change and then will make the
  requested change.
* A restore() method.  When this is called, it should undo the changes
  made by the enable() method.

This commit also include a RestorableRpmKey class which implements
RestorableChange for rpm gpg keys.  Upon enable(), the class will record
whether the key is already present in the rpm database.  If it isn't, it
will then be added.  On restore(), it will check whether the key was
installed before enable() was called.  If it wasn't, it will uninstall
that key.

pkghandler.install_gpg_keys() has been enhanced to use the
RestorableRpmKey() and the step that installs gpg keys has been moved
into the Pre-ponr section of the code.
  • Loading branch information
abadger authored Oct 26, 2022
1 parent 5327a1f commit bb78d25
Show file tree
Hide file tree
Showing 9 changed files with 635 additions and 36 deletions.
165 changes: 165 additions & 0 deletions convert2rhel/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import abc
import logging
import os
import shutil

import six

from convert2rhel import utils
from convert2rhel.repo import get_hardcoded_repofiles_dir
from convert2rhel.systeminfo import system_info
from convert2rhel.utils import BACKUP_DIR, download_pkg, remove_orphan_folders, run_subprocess
Expand Down Expand Up @@ -104,6 +108,166 @@ def restore_pkgs(self):
self._install_removed_pkgs()


class BackupController(object):
"""
Controls backup and restore for all restorable types.
This is the second version of a backup controller. It handles all types of things that
convert2rhel will change on the system which it can restore in case of a failure before the
Point-of-no-return (PONR).
The basic interface to this is a LIFO stack. When a Restorable is pushed
onto the stack, it is backed up. When it is popped off of the stack, it is
restored. Changes are restored in the reverse order that that they were
added. Changes cannot be retrieved and restored out of order.
"""

def __init__(self):
self._restorables = []

def push(self, restorable):
"""
Enable a RestorableChange and track it in case it needs to be restored.
:arg restorable: RestorableChange object that can be restored later.
"""
if not isinstance(restorable, RestorableChange):
raise TypeError("`%s` is not a RestorableChange object" % restorable)

restorable.enable()

self._restorables.append(restorable)

def pop(self):
"""
Restore and then return the last RestorableChange added to the Controller.
:returns: RestorableChange object that was last added.
:raises IndexError: If there are no RestorableChanges currently known to the Controller.
"""
try:
restorable = self._restorables.pop()
except IndexError as e:
# Use a more specific error message
args = list(e.args)
args[0] = "No backups to restore"
e.args = tuple(args)
raise e

restorable.restore()

return restorable

def pop_all(self):
"""
Restores all RestorableChanges known to the Controller and then returns them.
:returns: List of RestorableChange objects that were known to the Controller.
:raises IndexError: If there are no RestorableChanges currently known to the Controller.
After running, the Controller object will not know about any RestorableChanges.
"""
restorables = self._restorables

if not restorables:
raise IndexError("No backups to restore")

# We want to restore in the reverse order the changes were enabled.
for restorable in reversed(restorables):
restorable.restore()

# Reset the internal storage in case we want to use it again
self._restorables = []

# Now that we know everything succeeded, reverse the list that we return to the user
restorables.reverse()

return restorables


@six.add_metaclass(abc.ABCMeta)
class RestorableChange(object):
"""
Interface definition for types which can be restored.
"""

@abc.abstractmethod
def __init__(self):
self.enabled = False

@abc.abstractmethod
def enable(self):
"""
Backup should be idempotent. In other words, it should know if the resource has already
been backed up and refuse to do so a second time.
"""
self.enabled = True

@abc.abstractmethod
def restore(self):
"""
Restore the state of the system.
"""
self.enabled = False


class RestorableRpmKey(RestorableChange):
"""Import a GPG key into rpm in a reversible fashion."""

def __init__(self, keyfile):
"""
Setup a RestorableRpmKey to reflect the GPG key in a file.
:arg keyfile: Filepath for a GPG key. The RestorableRpmKey instance will be able to import
this into the rpmdb when enabled and remove it when restored.
"""
super(RestorableRpmKey, self).__init__()
self.previously_installed = None
self.keyfile = keyfile
self.keyid = utils.find_keyid(keyfile)

def enable(self):
"""Ensure that the GPG key has been imported into the rpmdb."""
# For idempotence, do not back this up if we've already done so.
if self.enabled:
return

if not self.installed:
output, ret_code = utils.run_subprocess(["rpm", "--import", self.keyfile], print_output=False)
if ret_code != 0:
raise utils.ImportGPGKeyError("Failed to import the GPG key %s: %s" % (self.keyfile, output))

self.previously_installed = False
loggerinst.info("GPG key %s imported", self.keyid)

else:
self.previously_installed = True

super(RestorableRpmKey, self).enable()

@property
def installed(self):
"""Whether the GPG key has been imported into the rpmdb."""
output, status = utils.run_subprocess(["rpm", "-q", "gpg-pubkey-%s" % self.keyid], print_output=False)

if status == 0:
return True

if status == 1 and "package gpg-pubkey-%s is not installed" % self.keyid in output:
return False

raise utils.ImportGPGKeyError(
"Searching the rpmdb for the gpg key %s failed: Code %s: %s" % (self.keyid, status, output)
)

def restore(self):
"""Ensure the rpmdb has or does not have the GPG key according to the state before we ran."""
if self.enabled and self.previously_installed is False:
utils.run_subprocess(["rpm", "-e", "gpg-pubkey-%s" % self.keyid])

super(RestorableRpmKey, self).restore()


class RestorableFile(object):
def __init__(self, filepath):
self.filepath = filepath
Expand Down Expand Up @@ -207,3 +371,4 @@ def remove_pkgs(pkgs_to_remove, backup=True, critical=True):


changed_pkgs_control = ChangedRPMPackagesController() # pylint: disable=C0103
backup_control = BackupController()
13 changes: 11 additions & 2 deletions convert2rhel/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ def pre_ponr_conversion():
loggerinst.task("Convert: Resolve possible edge cases")
special_cases.check_and_resolve()

# Import the Red Hat GPG Keys for installing Subscription-manager and for later.
loggerinst.task("Convert: Import Red Hat GPG keys")
pkghandler.install_gpg_keys()

rhel_repoids = []
if not toolopts.tool_opts.no_rhsm:
loggerinst.task("Convert: Subscription Manager - Download packages")
Expand Down Expand Up @@ -232,8 +236,6 @@ def pre_ponr_conversion():
def post_ponr_conversion():
"""Perform main steps for system conversion."""

loggerinst.task("Convert: Import Red Hat GPG keys")
pkghandler.install_gpg_keys()
loggerinst.task("Convert: Prepare kernel")
pkghandler.preserve_only_rhel_kernel()
loggerinst.task("Convert: Replace packages")
Expand Down Expand Up @@ -271,6 +273,13 @@ def rollback_changes():
pkghandler.versionlock_file.restore()
system_cert = cert.SystemCert()
system_cert.remove()
try:
backup.backup_control.pop_all()
except IndexError as e:
if e.args[0] == "No backups to restore":
loggerinst.info("During rollback there were no backups to restore")
else:
raise

return

Expand Down
16 changes: 8 additions & 8 deletions convert2rhel/pkghandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@

import rpm

from convert2rhel import pkgmanager, utils
from convert2rhel.backup import RestorableFile, remove_pkgs
from convert2rhel import backup, pkgmanager, utils
from convert2rhel.backup import RestorableFile, RestorableRpmKey, remove_pkgs
from convert2rhel.systeminfo import system_info
from convert2rhel.toolopts import tool_opts

Expand Down Expand Up @@ -645,12 +645,12 @@ def install_gpg_keys():
gpg_path = os.path.join(utils.DATA_DIR, "gpg-keys")
gpg_keys = [os.path.join(gpg_path, key) for key in os.listdir(gpg_path)]
for gpg_key in gpg_keys:
output, ret_code = utils.run_subprocess(
["rpm", "--import", os.path.join(gpg_path, gpg_key)],
print_output=False,
)
if ret_code != 0:
loggerinst.critical("Unable to import the GPG key %s:\n %s.", gpg_key, output)
try:
restorable_key = RestorableRpmKey(gpg_key)
backup.backup_control.push(restorable_key)
except utils.ImportGPGKeyError as e:
loggerinst.critical("Importing the GPG key into rpm failed:\n %s" % str(e))

loggerinst.info("GPG key %s imported successfuly.", gpg_key)


Expand Down
Loading

0 comments on commit bb78d25

Please sign in to comment.