summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHelmut Grohne <helmut@subdivi.de>2024-02-15 22:31:51 +0100
committerHelmut Grohne <helmut@subdivi.de>2024-02-15 22:50:26 +0100
commitd8ecc510108426fba8f9a53e0d5fa54d5942e75f (patch)
treed37c1fe311be473745df1717bc213afb976133eb
parent72ff759384538d412b5114d72b3115b730c06406 (diff)
downloadpython-linuxnamespaces-d8ecc510108426fba8f9a53e0d5fa54d5942e75f.tar.gz
MountFlags: support conversion to and from a textual representation
The textual representation matches util-linux. Not all flag values can be represented textually.
-rwxr-xr-xexamples/cgroup.py6
-rw-r--r--linuxnamespaces/syscalls.py117
-rw-r--r--tests/test_simple.py18
3 files changed, 136 insertions, 5 deletions
diff --git a/examples/cgroup.py b/examples/cgroup.py
index 06eb3b3..0c52efb 100755
--- a/examples/cgroup.py
+++ b/examples/cgroup.py
@@ -123,11 +123,7 @@ def main() -> None:
"tmpfs",
"/sys",
"tmpfs",
- linuxnamespaces.MountFlags.REMOUNT
- | linuxnamespaces.MountFlags.RDONLY
- | linuxnamespaces.MountFlags.NOEXEC
- | linuxnamespaces.MountFlags.NOSUID
- | linuxnamespaces.MountFlags.NODEV,
+ linuxnamespaces.MountFlags.fromstr("remount,ro,noexec,nosuid,nodev"),
"mode=0755",
)
linuxnamespaces.move_mount(cgroupfd, "/sys/fs/cgroup")
diff --git a/linuxnamespaces/syscalls.py b/linuxnamespaces/syscalls.py
index 0e33a44..338e602 100644
--- a/linuxnamespaces/syscalls.py
+++ b/linuxnamespaces/syscalls.py
@@ -112,6 +112,123 @@ class MountFlags(enum.IntFlag):
PROPAGATION_FLAGS = UNBINDABLE | PRIVATE | SLAVE | SHARED
+ # Map each flag to:
+ # * The flag value
+ # * Whether the flag value is negated
+ # * Whether the flag must be negated
+ # * Whether the flag can be negated
+ __flagstrmap = {
+ "acl": (POSIXACL, False, False, False),
+ "async": (SYNCHRONOUS, True, False, False),
+ "atime": (NOATIME, True, False, True),
+ "bind": (BIND, False, False, False),
+ "dev": (NODEV, True, False, True),
+ "diratime": (NODIRATIME, True, False, True),
+ "dirsync": (DIRSYNC, False, False, False),
+ "exec": (NOEXEC, True, False, True),
+ "iversion": (I_VERSION, False, False, True),
+ "lazytime": (LAZYTIME, False, False, True),
+ "loud": (SILENT, True, False, False),
+ "mand": (MANDLOCK, False, False, True),
+ "private": (PRIVATE, False, False, False),
+ "rbind": (BIND | REC, False, False, False),
+ "relatime": (RELATIME, False, False, True),
+ "remount": (REMOUNT, False, False, True),
+ "ro": (RDONLY, False, False, False),
+ "rprivate": (PRIVATE | REC, False, False, False),
+ "rshared": (SHARED | REC, False, False, False),
+ "rslave": (SLAVE | REC, False, False, False),
+ "runbindable": (UNBINDABLE | REC, False, False, False),
+ "rw": (RDONLY, True, False, False),
+ "shared": (SHARED, False, False, False),
+ "silent": (SILENT, False, False, False),
+ "slave": (SLAVE, False, False, False),
+ "strictatime": (STRICTATIME, False, False, True),
+ "suid": (NOSUID, True, False, True),
+ "symfollow": (NOSYMFOLLOW, True, False, True),
+ "sync": (SYNCHRONOUS, False, False, False),
+ "unbindable": (UNBINDABLE, False, False, False),
+ }
+
+ def change(self, flagsstr: str) -> "MountFlags":
+ """Return modified mount flags after applying comma-separated mount
+ flags represented as a str. Raise a ValueError if any given flag
+ does not correspond to a textual mount flag.
+ """
+ ret = self
+ for flagstr in flagsstr.split(","):
+ if not flagstr:
+ continue
+ flag, negated, mustnegate, cannegate = self.__flagstrmap.get(
+ flagstr.removeprefix("no"),
+ (MountFlags.NONE, False, True, False),
+ )
+ if mustnegate <= flagstr.startswith("no") <= cannegate:
+ if negated ^ flagstr.startswith("no"):
+ ret &= ~flag
+ else:
+ if flag & MountFlags.PROPAGATION_FLAGS:
+ ret &= ~MountFlags.PROPAGATION_FLAGS
+ ret |= flag
+ else:
+ raise ValueError(f"not a valid mount flag: {flagstr!r}")
+ return ret
+
+ @staticmethod
+ def fromstr(flagsstr: str) -> "MountFlags":
+ """Construct mount flags by changing flags according to the passed
+ flagsstr using the change method on an initial value with all flags
+ cleared.
+ """
+ return MountFlags.NONE.change(flagsstr)
+
+ __flagvals: list[tuple[int, str]] = sorted(
+ [
+ (RDONLY, "ro"),
+ (NOSUID, "nosuid"),
+ (NODEV, "nodev"),
+ (NOEXEC, "noexec"),
+ (SYNCHRONOUS, "sync"),
+ (REMOUNT, "remount"),
+ (MANDLOCK, "mand"),
+ (DIRSYNC, "dirsync"),
+ (NOSYMFOLLOW, "nosymfollow"),
+ (NOATIME, "noatime"),
+ (NODIRATIME, "nodiratime"),
+ (BIND, "bind"),
+ (BIND | REC, "rbind"),
+ (SILENT, "silent"),
+ (POSIXACL, "acl"),
+ (UNBINDABLE, "unbindable"),
+ (UNBINDABLE | REC, "runbindable"),
+ (PRIVATE, "private"),
+ (PRIVATE | REC, "rprivate"),
+ (SLAVE, "slave"),
+ (SLAVE | REC, "rslave"),
+ (SHARED, "shared"),
+ (SHARED | REC, "rshared"),
+ (RELATIME, "relatime"),
+ (I_VERSION, "iversion"),
+ (STRICTATIME, "strictatime"),
+ (LAZYTIME, "lazytime"),
+ ],
+ reverse=True
+ )
+
+ def tostr(self) -> str:
+ """Attempt to represent the flags in a comma-separated, textual way."""
+ if (self & MountFlags.PROPAGATION_FLAGS).bit_count() > 1:
+ raise ValueError("cannot represent conflicting propagtion flags")
+ parts: list[str] = []
+ remain = self
+ for val, text in MountFlags.__flagvals:
+ if remain & val == val:
+ parts.insert(0, text)
+ remain &= ~val
+ if remain:
+ raise ValueError("cannot represent flags {remain}")
+ return ",".join(parts)
+
class MountSetattrFlags(enum.IntFlag):
"""This value may be supplied as flags to mount_setattr(2)."""
diff --git a/tests/test_simple.py b/tests/test_simple.py
index cb654aa..960bf02 100644
--- a/tests/test_simple.py
+++ b/tests/test_simple.py
@@ -12,6 +12,24 @@ import pytest
import linuxnamespaces
+class MountFlagsTest(unittest.TestCase):
+ def test_tostrfromstr(self) -> None:
+ for bit1 in range(32):
+ for bit2 in range(bit1, 32):
+ flag = (
+ linuxnamespaces.MountFlags(1 << bit1)
+ | linuxnamespaces.MountFlags(1 << bit2)
+ )
+ try:
+ text = flag.tostr()
+ except ValueError:
+ continue
+ self.assertEqual(
+ linuxnamespaces.MountFlags.fromstr(text),
+ flag
+ )
+
+
class IDAllocationTest(unittest.TestCase):
def test_idalloc(self) -> None:
alloc = linuxnamespaces.IDAllocation()