Skip to content

BackupController

Adam Hosek edited this page May 20, 2024 · 3 revisions

The BackupController

Basic Concept

The BackupController is a stack. As new changes are made, you need to instantiate a RestorableChange and then push it onto the stack stored.

Usage

As part of both convert and analyze convert2rhel will make changes to the system. For everything in analyze and for as much as possible in convert, convert2rhel will attempt to record the state from before the changes were made and rollback to that previous state in case of problems. To aid in that, we have a backup.BackupController class where we can store the old state, make the changes, and then revert back later.

RestorableChanges

For every type of state that we change, we need to create a subclass of backup.RestorableChange. Whenever possible, we try to reuse a RestorableChange written for one section of code with a RestorableChange that does the same thing in another section. For instance, placing TLS certificates on the system is similar no matter what the certificate is. So we have one cert.Cert class which can be used from multiple places to save the present state of the certificates and roll back to that if we need to.

RestorableChange API

Each RestorableChange class needs to implement three methods:

  • __init__() needs to take all of the parameters necessary for the other two methods to execute and save that information into instance variables if necessary.
  • enable() will be called when the change is added to the BackupController. It needs to enable() takes no parameters. Anything it needs to know must be passed to __init__().
  • restore() will be called when the change is started. It takes no parameters.

Each method should call the parent class's method via super().

Backup folder structure

This is the actual basic structure of the backup folder, where we sub-divide into small modules to keep track of different backup items.

backup/
├── certs.py        # Module to keep track of backup/restore of certifications related material
├── files.py        # Module to keep track of backup/restore of anything that is related to a fiel
├── __init__.py     
├── packages.py     # Module to keep track of backup/restore of anything that is a package (rpm) operation

 1 directory, 4 files

Creating a new Backup class

Below, we can see a basic structure of how a RestorableBackup class can be composed. Basically, as described in the API breakdown above, every RestorableBackup class will need to have the enable and restore methods.

from convert2rhel.backup import RestorableChange

class RestorableExample(RestorableChange):
    def __init__(self, ...):
        """
        docstring explaining what the class do
        """
        super(RestorableExample, self).__init__()

    def enable(self):
        """docstring for how the backup work"""
        # Prevent multiple backup
        if self.enabled:
            return

        # Code related to backup

        # Set the enabled value
        super(RestorableExample, self).enable()

    def restore(self):
        """docstring for how the restore work"""
        if not self.enabled:
            return

        # Code related to restore

        super(RestorableExample, self).restore()

In the enable method, you will put all the logic behind how you want to backup your asset. This can involve multiples steps and other functions if needed, but in the end, the BackupController will only be able to work properly if you use and define this method. Trying to use anything else for backup will not be handled by the BackupController.

On the other hand, the restore method will handle how we will restore the asset we backed up earlier in the enable method. This can contain any type of logic needed by the asset to be restored to the system.

It is essential, for both of them, that you call super() at the end of the method for the specific base method you are executing. This will ensure to set a couple of properties internally for the class that will help the BackupController to control if the asset being saved or restored was already been processed.

How does the backup works?

Backups are placed in order into a list that tracks all the RestorableChanges derivated classes. Essentially, the backup is very simple, see the below code:

from convert2rhel.backup import backup_control
from convert2rhel.backup.files import RestorableFile

VERY_IMPORTANT_FILE_TO_BACKUP = "/etc/super-very-most-important-file.txt"

restorable_file = RestorableFile(VERY_IMPORTANT_FILE_TO_BACKUP)
backup_control.push(restorable_file)

After we pass the instance of the restorable_file to be pushed onto the list of the BackupController, the controller will be responsible to call the enable method inside the class that we just pushed to the stack.

In essence, after each push we do, the last item will be added to the last position of the list, becoming then a stack of items to backup.

How does the rollback works?

The rollback will pick all items inside the list controlled by the BackupController internals, revert the list, and then call the restore method for each item inside it.

The BackupController class contains two public methods to help us trigger the rollback for the instances we added to the stack.

The first method is the pop, in which case will remove the last element from the list and trigger the rollback for that item via the restore method contained inside the class instance.

from convert2rhel.backup import backup_control

print(len(backup_control._restorables)) # 10
backup_control.pop() # Pop the last element and trigger the `restore` method for that instance
print(len(backup_control._restorables)) # 9

The second one is the pop_all in which it is used during the rollback phase for convert2rhel. This method will call pop for all elements inside the list and trigger the rollback for each one of them.

from convert2rhel.backup import backup_control

print(len(backup_control._restorables)) # 10
backup_control.pop_all() # Pop all elements and trigger the `restore` method for each instance
print(len(backup_control._restorables)) # 0

If any of the restorable fail to restore, the exception is handled in the pop_all() method by logging the problem and appending the restorable to BackupController._rollback_failures list. Then the restore process continues with the next restorable.