diff --git a/repos/system_upgrade/common/actors/checkmountoptions/actor.py b/repos/system_upgrade/common/actors/checkmountoptions/actor.py index ce0e01c7ad..25f03c1436 100644 --- a/repos/system_upgrade/common/actors/checkmountoptions/actor.py +++ b/repos/system_upgrade/common/actors/checkmountoptions/actor.py @@ -11,6 +11,7 @@ class CheckMountOptions(Actor): Checks performed: - /var is mounted with the noexec option + - any fstab entry uses the _netdev mount option """ name = "check_mount_options" consumes = (StorageInfo,) diff --git a/repos/system_upgrade/common/actors/checkmountoptions/libraries/checkmountoptions.py b/repos/system_upgrade/common/actors/checkmountoptions/libraries/checkmountoptions.py index 869c92f9b2..c0c9fc3de3 100644 --- a/repos/system_upgrade/common/actors/checkmountoptions/libraries/checkmountoptions.py +++ b/repos/system_upgrade/common/actors/checkmountoptions/libraries/checkmountoptions.py @@ -1,4 +1,5 @@ from leapp import reporting +from leapp.libraries.common.config import get_env from leapp.libraries.stdlib import api from leapp.models import StorageInfo @@ -70,6 +71,56 @@ def check_noexec_on_var(storage_info): return +def check_netdev_mounts(storage_info): + """Check for fstab entries with the _netdev mount option without nofail. + + Entries combining _netdev with nofail are skipped: nofail tells systemd not + to fail the boot if the mount fails, so the upgrade can proceed even though + the network mount itself will not come up before the first reboot. + """ + if get_env('LEAPP_DEVEL_INITRAM_NETWORK', None): + return + + netdev_entries = [ + entry for entry in storage_info.fstab + if '_netdev' in entry.fs_mntops.split(',') + and 'nofail' not in entry.fs_mntops.split(',') + ] + + if not netdev_entries: + return + + entries_str = '\n'.join( + '- {} (mounted at {})'.format(entry.fs_spec, entry.fs_file) + for entry in netdev_entries + ) + + reporting.create_report([ + reporting.Title( + 'Detected _netdev mount option in /etc/fstab, preventing a successful in-place upgrade.' + ), + reporting.Summary( + 'Leapp detected one or more entries in /etc/fstab using the _netdev mount option ' + 'without nofail:\n{}\n\n' + 'During the in-place upgrade, the system is disconnected from the network before ' + 'the first reboot. Entries with the _netdev option cannot be mounted at that point, ' + 'which causes the upgrade to fail.'.format(entries_str) + ), + reporting.Remediation( + hint=( + 'Either remove the _netdev option from the affected /etc/fstab entries before ' + 'proceeding with the upgrade (and add it back afterwards if needed), or add the ' + 'nofail option so the boot does not fail when the network mount is unavailable.' + ) + ), + reporting.RelatedResource('file', '/etc/fstab'), + reporting.Severity(reporting.Severity.HIGH), + reporting.Groups([reporting.Groups.FILESYSTEM, reporting.Groups.NETWORK]), + reporting.Groups([reporting.Groups.INHIBITOR]), + ]) + + def check_mount_options(): for storage_info in api.consume(StorageInfo): check_noexec_on_var(storage_info) + check_netdev_mounts(storage_info) diff --git a/repos/system_upgrade/common/actors/checkmountoptions/tests/test_checkmountoptions.py b/repos/system_upgrade/common/actors/checkmountoptions/tests/test_checkmountoptions.py index de19856bcc..01155142b7 100644 --- a/repos/system_upgrade/common/actors/checkmountoptions/tests/test_checkmountoptions.py +++ b/repos/system_upgrade/common/actors/checkmountoptions/tests/test_checkmountoptions.py @@ -4,7 +4,7 @@ import pytest from leapp import reporting -from leapp.libraries.actor.checkmountoptions import check_mount_options +from leapp.libraries.actor.checkmountoptions import check_mount_options, check_netdev_mounts from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked from leapp.libraries.stdlib import api from leapp.models import FstabEntry, MountEntry, StorageInfo @@ -59,3 +59,42 @@ def test_var_mounted_with_noexec_is_detected(monkeypatch, fstab_entries, mounts, check_mount_options() assert bool(created_reports.called) == should_inhibit + + +def _make_fstab_entry(fs_spec, fs_file, fs_mntops, fs_vfstype='ext4'): + return FstabEntry(fs_spec=fs_spec, fs_file=fs_file, fs_vfstype=fs_vfstype, + fs_mntops=fs_mntops, fs_freq='0', fs_passno='0') + + +@pytest.mark.parametrize( + ('fstab_mntops', 'initram_network_envar', 'should_inhibit'), + [ + ('_netdev,defaults', None, True), + ('defaults,_netdev', None, True), + ('_netdev', None, True), + ('defaults', None, False), + ('netdev,defaults', None, False), + ('_netdev,defaults', '1', False), + ('_netdev,nofail', None, False), + ('nofail,_netdev,defaults', None, False), + ('_netdev,nofail,defaults', None, False), + ] +) +def test_netdev_in_fstab_is_detected(monkeypatch, fstab_mntops, initram_network_envar, should_inhibit): + fstab_entries = [ + _make_fstab_entry('UUID=abc123', '/var/lib/psa/dumps', fstab_mntops), + _make_fstab_entry('/dev/sda1', '/', 'defaults'), + ] + storage_info = StorageInfo(fstab=fstab_entries) + + envars = {'LEAPP_DEVEL_INITRAM_NETWORK': initram_network_envar} if initram_network_envar else {} + created_reports = create_report_mocked() + monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(envars=envars)) + monkeypatch.setattr(reporting, 'create_report', created_reports) + + check_netdev_mounts(storage_info) + + assert bool(created_reports.called) == should_inhibit + if should_inhibit: + assert '_netdev' in created_reports.report_fields['title'] + assert 'UUID=abc123' in created_reports.report_fields['summary']