Skip to content

Commit 72b64f1

Browse files
committedMar 18, 2025
facts: server.Mounts: fix whitespaces and escaped characters
The `mount` command can not be parsed unambigiously. Whitespaces in paths and , in option lists are not escaped. See the new tests for examples the old code can not handle. This patch switches to reading from `/proc/self/mountinfo` instead (see man 5 proc for documentation) where strings are properly escaped. While `/proc/self/mountinfo` provides a lot more details, these are ignored to keep compatibility with the old type. If there is a need, the missing fields can be easily added.
1 parent efb7cb3 commit 72b64f1

File tree

2 files changed

+60
-46
lines changed

2 files changed

+60
-46
lines changed
 

‎pyinfra/facts/server.py

+31-22
Original file line numberDiff line numberDiff line change
@@ -189,33 +189,42 @@ class Mounts(FactBase[Dict[str, MountsDict]]):
189189
default = dict
190190

191191
def command(self):
192-
return "mount"
192+
return "cat /proc/self/mountinfo"
193193

194194
def process(self, output) -> dict[str, MountsDict]:
195195
devices: dict[str, MountsDict] = {}
196196

197197
for line in output:
198-
is_map = False
199-
if line.startswith("map "):
200-
line = line[4:]
201-
is_map = True
202-
203-
device, _, path, other_bits = line.split(" ", 3)
204-
205-
if is_map:
206-
device = "map {0}".format(device)
207-
208-
if other_bits.startswith("type"):
209-
_, type_, options = other_bits.split(" ", 2)
210-
options = options.strip("()").split(",")
211-
else:
212-
options = other_bits.strip("()").split(",")
213-
type_ = options.pop(0)
214-
215-
devices[path] = {
216-
"device": device,
217-
"type": type_,
218-
"options": [option.strip() for option in options],
198+
# ignore mount ID, parent ID, major:minor, root
199+
_, _, _, _, mount_point, mount_options, line = line.split(sep=" ", maxsplit=6)
200+
201+
# ignore optional tags "shared", "master", "propagate_from" and "unbindable"
202+
while True:
203+
optional, line = line.split(maxsplit=1)
204+
if optional == "-":
205+
break
206+
207+
fs_type, mount_source, super_options = line.split(sep=" ")
208+
209+
mount_options = mount_options.split(sep=",")
210+
211+
# escaped: mount_point, mount_source, super_options
212+
# these strings can contain characters encoded in octal, e.g. '\054' for ','
213+
mount_point = mount_point.encode().decode("unicode-escape")
214+
mount_source = mount_source.encode().decode("unicode-escape")
215+
216+
# mount_options will override ro/rw and can be different than the super block options
217+
# filter them, so they don't appear twice
218+
super_options = [
219+
opt.encode().decode("unicode-escape")
220+
for opt in super_options.split(sep=",")
221+
if opt not in ["ro", "rw"]
222+
]
223+
224+
devices[mount_point] = {
225+
"device": mount_source,
226+
"type": fs_type,
227+
"options": mount_options + super_options,
219228
}
220229

221230
return devices

‎tests/facts/server.Mounts/mounts.json

+29-24
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,44 @@
11
{
2-
"command": "mount",
2+
"command": "cat /proc/self/mountinfo",
33
"output": [
4-
"/dev/sda1 on /boot type ext2 (rw,relatime,block_validity,barrier,user_xattr,acl)",
5-
"/dev/disk1s4 on /private/var/vm (apfs, local, noexec, journaled, noatime, nobrowse)",
6-
"map thing on / type something ()"
4+
"2 1 0:26 / / ro,noatime - ext4 /dev/sdb1 rw",
5+
"33 29 8:1 / /boot rw,relatime - vfat /dev/sda1 rw,lazytime,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro",
6+
"408 35 0:352 / /mnt/overlay/overlay,dir\\040with\\040spaces\\040and\\040\\134backslashes ro,relatime shared:27 - overlay none ro,lowerdir=lower\\054dir,upperdir=upper\\054dir,workdir=work\\054dir"
77
],
88
"fact": {
9+
"/": {
10+
"device": "/dev/sdb1",
11+
"type": "ext4",
12+
"options": [
13+
"ro",
14+
"noatime"
15+
]
16+
},
917
"/boot": {
1018
"device": "/dev/sda1",
11-
"type": "ext2",
19+
"type": "vfat",
1220
"options": [
1321
"rw",
1422
"relatime",
15-
"block_validity",
16-
"barrier",
17-
"user_xattr",
18-
"acl"
23+
"lazytime",
24+
"fmask=0022",
25+
"dmask=0022",
26+
"codepage=437",
27+
"iocharset=iso8859-1",
28+
"shortname=mixed",
29+
"utf8",
30+
"errors=remount-ro"
1931
]
2032
},
21-
"/private/var/vm": {
22-
"device": "/dev/disk1s4",
23-
"type": "apfs",
33+
"/mnt/overlay/overlay,dir with spaces and \\backslashes": {
34+
"device": "none",
35+
"type": "overlay",
2436
"options": [
25-
"local",
26-
"noexec",
27-
"journaled",
28-
"noatime",
29-
"nobrowse"
30-
]
31-
},
32-
"/": {
33-
"device": "map thing",
34-
"type": "something",
35-
"options": [
36-
""
37+
"ro",
38+
"relatime",
39+
"lowerdir=lower,dir",
40+
"upperdir=upper,dir",
41+
"workdir=work,dir"
3742
]
3843
}
3944
}

0 commit comments

Comments
 (0)
Failed to load comments.