diff --git a/data/10-osbuild-inhibitor.rules b/data/10-osbuild-inhibitor.rules new file mode 100644 index 00000000..2397753d --- /dev/null +++ b/data/10-osbuild-inhibitor.rules @@ -0,0 +1,39 @@ +# This file is part of osbuild +# +# A set of rules that implements an inhibition mechanism that allows +# osbuild to suppress the execution of certain udev rules for block +# devices that are known to cause problems for its use case. +# +# osbuild indicates that it wants to inhibit a device by creating a +# lock file in a special lock folder which is then in turn detected +# by this set of rules. As a result various environment variables +# are set that should inhibit the other udev rules. +# +# A udev property called 'OSBUILD_INHIBIT' will be set for devices +# that are inhibited via this set of rules. + +SUBSYSTEM!="block", GOTO="osbuild_end" +ACTION=="remove", GOTO="osbuild_end" + +# Support locking via device major and minor numbers +TEST=="/run/osbuild/locks/udev/device-$major:$minor", ENV{OSBUILD_INHIBIT}="1" + +# Support locking via the "device mapper" device name +ENV{DM_NAME}=="?*", TEST=="/run/osbuild/locks/udev/dm-$env{DM_NAME}", ENV{OSBUILD_INHIBIT}="1" + +# Setting `DM_UDEV_DISABLE_OTHER_RULES_FLAG` should prevent the processing +# of the device by device mapper related rules; specifically it should +# prevent lvm2 from scanning the device and activating its volume groups +# and logival volumes. +# On Fedora/RHEL these rules are in '69-dm-lvm-metad.rules' +ENV{OSBUILD_INHIBIT}=="1", ENV{DM_UDEV_DISABLE_OTHER_RULES_FLAG}="1" + +# Setting `UDEV_DISABLE_PERSISTENT_STORAGE_RULES_FLAG` should opt-out of +# the creation of various links in udev that are useful for host devices +# but are not needed by osbuild and might even conflict if multiple +# concurrent builds are ongoing which use the same device labels, like +# like "disk/by-label". +# On Fedora/RHEL these rules are in '60-persistent-storage.rules' +ENV{OSBUILD_INHIBIT}=="1", ENV{UDEV_DISABLE_PERSISTENT_STORAGE_RULES_FLAG}="1" + +LABEL="osbuild_end" diff --git a/osbuild.spec b/osbuild.spec index f0cec68e..1fab06df 100644 --- a/osbuild.spec +++ b/osbuild.spec @@ -21,6 +21,7 @@ Summary: A build system for OS images BuildRequires: make BuildRequires: python3-devel BuildRequires: python3-docutils +BuildRequires: systemd Requires: bash Requires: bubblewrap @@ -164,6 +165,10 @@ install -p -m 0644 -t %{buildroot}%{_mandir}/man5/ docs/*.5 install -D -m 0644 -t %{buildroot}%{_datadir}/selinux/packages/%{selinuxtype} %{name}.pp.bz2 install -D -m 0644 -t %{buildroot}%{_mandir}/man8 selinux/%{name}_selinux.8 +# Udev rules +mkdir -p %{buildroot}%{_udevrulesdir} +install -p -m 0755 data/10-osbuild-inhibitor.rules %{buildroot}%{_udevrulesdir} + %check exit 0 # We have some integration tests, but those require running a VM, so that would @@ -176,6 +181,7 @@ exit 0 %{_mandir}/man5/%{name}-manifest.5* %{_datadir}/osbuild/schemas %{pkgdir} +%{_udevrulesdir}/*.rules # the following files are in the lvm2 sub-package %exclude %{pkgdir}/devices/org.osbuild.lvm2* %exclude %{pkgdir}/stages/org.osbuild.lvm2* diff --git a/osbuild/util/udev.py b/osbuild/util/udev.py new file mode 100644 index 00000000..6236ce45 --- /dev/null +++ b/osbuild/util/udev.py @@ -0,0 +1,59 @@ +"""userspace /dev device manager (udev) utilities""" + +import contextlib +import pathlib + + +# The default lock dir to use +LOCKDIR = "/run/osbuild/locks/udev" + + +class UdevInhibitor: + """ + Inhibit execution of certain udev rules for block devices + + This is the osbuild side of the custom mechanism that + allows us to inhibit certain udev rules for block devices. + + For each device a lock file is created in a well known + directory (LOCKDIR). A custom udev rule set[1] checks + for the said lock file and inhibits other udev rules from + being executed. + See the aforementioned rules file for more information. + + [1] 10-osbuild-inhibitor.rules + """ + + def __init__(self, path: pathlib.Path): + self.path = path + path.parent.mkdir(parents=True, exist_ok=True) + + def inhibit(self) -> None: + self.path.touch() + + def release(self) -> None: + with contextlib.suppress(FileNotFoundError): + self.path.unlink() + + @property + def active(self) -> bool: + return self.path.exists() + + def __str__(self): + return f"UdevInhibtor at '{self.path}'" + + @classmethod + def for_dm_name(cls, name: str, lockdir=LOCKDIR): + """Inhibit a Device Mapper device with the given name""" + path = pathlib.Path(lockdir, f"dm-{name}") + ib = cls(path) + ib.inhibit() + return ib + + @classmethod + def for_device(cls, major: int, minor: int, lockdir=LOCKDIR): + """Inhibit a device given its major and minor number""" + path = pathlib.Path(lockdir, f"device-{major}-{minor}") + ib = cls(path) + ib.inhibit() + return ib diff --git a/test/mod/test_util_udev.py b/test/mod/test_util_udev.py new file mode 100644 index 00000000..d7ea53c8 --- /dev/null +++ b/test/mod/test_util_udev.py @@ -0,0 +1,32 @@ +# +# Test for the util.udev module +# + +from tempfile import TemporaryDirectory + +import pytest + +from osbuild.util.udev import UdevInhibitor + + +@pytest.fixture(name="tempdir") +def tempdir_fixture(): + with TemporaryDirectory(prefix="udev-") as tmp: + yield tmp + + +def test_udev_inhibitor(tempdir): + ib = UdevInhibitor.for_dm_name("test", lockdir=tempdir) + assert ib.active + + ib.release() + assert not ib.active + + ib = UdevInhibitor.for_device(7, 1, lockdir=tempdir) + assert ib.active + + ib.release() + assert not ib.active + + ib.release() + assert not ib.active