Skip to content

Commit 7b05e67

Browse files
lemmiFizzadar
authored andcommitted
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 c3b0869 commit 7b05e67

File tree

2 files changed

+69
-45
lines changed

2 files changed

+69
-45
lines changed

pyinfra/facts/server.py

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -206,34 +206,53 @@ class Mounts(FactBase[Dict[str, MountsDict]]):
206206

207207
@override
208208
def command(self):
209-
return "mount"
209+
return "cat /proc/self/mountinfo"
210210

211211
@override
212212
def process(self, output) -> dict[str, MountsDict]:
213213
devices: dict[str, MountsDict] = {}
214214

215-
for line in output:
216-
is_map = False
217-
if line.startswith("map "):
218-
line = line[4:]
219-
is_map = True
220-
221-
device, _, path, other_bits = line.split(" ", 3)
215+
def unescape_octal(match: re.Match) -> str:
216+
s = match.group(0)[1:] # skip the backslash
217+
return chr(int(s, base=8))
222218

223-
if is_map:
224-
device = "map {0}".format(device)
219+
def replace_octal(s: str) -> str:
220+
"""
221+
Unescape strings encoded by linux's string_escape_mem with ESCAPE_OCTAL flag.
222+
"""
223+
return re.sub(r"\\[0-7]{3}", unescape_octal, s)
225224

226-
if other_bits.startswith("type"):
227-
_, type_, options = other_bits.split(" ", 2)
228-
options = options.strip("()").split(",")
229-
else:
230-
options = other_bits.strip("()").split(",")
231-
type_ = options.pop(0)
232-
233-
devices[path] = {
234-
"device": device,
235-
"type": type_,
236-
"options": [option.strip() for option in options],
225+
for line in output:
226+
# ignore mount ID, parent ID, major:minor, root
227+
_, _, _, _, mount_point, mount_options, line = line.split(sep=" ", maxsplit=6)
228+
229+
# ignore optional tags "shared", "master", "propagate_from" and "unbindable"
230+
while True:
231+
optional, line = line.split(sep=" ", maxsplit=1)
232+
if optional == "-":
233+
break
234+
235+
fs_type, mount_source, super_options = line.split(sep=" ")
236+
237+
mount_options = mount_options.split(sep=",")
238+
239+
# escaped: mount_point, mount_source, super_options
240+
# these strings can contain characters encoded in octal, e.g. '\054' for ','
241+
mount_point = replace_octal(mount_point)
242+
mount_source = replace_octal(mount_source)
243+
244+
# mount_options will override ro/rw and can be different than the super block options
245+
# filter them, so they don't appear twice
246+
super_options = [
247+
replace_octal(opt)
248+
for opt in super_options.split(sep=",")
249+
if opt not in ["ro", "rw"]
250+
]
251+
252+
devices[mount_point] = {
253+
"device": mount_source,
254+
"type": fs_type,
255+
"options": mount_options + super_options,
237256
}
238257

239258
return devices

tests/facts/server.Mounts/mounts.json

Lines changed: 29 additions & 24 deletions
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)