Skip to content

Commit

Permalink
Azure hybrid image handling
Browse files Browse the repository at this point in the history
Detect Azure hybrid (BIOS/EFI) image and convert
/boot/grub2/grubenv symling pointing to
/boot/efi/EFI/redhat/grubenv to a normal file
as GRUB cannot read the symlink if it is pointing
to different partition (in case of BIOS).

Consequently, we do not need the "KernelCmdlineArg"
with "root_delay" anymore.
  • Loading branch information
Rezney authored and pirat89 committed Sep 30, 2020
1 parent 4e97f1e commit dce08fc
Show file tree
Hide file tree
Showing 11 changed files with 262 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from leapp.actors import Actor
from leapp.models import InstalledRPM, HybridImage, FirmwareFacts
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
from leapp.libraries.actor.checkhybridimage import check_hybrid_image


class CheckHybridImage(Actor):
"""
Check if the system is using Azure hybrid image.
These images have a default relative symlink to EFI
partion even when booted using BIOS and in such cases
GRUB is not able find "grubenv" to get the kernel cmdline
options and fails to boot after upgrade`.
"""

name = 'checkhybridimage'
consumes = (InstalledRPM, FirmwareFacts)
produces = (HybridImage,)
tags = (ChecksPhaseTag, IPUWorkflowTag)

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

from leapp.libraries.common.rpms import has_package
from leapp.libraries.stdlib import api
from leapp.models import InstalledRPM, HybridImage, FirmwareFacts
from leapp import reporting
from leapp.libraries.common import rhui


BIOS_PATH = '/boot/grub2/grubenv'
EFI_PATH = '/boot/efi/EFI/redhat/grubenv'


def is_grubenv_symlink_to_efi():
"""
Check whether '/boot/grub2/grubenv' is a relative symlink to
'/boot/efi/EFI/redhat/grubenv'.
"""
return os.path.islink(BIOS_PATH) and os.path.realpath(BIOS_PATH) == os.path.realpath(EFI_PATH)


def is_azure_agent_installed():
"""Check whether 'WALinuxAgent' package is installed."""
arch = api.current_actor().configuration.architecture
agent_pkg = rhui.RHUI_CLOUD_MAP[arch]['azure']['agent_pkg']
return has_package(InstalledRPM, agent_pkg)


def is_bios():
"""Check whether system is booted into BIOS"""
ff = next(api.consume(FirmwareFacts), None)
return ff and ff.firmware == 'bios'


def check_hybrid_image():
"""Check whether the system is using Azure hybrid image."""
if all([is_grubenv_symlink_to_efi(), is_azure_agent_installed(), is_bios()]):
api.produce(HybridImage(detected=True))
reporting.create_report([
reporting.Title(
'Azure hybrid (BIOS/EFI) image detected. "grubenv" symlink will be converted to a regular file'
),
reporting.Summary(
'Leapp detected the system is running on Azure cloud, booted using BIOS and '
'the "/boot/grub2/grubenv" file is a symlink to "../efi/EFI/redhat/grubenv". In case of such a '
'hybrid image scenario GRUB is not able to locate "grubenv" as it is a symlink to different '
'partition and fails to boot. If the system needs to be run in EFI mode later, please re-create '
'the relative symlink again.'
),
reporting.Severity(reporting.Severity.HIGH),
reporting.Tags([reporting.Tags.PUBLIC_CLOUD]),
])
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import pytest

from leapp.libraries.actor import checkhybridimage
from leapp.libraries.common.testutils import produce_mocked, create_report_mocked, CurrentActorMocked
from leapp.libraries.stdlib import api
from leapp.models import FirmwareFacts, InstalledRPM, RPM
from leapp.reporting import Report
from leapp import reporting


RH_PACKAGER = 'Red Hat, Inc. <http://bugzilla.redhat.com/bugzilla>'
WA_AGENT_RPM = RPM(
name='WALinuxAgent', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'
)
NO_AGENT_RPM = RPM(
name='NoAgent', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'
)

INSTALLED_AGENT = InstalledRPM(items=[WA_AGENT_RPM])
NOT_INSTALLED_AGENT = InstalledRPM(items=[NO_AGENT_RPM])

BIOS_FIRMWARE = FirmwareFacts(firmware='bios')
EFI_FIRMWARE = FirmwareFacts(firmware='efi')

BIOS_PATH = '/boot/grub2/grubenv'
EFI_PATH = '/boot/efi/EFI/redhat/grubenv'


def test_hybrid_image(monkeypatch, tmpdir):
grubenv_efi = tmpdir.join('grubenv_efi')
grubenv_efi.write('grubenv')

grubenv_boot = tmpdir.join('grubenv_boot')
grubenv_boot.mksymlinkto('grubenv_efi')

monkeypatch.setattr(checkhybridimage, 'BIOS_PATH', grubenv_boot.strpath)
monkeypatch.setattr(checkhybridimage, 'EFI_PATH', grubenv_efi.strpath)
monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
monkeypatch.setattr(
api, 'current_actor', CurrentActorMocked(arch='x86_64', msgs=[BIOS_FIRMWARE, INSTALLED_AGENT])
)
monkeypatch.setattr(api, "produce", produce_mocked())

checkhybridimage.check_hybrid_image()
assert reporting.create_report.called == 1
assert 'hybrid' in reporting.create_report.report_fields['title']
assert api.produce.called == 1


@pytest.mark.parametrize('is_symlink, realpath_match, is_bios, agent_installed', [
(False, True, True, True),
(True, False, True, True),
(True, True, False, True),
(True, True, True, False),
])
def test_no_hybrid_image(monkeypatch, is_symlink, realpath_match, is_bios, agent_installed, tmpdir):
grubenv_efi = tmpdir.join('grubenv_efi')
grubenv_efi.write('grubenv')
grubenv_efi_false = tmpdir.join('grubenv_efi_false')
grubenv_efi.write('nope')
grubenv_boot = tmpdir.join('grubenv_boot')

grubenv_target = grubenv_efi if realpath_match else grubenv_efi_false

if is_symlink:
grubenv_boot.mksymlinkto(grubenv_target)

firmw = BIOS_FIRMWARE if is_bios else EFI_FIRMWARE
inst_rpms = INSTALLED_AGENT if agent_installed else NOT_INSTALLED_AGENT

monkeypatch.setattr(checkhybridimage, 'BIOS_PATH', grubenv_boot.strpath)
monkeypatch.setattr(checkhybridimage, 'EFI_PATH', grubenv_efi.strpath)
monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
monkeypatch.setattr(
api, 'current_actor', CurrentActorMocked(arch='x86_64', msgs=[firmw, inst_rpms])
)
monkeypatch.setattr(api, "produce", produce_mocked())

checkhybridimage.check_hybrid_image()
assert not reporting.create_report.called
assert not api.produce.called
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,6 @@ def process(self):
# We have to disable Amazon-id plugin in the initramdisk phase as the network
# is down at the time
self.produce(DNFPluginTask(name='amazon-id', disable_in=['upgrade']))
if provider == 'azure':
# This option is important on Azure. Due to the special Azure hybrid image
# GRUB cannot find grubenv and is getting just default options.
self.produce(KernelCmdlineArg(**{'key': 'rootdelay', 'value': '300'}))
# if RHEL7 and RHEL8 packages differ, we cannot rely on simply updating them
if info['el7_pkg'] != info['el8_pkg']:
self.produce(RpmTransactionTasks(to_install=[info['el8_pkg']]))
Expand Down
28 changes: 28 additions & 0 deletions repos/system_upgrade/el7toel8/actors/cloud/grubenvtofile/actor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from leapp.actors import Actor
from leapp.models import HybridImage
from leapp.tags import FinalizationPhaseTag, IPUWorkflowTag
from leapp.libraries.actor.grubenvtofile import grubenv_to_file


class GrubenvToFile(Actor):
"""
Convert "grubenv" symlink to a regular file on Azure hybrid images using BIOS.
Azure images provided by Red Hat aim for hybrid (BIOS/EFI) functionality,
however, currently GRUB is not able to see the "grubenv" file if it is a symlink
to a different partition (default on EFI with grub2-efi pkg installed) and
fails on BIOS systems. This actor converts the symlink to the normal file
with the content of grubenv on the EFI partition in case the system is using BIOS
and running on the Azure cloud. This action is reported in the preupgrade phase.
"""

name = 'grubenvtofile'
consumes = (HybridImage,)
produces = ()
tags = (FinalizationPhaseTag, IPUWorkflowTag)

def process(self):
grubenv_msg = next(self.consume(HybridImage), None)

if grubenv_msg and grubenv_msg.detected:
grubenv_to_file()
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from leapp.libraries.stdlib import api, run, CalledProcessError


BIOS_PATH = '/boot/grub2/grubenv'
EFI_PATH = '/boot/efi/EFI/redhat/grubenv'


def grubenv_to_file():
try:
run(['unlink', BIOS_PATH])
except CalledProcessError as err:
api.current_logger().warning('Could not unlink {}: {}'.format(BIOS_PATH, str(err)))
return
try:
run(['cp', '-a', EFI_PATH, BIOS_PATH])
api.current_logger().info(
'{} converted from being a symlink pointing to {} file into a regular file'.format(BIOS_PATH, EFI_PATH)
)
except CalledProcessError as err:
api.current_logger().warning('Could not copy content of {} to {}: {}'.format(EFI_PATH, BIOS_PATH, str(err)))
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import pytest

from leapp.models import HybridImage
from leapp.libraries.common.testutils import logger_mocked
from leapp.libraries.actor import grubenvtofile
from leapp.libraries.stdlib import CalledProcessError, api


def raise_call_error(args=None):
raise CalledProcessError(
message='A Leapp Command Error occured.',
command=args,
result={'signal': None, 'exit_code': 1, 'pid': 0, 'stdout': 'fake', 'stderr': 'fake'}
)


class run_mocked(object):
def __init__(self, raise_err=False):
self.called = 0
self.args = []
self.raise_err = raise_err

def __call__(self, *args):
self.called += 1
self.args.append(args)
if self.raise_err:
raise_call_error(args)


def test_grubenv_to_file(monkeypatch):
monkeypatch.setattr(api, 'consume', lambda x: iter([HybridImage()]))
monkeypatch.setattr(grubenvtofile, 'run', run_mocked())
grubenvtofile.grubenv_to_file()
assert grubenvtofile.run.called == 2


def test_fail_grubenv_to_file(monkeypatch):
monkeypatch.setattr(api, 'consume', lambda x: iter([HybridImage()]))
monkeypatch.setattr(grubenvtofile, 'run', run_mocked(raise_err=True))
monkeypatch.setattr(api, 'current_logger', logger_mocked())
grubenvtofile.grubenv_to_file()
assert grubenvtofile.run.called == 1
assert api.current_logger.warnmsg[0].startswith('Could not unlink')
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ def has_katello_prefix(pkg):

def is_azure_pkg(pkg):
"""Whitelist Azure config package."""

arch = self.configuration.architecture

el7_pkg = rhui.RHUI_CLOUD_MAP[arch]['azure']['el7_pkg']
Expand Down
1 change: 1 addition & 0 deletions repos/system_upgrade/el7toel8/libraries/rhui.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
AZURE_MAP = {
'el7_pkg': 'rhui-azure-rhel7',
'el8_pkg': 'rhui-azure-rhel8',
'agent_pkg': 'WALinuxAgent',
'leapp_pkg': 'leapp-rhui-azure',
'leapp_pkg_repo': 'leapp-azure.repo'
}
Expand Down
12 changes: 12 additions & 0 deletions repos/system_upgrade/el7toel8/models/grubenv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from leapp.models import Model, fields
from leapp.topics import SystemFactsTopic


class HybridImage(Model):
"""
Model used for instructing Leapp to convert "grubenv" symlink
into a regular file in case of hybrid (BIOS/EFI) images using BIOS
on Azure.
"""
topic = SystemFactsTopic
detected = fields.Boolean(default=False)

0 comments on commit dce08fc

Please sign in to comment.