From 88a81e5fbbbb5761c489dcee496dd466048ddca7 Mon Sep 17 00:00:00 2001 From: Roman Prilipskii Date: Wed, 6 May 2026 09:50:10 -0400 Subject: [PATCH 1/2] CLOS-3465: Inhibit upgrade when _netdev found in fstab During in-place upgrade, leapp disconnects the system from the network before the first reboot. Any fstab entry with the _netdev option relies on network availability and cannot be mounted at that stage, causing the upgrade to fail. Add check_netdev_mounts() to the CheckMountOptions actor to detect such entries and report an inhibitor. The check is bypassed when LEAPP_DEVEL_INITRAM_NETWORK is set (network available in initramfs). Co-Authored-By: Claude Sonnet 4.6 --- .../common/actors/checkmountoptions/actor.py | 1 + .../libraries/checkmountoptions.py | 43 +++++++++++++++++++ .../tests/test_checkmountoptions.py | 38 +++++++++++++++- 3 files changed, 81 insertions(+), 1 deletion(-) 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..1b760662ea 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,48 @@ def check_noexec_on_var(storage_info): return +def check_netdev_mounts(storage_info): + """Check for fstab entries with the _netdev mount option.""" + 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(',') + ] + + 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:\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=( + 'Remove the _netdev option from the affected /etc/fstab entries before proceeding ' + 'with the upgrade. Add it back after the upgrade is complete if needed.' + ) + ), + 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..fd5838fe8f 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,39 @@ 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), + ] +) +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'] From 8ccb119ddee9a87d70fa580238b8e00b88eaff7f Mon Sep 17 00:00:00 2001 From: Roman Prilipskii Date: Mon, 11 May 2026 22:04:55 -0400 Subject: [PATCH 2/2] CLOS-3465: Skip _netdev entries that also set nofail When an fstab entry pairs _netdev with nofail, systemd will not fail the boot if the mount cannot be brought up - the standard idiom for non- critical network mounts. Inhibiting these would be a false positive: the upgrade proceeds past the first reboot whether the mount succeeds or not. Validated on a CL7 test VM (template USERLAND-AUTO-all_in_one-CL7.stable.cpanel-11.110): - _netdev,defaults -> inhibitor fires (3 total inhibitors) - _netdev,nofail -> inhibitor does not fire (2 total inhibitors, grep -c 'netdev' on report = 0) Also reword summary/remediation to mention the nofail escape hatch. Co-Authored-By: Claude Opus 4.7 --- .../libraries/checkmountoptions.py | 22 +++++++++++++------ .../tests/test_checkmountoptions.py | 3 +++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/repos/system_upgrade/common/actors/checkmountoptions/libraries/checkmountoptions.py b/repos/system_upgrade/common/actors/checkmountoptions/libraries/checkmountoptions.py index 1b760662ea..c0c9fc3de3 100644 --- a/repos/system_upgrade/common/actors/checkmountoptions/libraries/checkmountoptions.py +++ b/repos/system_upgrade/common/actors/checkmountoptions/libraries/checkmountoptions.py @@ -72,13 +72,19 @@ def check_noexec_on_var(storage_info): def check_netdev_mounts(storage_info): - """Check for fstab entries with the _netdev mount option.""" + """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: @@ -94,15 +100,17 @@ def check_netdev_mounts(storage_info): '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:\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) + '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=( - 'Remove the _netdev option from the affected /etc/fstab entries before proceeding ' - 'with the upgrade. Add it back after the upgrade is complete if needed.' + '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'), 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 fd5838fe8f..01155142b7 100644 --- a/repos/system_upgrade/common/actors/checkmountoptions/tests/test_checkmountoptions.py +++ b/repos/system_upgrade/common/actors/checkmountoptions/tests/test_checkmountoptions.py @@ -75,6 +75,9 @@ def _make_fstab_entry(fs_spec, fs_file, fs_mntops, fs_vfstype='ext4'): ('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):