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 check for available space on /boot #5

Merged
merged 1 commit into from
Feb 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 19 additions & 0 deletions repos/system_upgrade/el7toel8/actors/addupgradebootentry/actor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from leapp.actors import Actor
from leapp.libraries.actor.library import add_boot_entry
from leapp.models import BootContent
from leapp.tags import InterimPreparationPhaseTag, IPUWorkflowTag


class AddUpgradeBootEntry(Actor):
name = 'add_upgrade_boot_entry'
description = '''
Add new boot entry for the leapp-provided initramfs so that leapp can continue with the upgrade
process in the initramfs after reboot.
'''

consumes = (BootContent,)
produces = ()
tags = (IPUWorkflowTag, InterimPreparationPhaseTag)

def process(self):
add_boot_entry()
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import os

from leapp.exceptions import StopActorExecutionError
from leapp.libraries import stdlib
from leapp.models import BootContent


def add_boot_entry():
debug = 'debug' if os.getenv('LEAPP_DEBUG', '0') == '1' else ''

kernel_dst_path, initram_dst_path = get_boot_file_paths()
stdlib.call([
'/usr/sbin/grubby',
'--add-kernel={0}'.format(kernel_dst_path),
'--initrd={0}'.format(initram_dst_path),
'--title=RHEL Upgrade Initramfs',
'--copy-default',
'--make-default',
'--args="{DEBUG} enforcing=0 rd.plymouth=0 plymouth.enable=0"'.format(DEBUG=debug)
])


def get_boot_file_paths():
boot_content_msgs = stdlib.api.consume(BootContent)
boot_content = next(boot_content_msgs, None)
if list(boot_content_msgs):
stdlib.api.current_logger().warning('Unexpectedly received more than one BootContent message.')
if not boot_content:
raise StopActorExecutionError('Could not create a GRUB boot entry for the upgrade initramfs',
details={'details': 'Did not receive a message about the leapp-provided'
'kernel and initramfs'})
return boot_content.kernel_path, boot_content.initram_path
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import pytest

from leapp.exceptions import StopActorExecutionError
from leapp.libraries import stdlib
from leapp.libraries.actor import library
from leapp.models import BootContent


class call_mocked(object):
def __call__(self, args, split=True):
self.args = args


def test_add_boot_entry(monkeypatch):
def get_boot_file_paths_mocked():
return '/abc', '/def'
monkeypatch.setattr(library, 'get_boot_file_paths', get_boot_file_paths_mocked)
monkeypatch.setenv('LEAPP_DEBUG', '1')
monkeypatch.setattr(stdlib, 'call', call_mocked())

library.add_boot_entry()

assert ' '.join(stdlib.call.args) == ('/usr/sbin/grubby --add-kernel=/abc --initrd=/def --title=RHEL'
' Upgrade Initramfs --copy-default --make-default --args="debug'
' enforcing=0 rd.plymouth=0 plymouth.enable=0"')


def test_get_boot_file_paths(monkeypatch):
# BootContent message available
def consume_message_mocked(*models):
yield BootContent(kernel_path='/ghi', initram_path='/jkl')
monkeypatch.setattr('leapp.libraries.stdlib.api.consume', consume_message_mocked)

kernel_path, initram_path = library.get_boot_file_paths()

assert kernel_path == '/ghi' and initram_path == '/jkl'

# No BootContent message available
def consume_no_message_mocked(*models):
yield None
monkeypatch.setattr('leapp.libraries.stdlib.api.consume', consume_no_message_mocked)

with pytest.raises(StopActorExecutionError):
library.get_boot_file_paths()
27 changes: 27 additions & 0 deletions repos/system_upgrade/el7toel8/actors/checkbootavailspace/actor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from leapp.actors import Actor
from leapp.libraries.actor.library import (check_avail_space_on_boot,
get_avail_bytes_on_boot)
from leapp.models import Inhibitor
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckBootAvailSpace(Actor):
name = 'check_boot_avail_space'
description = '''
Require at least 100MiB of available space on /boot. Otherwise inhibit the upgrade.

Rationale for the requirement of 100MiB:
- Before reboot into initramfs, the CopyInitramfsToBoot actor copies kernel and initramfs to /boot, together
worth of 66MiB.
- After booting into initramfs, the RemoveBootFiles actor removes the copied kernel and initramfs from /boot.
- The DnfShellRpmUpgrade installs a new kernel-core package which puts additional 54MiB of data to /boot.
- Even though the available space needed at the time of writing this actor is 66MiB, the additional
100-66=34MiB is a leeway for potential growth of the kernel or initramfs in size.
'''

consumes = ()
produces = (Inhibitor,)
tags = (IPUWorkflowTag, ChecksPhaseTag)

def process(self):
check_avail_space_on_boot(get_avail_bytes_on_boot)
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from os import statvfs

from leapp.libraries.stdlib import api
from leapp.models import Inhibitor

MIN_AVAIL_BYTES_FOR_BOOT = 100 * 2**20 # 100 MiB


def check_avail_space_on_boot(boot_avail_space_getter):
avail_bytes = boot_avail_space_getter()
if is_additional_space_required(avail_bytes):
inhibit_upgrade(avail_bytes)


def get_avail_bytes_on_boot():
boot_stat = statvfs('/boot')
return boot_stat.f_frsize * boot_stat.f_bavail


def is_additional_space_required(avail_bytes):
return avail_bytes < MIN_AVAIL_BYTES_FOR_BOOT


def inhibit_upgrade(avail_bytes):
additional_mib_needed = (MIN_AVAIL_BYTES_FOR_BOOT - avail_bytes) / 2**20
api.produce(Inhibitor(
summary='Not enough space on /boot',
details='/boot needs additional {0} MiB to be able to accomodate the upgrade initramfs and new kernel.'
.format(additional_mib_needed)))
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from leapp.libraries.actor.library import (MIN_AVAIL_BYTES_FOR_BOOT,
check_avail_space_on_boot,
inhibit_upgrade)
from leapp.libraries.stdlib import api
from leapp.models import Inhibitor


class produce_mocked(object):
def __init__(self):
self.called = 0

def __call__(self, *model_instances):
self.called += 1
self.model_instances = model_instances


class fake_get_avail_bytes_on_boot(object):
def __init__(self, size):
self.size = size

def __call__(self, *args):
return self.size


def test_not_enough_space_available(monkeypatch):
monkeypatch.setattr(api, 'produce', produce_mocked())

# Test 0 bytes available /boot
get_avail_bytes_on_boot = fake_get_avail_bytes_on_boot(0)
check_avail_space_on_boot(get_avail_bytes_on_boot)

# Test 0.1 MiB less then required in /boot
get_avail_bytes_on_boot = fake_get_avail_bytes_on_boot(MIN_AVAIL_BYTES_FOR_BOOT - 0.1 * 2**20)
check_avail_space_on_boot(get_avail_bytes_on_boot)

assert api.produce.called == 2


def test_enough_space_available(monkeypatch):
monkeypatch.setattr(api, 'produce', produce_mocked())

get_avail_bytes_on_boot = fake_get_avail_bytes_on_boot(MIN_AVAIL_BYTES_FOR_BOOT)
check_avail_space_on_boot(get_avail_bytes_on_boot)

assert api.produce.called == 0


def test_inhibit_upgrade(monkeypatch):
monkeypatch.setattr(api, 'produce', produce_mocked())

# Test 4.2 MiB available on /boot
bytes_available = 4.2 * 2**20
inhibit_upgrade(bytes_available)

assert api.produce.called == 1
assert type(api.produce.model_instances[0]) is Inhibitor
mib_needed = (MIN_AVAIL_BYTES_FOR_BOOT - bytes_available) / 2**20
assert "needs additional {0} MiB".format(mib_needed) in api.produce.model_instances[0].details
15 changes: 15 additions & 0 deletions repos/system_upgrade/el7toel8/actors/copytoboot/actor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from leapp.actors import Actor
from leapp.libraries.actor.library import copy_to_boot
from leapp.models import BootContent
from leapp.tags import InterimPreparationPhaseTag, IPUWorkflowTag


class CopyToBoot(Actor):
name = 'copy_to_boot'
description = 'Copy initramfs, which was specially prepared for the upgrade, together with its kernel to /boot/.'
consumes = ()
produces = (BootContent,)
tags = (IPUWorkflowTag, InterimPreparationPhaseTag)

def process(self):
copy_to_boot()
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import shutil
from os import path

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.stdlib import api
from leapp.models import BootContent

INITRAM_FILENAME = 'initramfs-upgrade.x86_64.img'
KERNEL_FILENAME = 'vmlinuz-upgrade.x86_64'


def copy_to_boot():
files_to_copy = get_files_to_copy()
copy_files(files_to_copy)
message_copied_files()


def get_files_to_copy():
files_to_copy = {}
for filename in KERNEL_FILENAME, INITRAM_FILENAME:
files_to_copy[filename] = {'src_path': get_src_filepath(filename), 'dst_path': get_dst_filepath(filename)}
return files_to_copy


def get_src_filepath(filename):
src_filepath = api.get_file_path(filename)
if src_filepath is None:
raise StopActorExecutionError('Could not find {0} in the following paths: {1}'
.format(filename, ' '.join(api.files_paths())),
details={'hint': 'You may want to try to reinstall'
' the "leapp-repository" package'})
return src_filepath


def get_dst_filepath(filename):
return path.join('/boot', filename)


def copy_files(files_to_copy):
for filename in files_to_copy.keys():
try:
shutil.copyfile(files_to_copy[filename]['src_path'], files_to_copy[filename]['dst_path'])
except IOError as err:
raise StopActorExecutionError('Could not copy {0} to /boot'.format(files_to_copy[filename]['src_path']),
details={'details': str(err)})


def message_copied_files():
"""Let the other actors know what files we've stored on /boot."""
api.produce(BootContent(kernel_path=get_dst_filepath(KERNEL_FILENAME),
initram_path=get_dst_filepath(INITRAM_FILENAME)))
68 changes: 68 additions & 0 deletions repos/system_upgrade/el7toel8/actors/copytoboot/tests/unit_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import pytest

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import library
from leapp.libraries.stdlib import api
from leapp.models import BootContent

FILES_TO_COPY = {library.INITRAM_FILENAME: {'src_path': '/src/{}'.format(library.INITRAM_FILENAME),
'dst_path': '/boot/{}'.format(library.INITRAM_FILENAME)},
library.KERNEL_FILENAME: {'src_path': '/src/{}'.format(library.KERNEL_FILENAME),
'dst_path': '/boot/{}'.format(library.KERNEL_FILENAME)}}


class produce_mocked(object):
def __call__(self, *model_instances):
self.model_instances = model_instances


def test_copy_to_boot(monkeypatch):
# Test that the actor produces the BootContent message
def do_nothing(*args):
pass
monkeypatch.setattr(library, 'get_files_to_copy', do_nothing)
monkeypatch.setattr(library, 'copy_files', do_nothing)
monkeypatch.setattr(api, 'produce', produce_mocked())

library.copy_to_boot()

assert type(api.produce.model_instances[0]) is BootContent


def test_get_files_to_copy(monkeypatch):
# Test that the get_files_to_copy() returns an expected dict
def get_src_filepath_mocked(filename):
return '/src/{}'.format(filename)
monkeypatch.setattr(library, 'get_src_filepath', get_src_filepath_mocked)

def get_dst_filepath_mocked(filename):
return '/boot/{}'.format(filename)
monkeypatch.setattr(library, 'get_dst_filepath', get_dst_filepath_mocked)

files_to_copy = library.get_files_to_copy()

assert files_to_copy == FILES_TO_COPY


def test_get_src_filepath(monkeypatch):
# Test that internal exception is raised if the source file for copying is not found
def get_file_path_mocked(filename):
return None
monkeypatch.setattr(api, 'get_file_path', get_file_path_mocked)

def files_paths_mocked():
return ['/path']
monkeypatch.setattr(api, 'files_paths', files_paths_mocked)

with pytest.raises(StopActorExecutionError):
library.get_src_filepath('filename')


def test_copy_files(monkeypatch):
# Test that internal exception is raised if it's not possible to copy a file
def copyfile_mocked(src, dst):
raise IOError
monkeypatch.setattr('shutil.copyfile', copyfile_mocked)

with pytest.raises(StopActorExecutionError):
library.copy_files(FILES_TO_COPY)

This file was deleted.

Loading